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:

TSX
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 null while 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:

TSX
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:

TSX
// 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:

TSX
<Defer fallback={<Loading />}>
    {/* Both chunks must load before content shows */}
    <LazyHeader />
    <LazyContent />
</Defer>

Nest <Defer> boundaries for more granular loading states:

TSX
<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:

TSX
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:

TSX
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.

ParameterTypeDescription
loader() => Promise<Component>Function returning a dynamic import

Returns: LazyComponentFactory - Component with preload() and isLoaded() methods

TSX
// 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.

PropTypeDescription
fallbackJSXElement | () => JSXElementContent to show while a chunk beneath it loads

isLazyComponent(component)#

Check if a component is a lazy-loaded component.

TSX
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:

TSX
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#

  1. Route-level splitting - Lazy load page components for the biggest impact:

    TSX
    const routes = [
        { path: '/', component: lazy(() => import('./pages/Home')) },
        { path: '/dashboard', component: lazy(() => import('./pages/Dashboard')) },
    ];
  2. Preload on intent - Start loading before the user needs it:

    TSX
    <Link
        href="/dashboard"
        onMouseEnter={() => DashboardPage.preload()}
    >
        Dashboard
    </Link>
  3. Group related components - Put related lazy components under one <Defer>:

    TSX
    <Defer fallback={<DashboardSkeleton />}>
        <DashboardHeader />
        <DashboardCharts />
        <DashboardTable />
    </Defer>
  4. Meaningful fallbacks - Use skeleton screens that match the content layout:

    TSX
    <Defer fallback={<TableSkeleton rows={10} columns={5} />}>
        <LazyDataTable />
    </Defer>

Next Steps#