Lazy Loading
SignalX provides built-in support for code splitting through lazy() and <Defer>. Load components on demand to reduce initial bundle size and improve performance.
<Defer> is the one tree-positional async wrapper, and its fallback covers lazy chunk loading — code-splitting. It is not a data-loading wrapper: a pending useData read renders through its own component's match() (value-first), never through a wrapper. See Data loading for that side.
Lazy Loading Components
Use lazy() to create a component that loads on first render:
import { lazy, Defer } from 'sigx';
// Component will be loaded when first rendered
const HeavyChart = lazy(() => import('./components/HeavyChart'));
// Usage with Defer
function App() {
return (
<Defer fallback={<div>Loading chart...</div>}>
<HeavyChart data={chartData} />
</Defer>
);
}
The lazy() function accepts an import function and returns a component that:
- Loads the component module on first render
- Renders
nullwhile the chunk loads — the nearest enclosing<Defer>shows its fallback - Renders the component in place once loaded
- Caches the loaded component for subsequent renders
Registration is deterministic: a lazy wrapper registers its load promise with the nearest <Defer> at setup time (before its own children set up), so there is no thrown-promise protocol and no render-ordering to reason about.
Defer Boundaries
<Defer> components define loading boundaries in your component tree:
import { lazy, Defer, component } from 'sigx';
const Dashboard = lazy(() => import('./Dashboard'));
const Charts = lazy(() => import('./Charts'));
const Reports = lazy(() => import('./Reports'));
const App = component(() => {
return () => (
<div class="app">
{/* Each section can load independently */}
<Defer fallback={<Skeleton type="dashboard" />}>
<Dashboard />
</Defer>
<div class="grid">
<Defer fallback={<Skeleton type="chart" />}>
<Charts />
</Defer>
<Defer fallback={<Skeleton type="report" />}>
<Reports />
</Defer>
</div>
</div>
);
});
The children stay mounted while a chunk loads (a pending lazy renders null), so sibling state survives and the resolved component appears in place.
Fallback Options
The fallback prop accepts either a JSX element or a function:
// Static fallback
<Defer fallback={<Spinner />}>
<LazyComponent />
</Defer>
// Dynamic fallback (called on each render)
<Defer fallback={() => <Spinner size={spinnerSize} />}>
<LazyComponent />
</Defer>
Nested Defer
Multiple lazy components under the same <Defer> wait together:
<Defer fallback={<Loading />}>
{/* Both chunks must load before content shows */}
<LazyHeader />
<LazyContent />
</Defer>
Nest <Defer> boundaries for more granular loading states:
<Defer fallback={<AppSkeleton />}>
<LazyLayout>
<Defer fallback={<ContentSkeleton />}>
<LazyContent />
</Defer>
</LazyLayout>
</Defer>
On the server, <Defer> streams its fallback with the shell and swaps in a single replacement once everything pending beneath it — chunks and keyed data — resolves.
Preloading Components
Lazy components expose a preload() method to start loading before render:
const HeavyEditor = lazy(() => import('./HeavyEditor'));
// Preload on hover
<button onMouseEnter={() => HeavyEditor.preload()}>
Open Editor
</button>
// Or preload on route navigation
router.beforeEach((to) => {
if (to.path === '/editor') {
HeavyEditor.preload();
}
});
Checking Load Status
Use isLoaded() to check if a lazy component has finished loading:
const LazyComponent = lazy(() => import('./Component'));
// Check status
if (LazyComponent.isLoaded()) {
console.log('Component is ready');
}
// Useful for conditional logic
function showStatus() {
return LazyComponent.isLoaded()
? 'Ready'
: 'Loading...';
}
API Reference
lazy(loader)
Creates a lazy-loaded component wrapper.
| Parameter | Type | Description |
|---|---|---|
loader | () => Promise<Component> | Function returning a dynamic import |
Returns: LazyComponentFactory - Component with preload() and isLoaded() methods
// Default export
const Comp = lazy(() => import('./Component'));
// Named export
const Comp = lazy(() =>
import('./Components').then(m => m.SpecificComponent)
);
Defer
Wrapper that shows fallback content while lazy children load.
| Prop | Type | Description |
|---|---|---|
fallback | JSXElement | () => JSXElement | Content to show while a chunk beneath it loads |
isLazyComponent(component)
Check if a component is a lazy-loaded component.
import { isLazyComponent } from 'sigx';
if (isLazyComponent(SomeComponent)) {
SomeComponent.preload();
}
Error Handling
If a chunk fails to load, the lazy wrapper throws the load error from render — it routes through the standard error path (the nearest errorScope, then app.onError), exactly like any render throw:
import { component, errorScope, lazy, Defer } from 'sigx';
const FailingComponent = lazy(() => import('./Broken')); // this import fails
const Section = component(() => {
errorScope({ fallback: (e, retry) => <LoadFailed error={e} retry={retry} /> });
return () => (
<Defer fallback={<Loading />}>
<FailingComponent />
</Defer>
);
});
retry remounts the subtree, so it re-attempts the import from a clean slate. See Error handling for the full model.
Best Practices
-
Route-level splitting - Lazy load page components for the biggest impact:
TSXconst routes = [ { path: '/', component: lazy(() => import('./pages/Home')) }, { path: '/dashboard', component: lazy(() => import('./pages/Dashboard')) }, ]; -
Preload on intent - Start loading before the user needs it:
TSX<Link href="/dashboard" onMouseEnter={() => DashboardPage.preload()} > Dashboard </Link> -
Group related components - Put related lazy components under one
<Defer>:TSX<Defer fallback={<DashboardSkeleton />}> <DashboardHeader /> <DashboardCharts /> <DashboardTable /> </Defer> -
Meaningful fallbacks - Use skeleton screens that match the content layout:
TSX<Defer fallback={<TableSkeleton rows={10} columns={5} />}> <LazyDataTable /> </Defer>
Next Steps
- Data loading -
useData,useAction,all - Error handling -
errorScopeandapp.onError - Portal - Render content outside the component tree
- Components - Component patterns and best practices
