svelte-lazy Svelte Themes

Svelte Lazy

Lazy-loaded component slots for Svelte 5 with code splitting, reactive promises, and {#await} mounting.

svelte-lazy

Lazy-loaded component slots for Svelte 5. Wraps () => import('./Foo.svelte') in a reactive cell so you can pass the not-yet-loaded component across function, class, and component boundaries, then mount it with a normal {#await} block or the bundled <Lazy> wrapper.

npm install @coroama/svelte-lazy
# or
bun add @coroama/svelte-lazy

Peer dependency: svelte ^5.0.0. Built on top of @coroama/svelte-box, pulled in as a transitive; you do not install it directly.

Live demo: https://isaiahcoroama.github.io/svelte-lazy/. Source: src/routes/+page.svelte.

Contents

Why does this exist

Svelte 5 already supports dynamic components. Assign the constructor to a variable and mount with <Component {...props} />. Dynamic import() already produces code-split chunks. So why a library?

Because the glue between the two is repetitive and easy to get wrong:

<!-- illustrative pseudocode, annotate types if you copy this -->
<script>
    let Comp = $state(null);
    let loading = $state(false);
    let error = $state(null);

    async function load() {
        if (Comp || loading) return;
        loading = true;
        try {
            const mod = await import('./HeavyPanel.svelte');
            Comp = mod.default;
        } catch (e) {
            error = e;
        } finally {
            loading = false;
        }
    }
</script>

Every call site re-implements:

  • A null-check to avoid double-loading.
  • A loading flag for the spinner.
  • An error slot for failures.
  • A way to share the in-flight promise between callers (e.g. prefetch on hover + render on click).
  • A way to pass the not-yet-loaded handle into a child component without losing reactivity.

svelte-lazy collapses all of that into one reactive handle (LazyComponent) plus one optional wrapper (<Lazy>):

<script>
    import { lazy, Lazy } from '@coroama/svelte-lazy';
    const heavy = lazy(() => import('./HeavyPanel.svelte'));
</script>

<Lazy lazy={heavy}>
    {#snippet children(HeavyPanel)}
        <HeavyPanel data={rows} />
    {/snippet}
</Lazy>

The lazy(...) factory returns a FastBox-backed cell that:

  • Dedupes concurrent calls automatically (one chunk, one network request).
  • Survives being passed across function, class, and component boundaries reactively, because it inherits the cross-boundary semantics of svelte-box.
  • Exposes a tiny API: prefetch(), ensure(), reset(), plus the underlying .value cell.

Quick start

<script lang="ts">
    import { lazy, Lazy } from '@coroama/svelte-lazy';

    const profilePanel = lazy(() => import('./ProfilePanel.svelte'));
</script>

<Lazy lazy={profilePanel}>
    {#snippet children(ProfilePanel)}
        <ProfilePanel />
    {/snippet}
    {#snippet pending()}
        <p>Loading…</p>
    {/snippet}
</Lazy>

<Lazy> fires the loader on mount and swaps the pending snippet for the resolved component once the chunk arrives. To kick the load earlier (hover, focus, route preload), call profilePanel.prefetch() from the relevant handler. See Prefetch on hover.

lazy() vs <Lazy>

The library ships one runtime primitive and one optional wrapper. Pick based on whether you need to control the template structure.

Feature lazy() (inline {#await}) <Lazy> wrapper
Dedupe concurrent loads yes yes
Reactive .value cell crosses boundaries yes yes
Manual prefetch() / ensure() / reset() yes yes
bind: on the lazy-loaded component yes yes (via children snippet)
Custom pending / error UI yes yes (named snippets)
Automatic prefetch on mount no, call yourself yes
Auto-retry after external reset() no, call prefetch() again yes
One-liner call site no, four-line template yes

Reach for <Lazy> when you want a one-liner with pending and error slots. Use the cell directly when you need to control the surrounding template, for example composing multiple lazy cells in one branch.

Usage

Inline {#await} pattern

The cell is a FastBox<Promise<{ default: Component }> | null>. Once the loader fires, the promise lives on .value and {#await} does the rest:

<script lang="ts">
    import { lazy } from '@coroama/svelte-lazy';

    let source = $state('# Hello');
    let dirty = $state(false);

    const editor = lazy(() => import('./MarkdownEditor.svelte'));
</script>

<button onclick={() => editor.prefetch()}>edit</button>

{#if editor.value}
    {#await editor.value then { default: MarkdownEditor }}
        <MarkdownEditor bind:value={source} bind:dirty />
    {/await}
{/if}

The outer {#if editor.value} guards the not-yet-fired case. After prefetch(), {#await} handles pending, resolved, and rejected branches.

The <Lazy> wrapper

The same example through <Lazy>:

<script lang="ts">
    import { lazy, Lazy } from '@coroama/svelte-lazy';

    let source = $state('# Hello');
    let dirty = $state(false);

    const editor = lazy(() => import('./MarkdownEditor.svelte'));
</script>

<Lazy lazy={editor}>
    {#snippet children(MarkdownEditor)}
        <MarkdownEditor bind:value={source} bind:dirty />
    {/snippet}
    {#snippet pending()}<p>Loading editor...</p>{/snippet}
    {#snippet error(err)}<p>Failed: {(err as Error).message}</p>{/snippet}
</Lazy>

<Lazy> calls prefetch() on mount, so the loader fires as soon as the wrapper appears in the tree. The children snippet receives the resolved component constructor, so bind:, props, slots, and events all work normally.

The error snippet is typed Snippet<[unknown]>. A dynamic import() rejection is not statically guaranteed to be Error, so callers narrow with (err as Error).message or err instanceof Error ? err.message : String(err). If the same <Lazy> shows up in more than one place, lift the narrowing into a tiny helper:

function narrowError(err: unknown): string {
    return err instanceof Error ? err.message : String(err);
}

and call {narrowError(err)} inside {#snippet error(err)}.

Prefetch on hover

prefetch() is the cheap warmup. Calling it kicks the loader and returns the same shared promise on subsequent calls. Wire it to whatever signal predicts the next interaction:

<button
    onpointerenter={() => settings.prefetch()}
    onfocus={() => settings.prefetch()}
    onclick={() => (showSettings = true)}
>
    settings
</button>

{#if showSettings}
    <Lazy lazy={settings}>
        {#snippet children(Settings)}<Settings />{/snippet}
    </Lazy>
{/if}

By the time the user clicks, the chunk is already in cache. The wrapper has nothing to fetch.

Retry after a failed load

If the loader rejects (offline, deploy mid-load, malformed chunk), the rejected promise is cached. reset() clears the cell so the next prefetch() / ensure() re-fires the loader. reset() returns the same LazyComponent, so the retry call site is one chain:

<Lazy lazy={panel}>
    {#snippet children(Panel)}<Panel />{/snippet}
    {#snippet error(err)}
        <p>Failed to load: {(err as Error).message}</p>
        <button onclick={() => panel.reset().prefetch()}>retry</button>
    {/snippet}
</Lazy>

Inside <Lazy>, calling panel.reset() alone is enough. The wrapper's $effect watches panel.value and auto-fires prefetch() when the cell goes back to null. The explicit .prefetch() in the chain makes the retry obvious at the call site.

During reset, the wrapper briefly sees lazy.value === null between the reset and the new prefetch assignment. Both the {#if lazy.value} else branch and the {#await} pending branch render the pending snippet, so the user sees a continuous loading state with no flash to empty. If your pending snippet has internal state (a spinner with an animation phase, an aria-live region), it remounts on each reset.

reset() does not cancel an in-flight load (dynamic import() has no abort signal). The stale promise is detached from value. The original request still completes in the background.

Passing a LazyComponent across boundaries

The whole point of building on svelte-box is that the cell is a real reactive handle, not a snapshot. Pass it to a class, a route store, a parent slot, or a sibling component:

// stores/panels.ts
import { lazy, type LazyComponent } from '@coroama/svelte-lazy';

export class PanelStore {
    settings = lazy(() => import('../panels/Settings.svelte'));
    profile = lazy(() => import('../panels/Profile.svelte'));

    /** Warm up everything the user is likely to open next. */
    warmAll() {
        this.settings.prefetch();
        this.profile.prefetch();
    }
}

export const panels = new PanelStore();
<!-- App.svelte -->
<script>
    import { panels } from './stores/panels';
    import { Lazy } from '@coroama/svelte-lazy';
</script>

<button onpointerenter={() => panels.warmAll()}>open menu</button>

<Lazy lazy={panels.settings}>
    {#snippet children(Settings)}<Settings />{/snippet}
</Lazy>

Same handle, multiple call sites, one network request.

API reference

lazy(loader)

Factory. Returns a fresh LazyComponent<T> wrapping loader. T infers from the loader's default export.

declare function lazy<T>(loader: LazyLoaderFn<T>): LazyComponent<T>;

LazyLoaderFn<T> and ComponentCell<T> are defined under Types below.

class LazyComponent<T>

A FastBox<Promise<ComponentCell<T>> | null> with three methods on top.

Member Description
new LazyComponent(loader) Direct construction. Prefer lazy(loader).
.value Inherited from FastBox. The cached promise, or null if the loader hasn't fired. Reactive.
.prefetch() Fire the loader if needed, return the import promise. Idempotent: subsequent calls reuse the same promise.
.ensure() Awaits the loader and the resulting tick(). Use before measuring the DOM or focusing inside the lazy tree.
.reset() Clear the cached promise so the next prefetch() / ensure() re-fires the loader. Returns this for chaining. Used for retry-after-failure.

Inherited from FastBox / BaseBox: get(), set(v), del(), snapshot(), eager(), toJSON(), and all 14 type guards (isNull, etc.). See the svelte-box README for details.

<Lazy>

Wrapper component. Calls prefetch() on mount and again any time lazy.value is reset to null, so an external panel.reset() makes the wrapper re-fire the loader. Renders pending / children / error snippets through a {#await} block.

The prop shape is exported as LazyProps<T> for callers who want to write their own wrappers on top:

import type { LazyProps } from '@coroama/svelte-lazy';

// LazyProps is defined as:
// type LazyProps<T extends AnyComponent = AnyComponent> = {
//     lazy: LazyComponent<T>;
//     children: Snippet<[T]>;
//     pending?: Snippet;
//     error?: Snippet<[unknown]>;
// };
  • children is required and receives the resolved component constructor. Use a normal <Component> tag inside the snippet to pass props, bind:, slots, and events.
  • pending renders during the load. Defaults to nothing.
  • error renders if the loader rejects. Defaults to nothing.

Types

type AnyComponent = Component<Record<string, any>, Record<string, any>>;
type ComponentCell<T> = { default: T };
type LazyLoaderFn<T> = () => Promise<ComponentCell<T>>;

Patterns and pitfalls

One lazy() per chunk

lazy(() => import('./Foo.svelte')) is the unit of code-splitting. Declare it once at module scope (or on a long-lived store) and share the handle. Declaring it inside a component's <script> is fine, but a new instance on every mount loses the dedupe benefit between mounts.

bind: works through the children snippet

The <Lazy> children snippet hands you the component constructor, not a pre-bound element. Bind whatever you want:

<script>
    let dirty = $state(false);
    let values = $state({});
</script>

<Lazy lazy={form}>
    {#snippet children(Form)}
        <Form bind:dirty bind:values />
    {/snippet}
</Lazy>

This is why <Lazy> takes a snippet instead of forwarding props directly: forwarding can't carry bind: through.

Don't await ensure() inside a $derived

$derived is synchronous. Use $effect for the await, and always handle rejection because ensure() propagates whatever the loader threw:

<script>
    $effect(() => {
        if (showPanel.value) {
            panel
                .ensure()
                .then(() => focusInput())
                .catch((err) => console.error('lazy load failed', err));
        }
    });
</script>

reset() does not cancel an in-flight load

Dynamic import() has no abort signal. reset() detaches the promise from .value so the next prefetch() re-fires, but the original network request still resolves in the background. The bundler caches the module, so the second prefetch() after a successful first load is free.

loaded is implicit

There's no lazy.loaded boolean. The {#await ... :then} branch is the source of truth for "loaded right now". If you need a sync flag outside the template (e.g. for analytics), set one yourself in the :then branch.

TypeScript

Inference works through Vite's import types:

const panel = lazy(() => import('./Settings.svelte'));
// panel: LazyComponent<typeof import('./Settings.svelte').default>

To annotate a parameter that accepts any lazy component, use the bare class:

import type { LazyComponent } from '@coroama/svelte-lazy';

function warm(panel: LazyComponent) {
    panel.prefetch();
}

The default type parameter is AnyComponent, so LazyComponent without an argument is the right type for "any code-split component".

Performance

The library is not a runtime speedup. The performance benefit is the chunk of code you no longer ship in the initial bundle, and that benefit comes from dynamic import(), not from the library. The library is the ergonomics layer that makes deferring those chunks cheap enough to actually do.

The right comparison

The honest comparison is lazy loading vs no lazy loading, not "this library vs a hand-rolled loader."

  • No lazy loading: a 200KB markdown editor sits in the initial bundle for every visitor, including the ones who never open it. Time-To-Interactive pays for it.
  • Lazy loading via this library: the editor is its own chunk. Initial load drops by 200KB. Users who never open the editor never download it.

Both versions of "lazy loading" (hand-rolled and via the library) ship the same chunks. The bench numbers below measure the library's per-call overhead inside the lazy-loading path, not whether your app feels faster.

Where the library actually helps

What the library gives you over a hand-rolled equivalent is correctness and ergonomics:

  • One line per lazy boundary instead of 15. Teams that find hand-rolling too tedious skip lazy loading entirely and ship larger bundles.
  • Repeated prefetch() calls on the same panel dedupe. Three hover events on one item (pointerenter, focus, click) fire one network request. A naive hand-rolled cache that does not self-check fires three.
  • Sync-throw and reject paths normalize to a thenable, so retries work even when the loader has a typo or a deploy mid-session leaves a chunk 404ing.
  • The cell is a FastBox. Passing it through plain functions, class fields, and component props keeps reactivity intact without re-wrapping.

If none of these concerns apply to your codebase, hand-roll. The runtime cost of hand-rolling is lower by a small constant; the maintenance cost is higher.

Cost model

The hot path is prefetch(). Each call does:

  1. One reactive read of this.value for the falsy check.
  2. If the cell is empty, one reactive write that assigns the import promise.
  3. Returns the cell.

After the first call the body is just a read and a return. The if (!this.value) branch is the only meaningful work, and that branch is taken once per LazyComponent instance for the lifetime of the page (or until a reset()).

ensure() adds one await this.prefetch() plus one await tick(). The tick is the dominant cost when the loader is already cached, because awaiting it queues a microtask and a render flush even when nothing changed.

reset() is a single reactive write of null and a return of this. No measurable cost.

The wrapper component runs one $effect that reads lazy.value and conditionally calls prefetch(). The effect re-fires when either the prop reference or lazy.value changes. Per render, this is one reactive read and at most one method call.

Memory per LazyComponent is one FastBox instance plus one private loader field. The FastBox itself is a small object with one $state cell. There is no proxy layer (this uses FastBox, not Box) so the per-instance overhead is the same as a class with a single $state field, which is the cheapest reactive container available.

What the bench measures

benchmarking/lazy.svelte.bench.ts runs a three-way comparison per scenario where it makes sense. The baseline is what a developer would write without the library (usually a plain object holding the cache plus an if(!cache) check). The other columns are lazy(loader) and direct new LazyComponent(loader).

The scenarios are app-shaped, not micro-shaped.

  • App boot, 20 panels declared. A medium app holds roughly 15 to 30 lazy boundaries. Measure the cost of constructing 20 instances. Fires once per page load.
  • Menu hover, 8 cold prefetches. User mouses across a menu, each item warms its panel. Fires once per item per session.
  • Menu re-hover, 8 warm prefetches. User mouses back, every call is a no-op cache hit. The path that fires most often.
  • First open, ensure() with tick. User clicks, the panel ensures itself before code that touches the DOM. Includes the await tick() step.
  • Retry, reset() + prefetch(). Deploy mid-session leaves a panel rejected. Retry button clears and refires.
  • Store pattern, 10 fields, warmAll(). A class with ten lazy fields plus a method that prefetches them all. Common for "preload this section" buttons.
  • Cross-boundary call, warm(panel). A panel is passed into a function from another component. One call per interaction.
  • Per-render .value read. One read per render, not a tight loop. This is the realistic cost of a {#if lazy.value} guard.

Benchmark results

Numbers below come from a single Chromium run on Linux x86_64 through @vitest/browser-playwright. Treat them as ballpark figures. They drift between machines, browsers, and Svelte versions.

Reading the column: Nx slower means the library completed 1/N of the baseline's operations in the same wall-clock time.

Scenario Baseline LazyComponent
App boot, 20 panels (lazy() factory) 20 hand-rolled cache objects 5.17x slower
App boot, 20 panels (new LazyComponent) 20 hand-rolled cache objects 5.56x slower
Menu hover, 8 cold prefetch() 8 manual if(!cache) cache = load() 3.71x slower
Menu re-hover, 8 warm prefetch() (no-op) 8 manual if(!cache) no-op checks 4.11x slower
First open, ensure() with tick await load(); await Promise.resolve 1.50x slower
Retry, reset() + prefetch() round trip cache = null; if(!cache) load() 2.38x slower
Store pattern, 10 fields, warmAll() class with 10 hand-rolled caches 3.16x slower
Cross-boundary warm(panel) call function mutates a cache object 2.21x slower
Per-render .value read on a warm cell plain object field read 2.19x slower

The cluster around 2x to 5x is the cost of the FastBox reactive cell against a plain { value: null } object. Every read goes through a $state getter, every write through a $state setter, and the class adds the usual function call overhead on top. The first-open row sits at 1.50x because ensure() is dominated by await tick(), which is the same on both sides, so the per-call gap is diluted.

There is no server-side bench in CI. Dynamic import() dominates the cost on the server; the library's per-call overhead does not. To get a Node-side number, copy the bench file and run it under environment: 'node' locally.

Reproducing locally

bun run bench          # human-readable
bun run bench:json     # machine-readable, writes bench-results.json

The bench file is benchmarking/lazy.svelte.bench.ts. Edit it to add a scenario that matches your app's call pattern.

What this means for a real app

The bench reports throughput in operations per second. Every single-call scenario in the table runs at hundreds of thousands of operations per second on the slower side. A 5x slowdown on something that takes around 1.4 microseconds lands at roughly 7 microseconds. None of that is visible to a user.

A 60Hz frame is 16 ms. The library only does meaningful work during three events:

  • The first prefetch() on a given LazyComponent. Cost is dominated by the network request, not by the library.
  • The first render after the loader resolves. Cost is the inner component's render work, not the wrapper.
  • A reset() + prefetch() retry. Two reactive writes and a new network request.

Compare that to the bundle-size win. Deferring a 200KB component out of the initial chunk shaves roughly 100 to 500 ms off Time-To-Interactive on a mid-tier phone over 4G. The library's per-call overhead is invisible against it.

If you have thousands of LazyComponent instances on one page and read their .value in tight loops, hoist the cell value into a local before the loop. Same advice as any FastBox use.

SSR and SvelteKit

Dynamic import() runs on whatever runtime you're in. On the server, the loader fetches the module from the local bundle. On the client, the bundler emits a code-split chunk. The library does nothing different between the two. The same lazy(...) call works in both contexts.

Two practical notes:

  • Don't put a LazyComponent in load() return data. It's a class instance with a FastBox inside, not serializable. Construct lazy handles on the client or in a shared module, not in route data.
  • Mount <Lazy> inside a route's component tree, not inside the layout <script> top level. Top-level prefetch() on the server adds a server-side import without a client benefit.

Bundle size

The library ships as ESM with "sideEffects": ["**/*.css"] so all exports are tree-shakeable. The whole runtime is one class plus one factory plus one wrapper component, on top of @coroama/svelte-box's FastBox.

Tree-shaking is independent across the two entry points. <Lazy> only references LazyComponent as a type, so importing the wrapper alone does not pull in the runtime class.

Compatibility

  • Svelte: declared peer of ^5.0.0. The library uses the runes API ($state, $props, snippets) through @coroama/svelte-box.
  • TypeScript: works under strict mode. Loader inference unifies with Vite's () => import('./X.svelte') automatically.
  • Node: 20.6 or newer for the build toolchain. The compiled package runs anywhere a modern JS runtime does.
  • Bundlers: any bundler that emits dynamic import() as a separate chunk. Verified with Vite (Rollup under the hood). Other bundlers that implement the same standards-compliant code-splitting model (Webpack 5+, Rspack, esbuild via plugin) should work but are not part of CI.

Debugging

A LazyComponent prints as a class instance with a FastBox inside, which is rarely what you want in the console. Use snapshot() for a plain non-reactive view:

console.log(panel.snapshot()); // null, or the cached Promise
console.log(JSON.stringify(panel)); // also fine, via the inherited toJSON hook

If you want a quick sanity check of which chunk a lazy(...) call produced, open the DevTools Network tab and watch the request fire when you call prefetch(). The bundler names dynamic chunks after the file path you imported, so import('./MarkdownEditor.svelte') becomes a MarkdownEditor-*.js request.

For unit tests, panel.value is null or a Promise, both safe to compare with expect(...).toEqual(...). To assert resolved content, await the cell:

import type { Component } from 'svelte';
import MarkdownEditor from './MarkdownEditor.svelte';

await panel.ensure();
const { default: Loaded } = await panel.value!;
expect(Loaded).toBe(MarkdownEditor);
// `Loaded` is typed as `Component<...>`, the same shape MarkdownEditor exports.

Caveats

  • get and set are inherited from FastBox. They reach the cell, not the inner component. panel.get() returns null or the cached Promise. To replace the cached promise (rare), use panel.value = newPromise or panel.set(newPromise).
  • Plain import() is not retryable on its own. If the bundler's network request fails, the rejected promise sticks until you call reset(). The library does not retry automatically because there is no general right answer for backoff, and most retry policies are app-specific.
  • reset() does not cancel an in-flight load. Dynamic import() has no abort signal. The original request still completes. Plan around this if you care about wasted bandwidth.
  • <Lazy> requires a children snippet. A wrapper with no children renders nothing once the loader resolves. The required snippet receives the resolved component constructor, so callers always control the mount.
  • structuredClone(panel) throws. The cell is a class instance wrapped around reactive state, not a plain object. Clone panel.value or panel.snapshot() instead.
  • The error snippet receives unknown. That matches what try/catch provides under strict TypeScript. Narrow with err instanceof Error or cast with as Error when you need .message. Svelte's inline {:catch err} binding types err more loosely (effectively any), so the demo can write err.message without a cast inside {:catch}. The recommended pattern is to apply the same narrowing in both places for portability across strict-TS targets.

Status and testing

This is a young, single-maintainer project.

The repository ships:

  • A Vitest browser-mode suite (@vitest/browser-playwright) across three files:
  • A benchmark file at benchmarking/lazy.svelte.bench.ts comparing the library against a hand-rolled object-literal baseline. Run bun run bench to reproduce.
  • A live demo route at src/routes/+page.svelte showing the wrapper, the inline pattern with bind:, and the retry path with simulated failure.
  • GitHub Actions workflows under .github/workflows/ covering CI (lint, check, test, build on Linux/macOS/Windows), post-merge benchmarks, GitHub Pages deploys of the demo route, npm publishing through Trusted Publisher OIDC with provenance and a CycloneDX SBOM, plus CodeQL static analysis.

Maintenance and support

Single-maintainer project.

  • No SLA. Bugs, feature requests, and questions are answered when the maintainer has time. For something time-critical, fork or vendor the code (it is small).
  • Severity heuristic. Reproducible correctness bugs and security issues take priority over feature requests and DX polish. Open an issue with a minimal reproduction for the fastest path to a fix.
  • Contributions welcome. PRs that include a test and keep the bench numbers within noise are the easiest to land. Big architectural changes should start as an issue first so the design conversation does not stall on a long branch. See CONTRIBUTING.md for the practical workflow.
  • Security disclosures go to the channel documented in SECURITY.md. Do not open public issues for vulnerability reports.
  • Single-maintainer dependency chain. The runtime depends on @coroama/svelte-box, authored by the same maintainer. Bus factor across both libraries is one person. Both packages publish through GitHub Trusted Publisher OIDC with provenance, so a compromise of the npm registry alone cannot push unsigned releases. If you have a strict policy against single-maintainer dependency chains, weigh this before adopting.
  • Svelte 6 plan. The library uses only the public Svelte 5 rune API through @coroama/svelte-box plus the standard dynamic import() syntax. When Svelte 6 lands, the intent is to support it on the same major version if the upgrade does not force a public-surface break. Otherwise cut a new major.

AI assistance disclosure

Parts of this project were written or refined with help from Anthropic's Claude. That includes documentation drafts, code review passes, test scaffolding, and configuration boilerplate. Every change was read, edited, and accepted by a human maintainer before landing on master. Treat AI involvement the same way you would treat any other contributor: the maintainer is accountable for the result, not the tool that produced the first draft.

License

MIT. See LICENSE.

Repository

Source, issues, and changelog: https://github.com/IsaiahCoroama/svelte-lazy. The changelog follows the Keep a Changelog format. Anything that changes the public surface gets an entry under that release.

Top categories

Loading Svelte Themes