Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/app/news/+wrapping-react-preview.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Load the Wrapping React overview's Spline runtime and 3D preview automatically when the preview is visible and the browser has idle time.
93 changes: 93 additions & 0 deletions docs/app/tests/test_editorial_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,99 @@
)


@pytest.mark.parametrize("width", [390, 1440])
def test_spline_preview_waits_for_visibility_and_idle(page: Page, width: int):
"""Load the 3D preview only near the viewport, after the browser becomes idle."""
page.set_viewport_size({"width": width, "height": 600})
page.add_init_script("""
window.previewIdleCallbacks = new Map();
let nextId = 0;
window.requestIdleCallback = callback => {
const id = ++nextId;
window.previewIdleCallbacks.set(id, callback);
return id;
};
window.cancelIdleCallback = id => window.previewIdleCallbacks.delete(id);
""")
scripts = []
scenes = []
errors = []
page.on("pageerror", lambda error: errors.append(str(error)))
page.on(
"request",
lambda request: (
scripts.append(request.url)
if request.resource_type == "script" and request.url.startswith(PREVIEW_URL)
else None
),
)
page.on(
"request",
lambda request: (
scenes.append(request.url) if "scene.splinecode" in request.url else None
),
)
page.goto(f"{PREVIEW_URL}/docs/wrapping-react/overview/", wait_until="networkidle")
preview = page.locator(".docs-demo-preview").first
pending = "window.previewIdleCallbacks.size"
# Beyond the viewport and the observer's 200px margin.
assert preview.bounding_box()["y"] >= 800
assert page.evaluate(pending) == 0
initial_scripts = len(scripts)

preview.scroll_into_view_if_needed()
page.wait_for_function(f"{pending} > 0")
page.evaluate("window.scrollTo(0, 0)")
page.wait_for_function(f"{pending} === 0")

preview.scroll_into_view_if_needed()
page.wait_for_function(f"{pending} > 0")
tabs = page.locator('[role="tablist"]').first
tabs.get_by_role("tab", name="Code", exact=True).click()
page.wait_for_function(f"{pending} === 0")
expect(
page.get_by_text("class Spline(rx.NoSSRComponent):", exact=False)
).to_be_visible()
tabs.get_by_role("tab", name="View", exact=True).click()
preview.scroll_into_view_if_needed()
page.wait_for_function(f"{pending} > 0")
assert scenes == []
assert len(scripts) == initial_scripts

with page.expect_response("**/scene.splinecode") as scene:
page.evaluate("""() => {
for (const [id, callback] of window.previewIdleCallbacks) {
window.previewIdleCallbacks.delete(id);
callback({didTimeout: false, timeRemaining: () => 50});
}
}""")
assert scene.value.ok
expect(page.locator(".docs-demo-preview canvas")).to_be_visible()
# The deferred import fetched the wrapper's chunk only after the idle callback.
assert len(scripts) > initial_scripts

# A loaded preview remounts without waiting for idle time again.
tabs.get_by_role("tab", name="Code", exact=True).click()
tabs.get_by_role("tab", name="View", exact=True).click()
expect(page.locator(".docs-demo-preview canvas")).to_be_visible()
assert page.evaluate(pending) == 0
assert errors == []


@pytest.mark.parametrize("idle_callback", [True, False])
def test_spline_preview_loads_automatically(page: Page, idle_callback: bool):
"""Scrolling loads the preview with native idle callbacks or their fallback."""
page.set_viewport_size({"width": 390, "height": 600})
if not idle_callback:
page.add_init_script("delete window.requestIdleCallback")
page.goto(f"{PREVIEW_URL}/docs/wrapping-react/overview/", wait_until="networkidle")
expect(page.locator(".docs-demo-preview canvas")).to_have_count(0)
with page.expect_response("**/scene.splinecode") as scene:
page.locator(".docs-demo-preview").first.scroll_into_view_if_needed()
assert scene.value.ok
expect(page.locator(".docs-demo-preview canvas")).to_be_visible()


@pytest.mark.parametrize("width", [320, 375, 768, 1440])
@pytest.mark.parametrize("route", ["/docs/", "/docs/getting-started/introduction/"])
def test_editorial_header_stays_in_viewport(page: Page, width: int, route: str):
Expand Down
21 changes: 21 additions & 0 deletions docs/app/tests/test_overview_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@
]


def test_wrapping_overview_defers_the_spline_import_and_preview():
"""Reading the overview must not import or mount the 3D demo eagerly."""
body, _ = render_docgen_document(
"docs/wrapping-react/overview.md", DOCS / "wrapping-react/overview.md"
)
imports = body._get_all_imports()
spline_imports = [
item
for library, items in imports.items()
if library.startswith("@splinetool/react-spline@")
for item in items
]
assert spline_imports
assert all(not item.render for item in spline_imports)
assert any(
"import('@splinetool/react-spline')" in statement
for statement in body._get_all_dynamic_imports()
)
assert "DeferredDemo" in str(body)


@pytest.mark.parametrize("path", PAGES)
def test_overview_faq_schema_matches_visible_answers(path):
"""Every structured answer must also appear in the visible FAQ."""
Expand Down
6 changes: 4 additions & 2 deletions docs/wrapping-react/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Simple components that don't have any interaction can be wrapped with just a few

Below we show how to wrap the [Spline](https://github.com/splinetool/react-spline) library can be used to create 3D scenes and animations.

```python demo exec
The 3D preview loads automatically as you scroll to it. The wrapper uses [`rx.NoSSRComponent`](/docs/wrapping-react/library-and-tags/) so Spline is imported only when it mounts in the browser.

```python demo exec defer
import reflex as rx


class Spline(rx.Component):
class Spline(rx.NoSSRComponent):
"""Spline component."""

# The name of the npm package.
Expand Down
4 changes: 3 additions & 1 deletion packages/reflex-site-shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ the exported `render_docgen_document` helper directly.

### Expensive documentation previews

Add `defer` to a `python demo exec` fence to mount a heavy preview as it approaches the viewport. The example's source code remains in the initial HTML; the preview reserves 450px of height and stays mounted once shown. This is useful for pages containing many Plotly charts.
Add `defer` to a `python demo exec` fence to mount a heavy preview once it is within 200px of the viewport and the browser has idle time. A pending load is canceled when the reader scrolls away or switches to the Code tab, and a preview that has loaded remounts immediately when the reader returns to it. The example's source code remains in the initial HTML, and the preview reserves 450px of height. This is useful for pages containing many Plotly charts.

Wrap expensive libraries with `rx.NoSSRComponent` so their JavaScript is also imported only when the preview mounts.

## Marketing footer

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Load deferred documentation previews as they near the viewport and the browser has idle time, canceling pending loads when readers scroll away and remounting loaded previews immediately after a tab switch.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@


class DeferredDemo(rx.Component):
"""Mount a costly interactive preview when it approaches the viewport."""
"""Mount a costly interactive preview when visible and the browser is idle."""

library = "$/public/components/DeferredDemo"
tag = "DeferredDemo"

# Identifies the demo so it remounts immediately once shown, e.g. after a tab switch.
demo_id: rx.Var[str]


def _reflex_build_icon() -> rx.Component:
"""Create the Reflex Build mark for the demo action.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ def __init__(self, virtual_filepath: str = "", filename: str = "") -> None:
self.virtual_filepath = virtual_filepath
self.filename = filename
self.env: dict = {}
self.deferred_demos = 0

# ------------------------------------------------------------------
# Top-level
Expand Down Expand Up @@ -533,7 +534,10 @@ def _render_demo(self, content: str, flags: set[str]) -> rx.Component:
raise

if "defer" in flags:
comp = DeferredDemo.create(comp)
self.deferred_demos += 1
comp = DeferredDemo.create(
comp, demo_id=f"{self.virtual_filepath}#{self.deferred_demos}"
)

demobox_props: dict = {}
for flag in flags:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import { createElement, useEffect, useRef, useState } from "react";

/** Mount expensive previews once, as readers approach them. */
export function DeferredDemo({ children, style, ...props }) {
// Demos already shown in this session remount at once, e.g. after a tab switch.
const loadedDemos = new Set();

/** Mount expensive previews near the viewport once the browser has idle time. */
export function DeferredDemo({ children, style, demoId, ...props }) {
const element = useRef(null);
const [ready, setReady] = useState(false);
const [ready, setReady] = useState(() => loadedDemos.has(demoId));
useEffect(() => {
if (!("IntersectionObserver" in window)) {
if (ready) return;
let observer;
let scheduled = null;
const useIdleCallback = "requestIdleCallback" in window;
const cancel = () => {
if (scheduled === null) return;
if (useIdleCallback) window.cancelIdleCallback(scheduled);
else window.clearTimeout(scheduled);
scheduled = null;
};
const load = () => {
scheduled = null;
observer?.disconnect();
loadedDemos.add(demoId);
setReady(true);
return;
};
const schedule = () => {
scheduled ??= useIdleCallback
? window.requestIdleCallback(load, { timeout: 2000 })
: window.setTimeout(load, 0);
};
if ("IntersectionObserver" in window) {
observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) schedule();
else cancel();
},
{ rootMargin: "200px" },
);
observer.observe(element.current);
} else {
schedule();
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setReady(true);
observer.disconnect();
}
},
{ rootMargin: "200px" },
);
observer.observe(element.current);
return () => observer.disconnect();
}, []);
return () => {
observer?.disconnect();
cancel();
};
}, [ready, demoId]);
return createElement(
"div",
{
Expand Down
60 changes: 60 additions & 0 deletions tests/units/reflex_site_shared/docs/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from pathlib import Path

import pytest
from reflex_base.components.component import BaseComponent
from reflex_site_shared.components.blocks.demo import DeferredDemo
from reflex_site_shared.docs.markdown import (
get_docgen_toc,
get_markdown_toc,
Expand Down Expand Up @@ -31,6 +33,64 @@ def example():
assert "Shared live example" in str(component)


_DEFERRED_DEMO = """```python demo exec defer
import reflex as rx

def example():
return rx.text("Activated preview")
```"""


def _deferred_previews(
component: BaseComponent,
) -> list[DeferredDemo]:
"""Collect the deferred previews in a rendered component tree.

Args:
component: The rendered component tree.

Returns:
The deferred previews, in no particular order.
"""
pending = [component]
previews = []
while pending:
child = pending.pop()
if isinstance(child, DeferredDemo):
previews.append(child)
pending.extend(child.children)
return previews


def test_deferred_demo_preserves_code_and_gates_preview() -> None:
"""Automatic loading must gate the live preview without hiding its code."""
component = render_markdown(
_DEFERRED_DEMO, virtual_filepath="tests/deferred-demo.md"
)
previews = _deferred_previews(component)

assert len(previews) == 1
assert "Activated preview" in str(previews[0])
assert "rx.text" in str(component)


def test_deferred_demo_id_is_stable_and_unique_per_fence() -> None:
"""Each fence keeps its id across renders, even beside an identical fence."""

def demo_props(path: str) -> list[str]:
previews = _deferred_previews(
render_markdown(
f"{_DEFERRED_DEMO}\n\n{_DEFERRED_DEMO}", virtual_filepath=path
)
)
return sorted(str(preview.render()["props"]) for preview in previews)

first, second = demo_props("tests/a.md")
assert first != second
assert demo_props("tests/a.md") == [first, second]
assert set(demo_props("tests/b.md")).isdisjoint({first, second})


def test_render_markdown_covers_core_blocks_and_spans() -> None:
"""Render every core block type with inline span formatting."""
component = render_markdown(
Expand Down
Loading