Skip to content
olegivanov edited this page May 18, 2026 · 1 revision

Lazy

1. Overview

  • Name: Lazy
  • Purpose: Lazy-load a Svelte component at runtime via a dynamic import() and render it with an optional fallback while loading.
  • When to use: Code-split a route screen, gate a heavy feature behind on-demand loading, or defer optional UI not needed on first paint.
  • Availability: @real-router/svelte only. React/Preact use lazy() + <Suspense>; Vue uses defineAsyncComponent; Solid uses lazy().

2. Import and Basic Usage

<script lang="ts">
  import { Lazy } from "@real-router/svelte";

  import Spinner from "./Spinner.svelte";
</script>

<Lazy loader={() => import("./HeavyChart.svelte")} fallback={Spinner} />

The dynamic import("./HeavyChart.svelte") resolves to a module whose default export is rendered when ready. Until then, <Spinner /> is shown.

3. Props

Prop Type Required Default Description
loader () => Promise<{ default: Component }> Yes Function that returns a Promise resolving to a module with a default export. Typically () => import(...)
fallback Component | undefined No undefined Component rendered while the loader is pending. Receives no props.

4. Behavior

Loader lifecycle

Lazy keeps internal state in a tagged union (loading | ready | error). On mount, an $effect invokes loader() and:

  • ready: stores the loaded default export as the active component and renders it.
  • error: if loader() rejects or resolves a module without a default export, the component renders a minimal <p>Error loading component: …</p> and never retries.
  • loading: the fallback (if provided) is rendered. Without a fallback, nothing is shown.

Cancellation on unmount

The $effect cleanup sets an internal active = false flag. If the loader resolves after unmount, state updates are skipped — no setState-after-unmount, no leak. Verified by tests/stress/lazy-loading.stress.ts (30 mount → immediate unmount cycles, discarded results stay bounded).

Loader is captured at mount

The loader function is invoked once on mount via the $effect. Passing a new loader reference reactively does NOT re-trigger the load. To swap content, remount via {#key}:

{#key tab}
  <Lazy loader={loaders[tab]} fallback={Spinner} />
{/key}

No SSR data hydration

Lazy uses $effect, which does NOT run on the server. SSR HTML contains only the fallback (or nothing). On hydration, $effect fires and the loader starts. For SSR-critical data, use state.context.data (via ssr-data-plugin) or a top-level await in <script>.

5. Examples

Route-level code split

<!-- Pages.svelte -->
<script lang="ts">
  import { Lazy, useRouteNode } from "@real-router/svelte";

  import Spinner from "./Spinner.svelte";

  const { route } = useRouteNode("");
</script>

{#if route.current?.name === "dashboard"}
  <Lazy loader={() => import("./Dashboard.svelte")} fallback={Spinner} />
{:else if route.current?.name === "settings"}
  <Lazy loader={() => import("./Settings.svelte")} fallback={Spinner} />
{/if}

No fallback (silent loading)

<Lazy loader={() => import("./BackgroundFeature.svelte")} />

Renders nothing until ready; useful for non-critical UI where a spinner would be visually noisy.

Catching a missing default export

A loader resolving without default triggers the error branch:

<Lazy
  loader={async () => {
    const m = await import("./module");
    return m; // No `default` — Lazy will render the error fallback.
  }}
/>

6. Gotchas

  • fallback is a Component, not a Snippet. Pass a Svelte component reference (import Spinner from "./Spinner.svelte") rather than a Snippet. The component receives no props.

  • No retry on failure. Once the loader rejects, the error state is terminal until the component is unmounted/remounted (use {#key retryCount} to force a fresh attempt).

  • Error message is plain text. The built-in <p>Error loading component: {err.message}</p> is intentionally minimal — wrap <Lazy> in an error boundary for richer UX:

    <RouterErrorBoundary>
      {#snippet children()}
        <Lazy loader={() => import("./Heavy.svelte")} fallback={Spinner} />
      {/snippet}
    </RouterErrorBoundary>
  • SSR HTML shows the fallback only. Async content cannot land in the initial HTML — content streams in post-hydration. For SSR data, see the <Streamed> / <Await> combo or ssr-data-plugin.

7. Dependencies

  • Bundler must support dynamic import() (Vite / Rollup / Webpack 5+ all do).
  • No router-context dependency — Lazy can render outside a <RouterProvider> (though the typical use case is inside one).

8. Related

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally