Lazy Components
Declare a component as a dynamic import, and its code downloads only when it first renders:
import { Pending } from "@opentf/afw";
const Chart = () => import("./Chart.jsx");
export default function Dashboard() {
let show = $state(false);
return (
<>
<button onclick={() => (show = true)} onmouseenter={Chart}>Show chart</button>
{show && (
<Pending fallback={<ChartSkeleton />}>
<Chart data={points} onselect={pick} />
</Pending>
)}
</>
);
}Use it like any component: props, children,
on*callbacks, custom events andref.Its own chunk: the bundler splits
Chart.jsxout of the page, in both the esdev and the Vite workflow.Downloaded once: every
<Chart>on the page shares one download.
Lazy components ship in the next releases of @opentf/afw and @opentf/afw-compiler.
The shape the compiler recognizes
| Declaration | Lazy component? |
|---|---|
const Chart = () => import("./Chart.jsx") | Yes: the module's default export |
const Line = () => import("./charts.jsx").then((m) => m.LineChart) | Yes: a named export |
export const Chart = () => import("./Chart.jsx") | Yes: importable from other modules |
const chart = () => import("./Chart.jsx") | No: the name must start with a capital letter |
const Chart = (name) => import(pathFor(name)) | No: the loader takes no parameters and imports a literal path |
let Chart = …, async () => … | No: a const arrow that returns the import() directly |
At module level, in a
.jsxor.tsxmodule that defines a component. A declaration inside a component body is ordinary code.Anything else stays ordinary JavaScript. Only the exact shape above becomes a lazy component.
Loading and errors
Inside a
<Pending>, its fallback shows until the code arrives, like a resource.Outside one, nothing shows until the code arrives.
A failed download goes where other errors go: the
<Pending error>, the nearest<ErrorBoundary>, or the page'serror.jsx.Retry reloads the page. A browser keeps a failed
import()failed for the page's lifetime, so the code can only be fetched again by a reload.
Preloading
Calling the loader starts the download early:
<button onmouseenter={Chart} onclick={() => (show = true)}>Show chart</button>Chart()returns a promise that resolves when the code has loaded; rendering<Chart>afterwards is immediate.
Refs and $expose
A
refpoints at the placeholder element that stands in for the component.Exposed methods are forwarded once the code has loaded:
chart.value.zoomIn()works after the component renders. Before that, the method is not there yet.
Server rendering
Rendered in the browser only. The point of a lazy component is to keep its code out of the first load, so the server sends its empty placeholder (or the
<Pending>fallback around it).For content in the server HTML, import the component normally.
TypeScript
import type ChartComponent from "./Chart.jsx";
import type { Lazy } from "@opentf/afw";
const Chart = () => import("./Chart.jsx"); // inferred
const Typed: Lazy<typeof ChartComponent> = () => import("./Chart.jsx"); // written out
<Chart data={[1, 2]} />; // props are checked against Chart's ownNo annotation needed: TypeScript infers the module type, and JSX takes the props from the component the loader loads.
Lazy<C>is available to write the type out.