{"_id":"@coroama/svelte-lazy","_rev":"4-15cb706d4072a0fad0a43c7165160eb1","name":"@coroama/svelte-lazy","dist-tags":{"next":"0.1.0-rc.0","latest":"1.0.1"},"versions":{"0.1.0-rc.0":{"name":"@coroama/svelte-lazy","version":"0.1.0-rc.0","keywords":["svelte","svelte5","sveltekit","runes","lazy","lazy-load","lazy-loading","lazy-component","code-splitting","code-split","dynamic-import","async-component","component-loader","prefetch","chunk","defer","suspense","await","reactive","typescript"],"author":{"name":"Isaiah Coroama","email":"coroamaisaiah@gmail.com"},"license":"MIT","_id":"@coroama/svelte-lazy@0.1.0-rc.0","maintainers":[{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"}],"homepage":"https://github.com/IsaiahCoroama/svelte-lazy#readme","bugs":{"url":"https://github.com/IsaiahCoroama/svelte-lazy/issues"},"dist":{"shasum":"2a52bbfe2bb10b73306cf7de34c04caf33b06adb","tarball":"https://registry.npmjs.org/@coroama/svelte-lazy/-/svelte-lazy-0.1.0-rc.0.tgz","fileCount":10,"integrity":"sha512-0CZirt9f3+DptluYlxyU/2O8E7aAoIJMqJnMSLgLGGVe7quUSmSpDU0NzGVc9lGWAA5SidWgEfdGi04ChnUQrA==","signatures":[{"sig":"MEYCIQCD7BXtV7oEb1Nf9eNhG6+YQDc5w00+0IVTqVxr0dUN9wIhAN/XJr+wFJ6I2rCbAgy1i4A8igea2iWG+zmZJFzF9XBn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57188},"type":"module","types":"./dist/index.d.ts","svelte":"./dist/index.js","engines":{"node":">=20.6"},"exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"}},"gitHead":"12da030c6daca2866dad7dff6b76be2a67886104","scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","bench":"vitest bench --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","build:app":"vite build","test:unit":"vitest","bench:json":"vitest bench --run --reporter=verbose --outputJson=bench-results.json","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"},"_npmUser":{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"},"overrides":{"cookie":"^0.7.1"},"repository":{"url":"git+https://github.com/IsaiahCoroama/svelte-lazy.git","type":"git"},"_npmVersion":"11.8.0","description":"Lazy-loaded component slots for Svelte 5. Wraps dynamic import() in a reactive cell with prefetch, retry, and a {#await}-friendly wrapper component.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.8.0","dependencies":{"@coroama/svelte-box":"^0.2.1"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.12","eslint":"^10.3.0","svelte":"^5.55.5","vitest":"^4.1.6","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^25.7.0","svelte-check":"^4.4.8","@sveltejs/kit":"^2.59.1","@eslint/compat":"^2.1.0","@sveltejs/package":"^2.5.7","typescript-eslint":"^8.59.3","eslint-plugin-svelte":"^3.17.1","vitest-browser-svelte":"^2.1.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^3.5.2","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.6","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/svelte-lazy_0.1.0-rc.0_1778636955725_0.4577485131006642","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@coroama/svelte-lazy","version":"0.1.0","keywords":["svelte","svelte5","sveltekit","runes","lazy","lazy-load","lazy-loading","lazy-component","code-splitting","code-split","dynamic-import","async-component","component-loader","prefetch","chunk","defer","suspense","await","reactive","typescript"],"author":{"name":"Isaiah Coroama","email":"coroamaisaiah@gmail.com"},"license":"MIT","_id":"@coroama/svelte-lazy@0.1.0","maintainers":[{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"}],"homepage":"https://github.com/IsaiahCoroama/svelte-lazy#readme","bugs":{"url":"https://github.com/IsaiahCoroama/svelte-lazy/issues"},"dist":{"shasum":"87f265a5797910f3aec63d8bc66c13982bd79311","tarball":"https://registry.npmjs.org/@coroama/svelte-lazy/-/svelte-lazy-0.1.0.tgz","fileCount":10,"integrity":"sha512-mTagJovNjmM0XNGrKmhD5Hwahv3E3l291Uj7rbTp+0yOzV6skORR6r8xgLD85GYN1CJwhmxHkgywfjArcmaiKw==","signatures":[{"sig":"MEUCIQCfdGUXN//RGpHOTDn2bqtRa2ehV+qASe/2Y1yf/1aA4AIgI89Y54rHPSxudiLVA/DqXVqCwW7AAOqwYz/OcRZmEx4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coroama%2fsvelte-lazy@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":57183},"type":"module","types":"./dist/index.d.ts","svelte":"./dist/index.js","engines":{"node":">=20.6"},"exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js"}},"gitHead":"10bfab7b9d3dc1f89923fd399018bc54a801f4bb","scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","bench":"vitest bench --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","build:app":"vite build","test:unit":"vitest","bench:json":"vitest bench --run --reporter=verbose --outputJson=bench-results.json","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:345070da-021a-4423-951d-8cc929109ec0"}},"overrides":{"cookie":"^0.7.1"},"repository":{"url":"git+https://github.com/IsaiahCoroama/svelte-lazy.git","type":"git"},"_npmVersion":"11.11.0","description":"Lazy-loaded component slots for Svelte 5. Wraps dynamic import() in a reactive cell with prefetch, retry, and a {#await}-friendly wrapper component.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.14.1","dependencies":{"@coroama/svelte-box":"^0.2.1"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.12","eslint":"^10.3.0","svelte":"^5.55.5","vitest":"^4.1.6","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^25.7.0","svelte-check":"^4.4.8","@sveltejs/kit":"^2.59.1","@eslint/compat":"^2.1.0","@sveltejs/package":"^2.5.7","typescript-eslint":"^8.59.3","eslint-plugin-svelte":"^3.17.1","vitest-browser-svelte":"^2.1.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^3.5.2","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.6","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/svelte-lazy_0.1.0_1778637775607_0.7463924703378653","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@coroama/svelte-lazy","version":"1.0.0","keywords":["svelte","svelte5","sveltekit","runes","lazy","lazy-load","lazy-loading","lazy-component","code-splitting","code-split","dynamic-import","async-component","component-loader","prefetch","chunk","defer","suspense","await","reactive","typescript"],"author":{"name":"Isaiah Coroama","email":"coroamaisaiah@gmail.com"},"license":"MIT","_id":"@coroama/svelte-lazy@1.0.0","maintainers":[{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"}],"homepage":"https://github.com/IsaiahCoroama/svelte-lazy#readme","bugs":{"url":"https://github.com/IsaiahCoroama/svelte-lazy/issues"},"dist":{"shasum":"b8657f2f3470e9766bb5322ccde40eb4a31375d3","tarball":"https://registry.npmjs.org/@coroama/svelte-lazy/-/svelte-lazy-1.0.0.tgz","fileCount":10,"integrity":"sha512-SBiKXgjl00O4DjpZ+1N1JZmqQ0EIrSTCDKuB9K9qVktqbMTvnCSba6EraxEN5LPcMlEszi8I1Yw/32eEjbWwaA==","signatures":[{"sig":"MEUCIDSnOoKGikv+EXcbzm3jDAPZ4zTtwDpZo9tfqYsTvJyZAiEA2PrK5XJES9SxbajiIlHSFLDgk8UOT3gsqByUHoDGZ74=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coroama%2fsvelte-lazy@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":64878},"type":"module","_from":"file:coroama-svelte-lazy-1.0.0.tgz","types":"./dist/index.d.ts","svelte":"./dist/index.js","engines":{"node":">=20.6"},"exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js","default":"./dist/index.js"}},"funding":{"url":"https://github.com/sponsors/IsaiahCoroama","type":"github"},"scripts":{"dev":"vite dev","lint":"prettier --check . && eslint .","test":"npm run test:unit -- --run","bench":"vitest bench --run","build":"vite build && npm run prepack","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","format":"prettier --write .","prepack":"svelte-kit sync && svelte-package && publint","prepare":"svelte-kit sync || echo ''","preview":"vite preview","build:app":"vite build","test:unit":"vitest","bench:json":"vitest bench --run --reporter=verbose --outputJson=bench-results.json","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch","test:coverage":"vitest --run --coverage"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:345070da-021a-4423-951d-8cc929109ec0"}},"_resolved":"/home/runner/work/svelte-lazy/svelte-lazy/coroama-svelte-lazy-1.0.0.tgz","overrides":{"cookie":"^0.7.1"},"_integrity":"sha512-SBiKXgjl00O4DjpZ+1N1JZmqQ0EIrSTCDKuB9K9qVktqbMTvnCSba6EraxEN5LPcMlEszi8I1Yw/32eEjbWwaA==","repository":{"url":"git+https://github.com/IsaiahCoroama/svelte-lazy.git","type":"git"},"_npmVersion":"11.12.1","description":"Lazy-loaded component slots for Svelte 5. Wraps dynamic import() in a reactive cell with prefetch, retry, and a {#await}-friendly wrapper component.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.15.0","dependencies":{"@coroama/svelte-box":"~1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.13","eslint":"^10.4.0","svelte":"^5.55.7","vitest":"^4.1.6","globals":"^17.6.0","publint":"^0.3.21","prettier":"^3.8.3","@eslint/js":"^10.0.1","playwright":"^1.60.0","typescript":"^6.0.3","@types/node":"^25.9.0","svelte-check":"^4.4.8","@sveltejs/kit":"^2.60.1","@eslint/compat":"^2.1.0","@sveltejs/package":"^2.5.7","typescript-eslint":"^8.59.3","@vitest/coverage-v8":"^4.1.6","eslint-plugin-svelte":"^3.17.1","vitest-browser-svelte":"^2.1.1","eslint-config-prettier":"^10.1.8","prettier-plugin-svelte":"^3.5.2","@sveltejs/adapter-static":"^3.0.10","@vitest/browser-playwright":"^4.1.6","@sveltejs/vite-plugin-svelte":"^7.1.2"},"peerDependencies":{"svelte":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/svelte-lazy_1.0.0_1779122337517_0.18726668358141807","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@coroama/svelte-lazy","version":"1.0.1","description":"Lazy-loaded component slots for Svelte 5. Wraps dynamic import() in a reactive cell with prefetch, retry, and a {#await}-friendly wrapper component.","license":"MIT","author":{"name":"Isaiah Coroama","email":"coroamaisaiah@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/IsaiahCoroama/svelte-lazy.git"},"bugs":{"url":"https://github.com/IsaiahCoroama/svelte-lazy/issues"},"homepage":"https://github.com/IsaiahCoroama/svelte-lazy#readme","scripts":{"dev":"vite dev","build":"vite build && npm run prepack","build:app":"vite build","preview":"vite preview","prepare":"svelte-kit sync || echo ''","prepack":"svelte-kit sync && svelte-package && publint","check":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json","check:watch":"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch","lint":"prettier --check . && eslint .","format":"prettier --write .","test:unit":"vitest","test":"npm run test:unit -- --run","test:coverage":"vitest --run --coverage","bench":"vitest bench --run","bench:json":"vitest bench --run --reporter=verbose --outputJson=bench-results.json"},"sideEffects":["**/*.css"],"svelte":"./dist/index.js","types":"./dist/index.d.ts","type":"module","engines":{"node":">=20.6"},"exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js","default":"./dist/index.js"}},"peerDependencies":{"svelte":"^5.0.0"},"devDependencies":{"@eslint/compat":"^2.1.0","@eslint/js":"^10.0.1","@sveltejs/adapter-static":"^3.0.10","@sveltejs/kit":"^2.60.1","@sveltejs/package":"^2.5.7","@sveltejs/vite-plugin-svelte":"^7.1.2","@types/node":"^25.9.0","@vitest/browser-playwright":"^4.1.6","@vitest/coverage-v8":"^4.1.6","eslint":"^10.4.0","eslint-config-prettier":"^10.1.8","eslint-plugin-svelte":"^3.17.1","globals":"^17.6.0","playwright":"^1.60.0","prettier":"^3.8.3","prettier-plugin-svelte":"^3.5.2","publint":"^0.3.21","svelte":"^5.55.7","svelte-check":"^4.4.8","typescript":"^6.0.3","typescript-eslint":"^8.59.3","vite":"^8.0.13","vitest":"^4.1.6","vitest-browser-svelte":"^2.1.1"},"publishConfig":{"access":"public"},"funding":{"type":"github","url":"https://github.com/sponsors/IsaiahCoroama"},"keywords":["svelte","svelte5","sveltekit","runes","lazy","lazy-load","lazy-loading","lazy-component","code-splitting","code-split","dynamic-import","async-component","component-loader","prefetch","chunk","defer","suspense","await","reactive","typescript"],"dependencies":{"@coroama/svelte-box":"~1.0.0"},"overrides":{"cookie":"^0.7.1"},"_id":"@coroama/svelte-lazy@1.0.1","_integrity":"sha512-RRCvVJiD82JtqiETT0T3O0rU0fgNwK38LTVKOWv7yOV6sdQy0Ijko22vjyzmICpD8CCoNH8KkiDGXvodVr6ZgA==","_resolved":"/home/runner/work/svelte-lazy/svelte-lazy/coroama-svelte-lazy-1.0.1.tgz","_from":"file:coroama-svelte-lazy-1.0.1.tgz","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-RRCvVJiD82JtqiETT0T3O0rU0fgNwK38LTVKOWv7yOV6sdQy0Ijko22vjyzmICpD8CCoNH8KkiDGXvodVr6ZgA==","shasum":"2575df8cd6c02112e4e4433b25e6d396e4888c78","tarball":"https://registry.npmjs.org/@coroama/svelte-lazy/-/svelte-lazy-1.0.1.tgz","fileCount":10,"unpackedSize":65756,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coroama%2fsvelte-lazy@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBVCGAy9v2G+He//VU5uuGZ1tvAlziUPMjJD540reXlNAiEA2UPHNo06r1+rwISLBJa18uKzaAnHYOBsadtTo3OCm3Q="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:345070da-021a-4423-951d-8cc929109ec0"}},"directories":{},"maintainers":[{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/svelte-lazy_1.0.1_1779123400750_0.4438830565482439"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-13T01:49:15.646Z","modified":"2026-05-18T16:56:41.221Z","0.1.0-rc.0":"2026-05-13T01:49:15.861Z","0.1.0":"2026-05-13T02:02:55.748Z","1.0.0":"2026-05-18T16:38:57.661Z","1.0.1":"2026-05-18T16:56:40.891Z"},"bugs":{"url":"https://github.com/IsaiahCoroama/svelte-lazy/issues"},"author":{"name":"Isaiah Coroama","email":"coroamaisaiah@gmail.com"},"license":"MIT","homepage":"https://github.com/IsaiahCoroama/svelte-lazy#readme","keywords":["svelte","svelte5","sveltekit","runes","lazy","lazy-load","lazy-loading","lazy-component","code-splitting","code-split","dynamic-import","async-component","component-loader","prefetch","chunk","defer","suspense","await","reactive","typescript"],"repository":{"type":"git","url":"git+https://github.com/IsaiahCoroama/svelte-lazy.git"},"description":"Lazy-loaded component slots for Svelte 5. Wraps dynamic import() in a reactive cell with prefetch, retry, and a {#await}-friendly wrapper component.","maintainers":[{"name":"isaiahcoroama","email":"coroamaisaiah@gmail.com"}],"readme":"# svelte-lazy\n\n[![npm version](https://img.shields.io/npm/v/@coroama/svelte-lazy.svg?logo=npm&label=npm)](https://www.npmjs.com/package/@coroama/svelte-lazy)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@coroama/svelte-lazy?label=min%2Bgzip)](https://bundlephobia.com/package/@coroama/svelte-lazy)\n[![CI](https://github.com/IsaiahCoroama/svelte-lazy/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/IsaiahCoroama/svelte-lazy/actions/workflows/ci.yml)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/IsaiahCoroama/svelte-lazy/badge)](https://scorecard.dev/viewer/?uri=github.com/IsaiahCoroama/svelte-lazy)\n[![license](https://img.shields.io/npm/l/@coroama/svelte-lazy.svg)](LICENSE)\n[![live demo](https://img.shields.io/badge/demo-live-blue?logo=svelte)](https://isaiahcoroama.github.io/svelte-lazy/)\n\nLazy-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.\n\n```sh\nnpm install @coroama/svelte-lazy\n# or\nbun add @coroama/svelte-lazy\n```\n\nPeer dependency: `svelte ^5.0.0`. Built on top of [`@coroama/svelte-box`](https://www.npmjs.com/package/@coroama/svelte-box), pulled in as a transitive; you do not install it directly.\n\nLive demo: <https://isaiahcoroama.github.io/svelte-lazy/>. The site root is a versions index that links to every deployed playground build. `latest/` tracks `master` and redeploys after a green CI run whose merge touches `src/`, `static/`, or a build config file. Each `v*` tag deploys to its own `v<tag>/` subpath, and the master deploy backfills any missing tags. Compare the `lazy()` factory, the `<Lazy>` wrapper, prefetch-on-hover, and reset-and-retry side-by-side without cloning the repo.\n\n## Contents\n\n- [Why does this exist](#why-does-this-exist)\n- [Quick start](#quick-start)\n- [`lazy()` vs `<Lazy>`](#lazy-vs-lazy)\n- [Usage](#usage)\n    - [Inline `{#await}` pattern](#inline-await-pattern)\n    - [The `<Lazy>` wrapper](#the-lazy-wrapper)\n    - [Prefetch on hover](#prefetch-on-hover)\n    - [Retry after a failed load](#retry-after-a-failed-load)\n    - [Passing a `LazyComponent` across boundaries](#passing-a-lazycomponent-across-boundaries)\n- [API reference](#api-reference)\n- [Patterns and pitfalls](#patterns-and-pitfalls)\n    - [One `lazy()` per chunk](#one-lazy-per-chunk)\n    - [`bind:` works through the children snippet](#bind-works-through-the-children-snippet)\n    - [Don't `await ensure()` inside a `$derived`](#dont-await-ensure-inside-a-derived)\n    - [`reset()` does not cancel an in-flight load](#reset-does-not-cancel-an-in-flight-load)\n    - [`loaded` is implicit](#loaded-is-implicit)\n- [TypeScript](#typescript)\n- [Performance](#performance)\n    - [The right comparison](#the-right-comparison)\n    - [Where the library actually helps](#where-the-library-actually-helps)\n    - [Cost model](#cost-model)\n    - [What the bench measures](#what-the-bench-measures)\n    - [Benchmark results](#benchmark-results)\n    - [Reproducing locally](#reproducing-locally)\n    - [What this means for a real app](#what-this-means-for-a-real-app)\n- [SSR and SvelteKit](#ssr-and-sveltekit)\n- [Bundle size](#bundle-size)\n- [Compatibility](#compatibility)\n- [Debugging](#debugging)\n- [Caveats](#caveats)\n- [Status and testing](#status-and-testing)\n- [Maintenance and support](#maintenance-and-support)\n- [AI assistance disclosure](#ai-assistance-disclosure)\n- [License](#license)\n- [Repository](#repository)\n\n## Why does this exist\n\nSvelte 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?\n\nBecause the glue between the two is repetitive and easy to get wrong:\n\n```svelte\n<!-- illustrative pseudocode, annotate types if you copy this -->\n<script>\n    let Comp = $state(null);\n    let loading = $state(false);\n    let error = $state(null);\n\n    async function load() {\n        if (Comp || loading) return;\n        loading = true;\n        try {\n            const mod = await import('./HeavyPanel.svelte');\n            Comp = mod.default;\n        } catch (e) {\n            error = e;\n        } finally {\n            loading = false;\n        }\n    }\n</script>\n```\n\nEvery call site re-implements:\n\n- A null-check to avoid double-loading.\n- A `loading` flag for the spinner.\n- An `error` slot for failures.\n- A way to share the in-flight promise between callers (e.g. prefetch on hover + render on click).\n- A way to pass the not-yet-loaded handle into a child component without losing reactivity.\n\n`svelte-lazy` collapses all of that into one reactive handle (`LazyComponent`) plus one optional wrapper (`<Lazy>`):\n\n```svelte\n<script>\n    import { lazy, Lazy } from '@coroama/svelte-lazy';\n    const heavy = lazy(() => import('./HeavyPanel.svelte'));\n</script>\n\n<Lazy lazy={heavy}>\n    {#snippet children(HeavyPanel)}\n        <HeavyPanel data={rows} />\n    {/snippet}\n</Lazy>\n```\n\nThe `lazy(...)` factory returns a `LazyBox`-backed cell that:\n\n- Dedupes concurrent calls automatically (one chunk, one network request).\n- Survives being passed across function, class, and component boundaries reactively, because it inherits the cross-boundary semantics of `svelte-box`.\n- Accepts both async (`() => import('./X.svelte')`) and sync (`() => ({ default: X })`) loaders. Sync loaders are normalized to a resolved promise so the call site is uniform.\n- Exposes a tiny API: `prefetch()`, `ensure()`, `reset()`, plus the underlying `.value` cell.\n\n## Quick start\n\n```svelte\n<script lang=\"ts\">\n    import { lazy, Lazy } from '@coroama/svelte-lazy';\n\n    const profilePanel = lazy(() => import('./ProfilePanel.svelte'));\n</script>\n\n<Lazy lazy={profilePanel}>\n    {#snippet children(ProfilePanel)}\n        <ProfilePanel />\n    {/snippet}\n    {#snippet pending()}\n        <p>Loading…</p>\n    {/snippet}\n</Lazy>\n```\n\n`<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](#prefetch-on-hover).\n\n## `lazy()` vs `<Lazy>`\n\nThe library ships one runtime primitive and one optional wrapper. Pick based on whether you need to control the template structure.\n\n| Feature                                      | `lazy()` (inline `{#await}`) | `<Lazy>` wrapper           |\n| -------------------------------------------- | ---------------------------- | -------------------------- |\n| Dedupe concurrent loads                      | yes                          | yes                        |\n| Reactive `.value` cell crosses boundaries    | yes                          | yes                        |\n| Manual `prefetch()` / `ensure()` / `reset()` | yes                          | yes                        |\n| `bind:` on the lazy-loaded component         | yes                          | yes (via children snippet) |\n| Custom pending / error UI                    | yes                          | yes (named snippets)       |\n| Automatic prefetch on mount                  | no, call yourself            | yes                        |\n| Auto-retry after external `reset()`          | no, call `prefetch()` again  | yes                        |\n| One-liner call site                          | no, four-line template       | yes                        |\n\nReach 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.\n\n## Usage\n\n### Inline `{#await}` pattern\n\nThe cell is a `LazyBox<{ default: Component }>` from `@coroama/svelte-box`, which exposes `.value` as `Promise<{ default: Component }> | null`. Once the loader fires, the promise lives on `.value` and `{#await}` does the rest:\n\n```svelte\n<script lang=\"ts\">\n    import { lazy } from '@coroama/svelte-lazy';\n\n    let source = $state('# Hello');\n    let dirty = $state(false);\n\n    const editor = lazy(() => import('./MarkdownEditor.svelte'));\n</script>\n\n<button onclick={() => editor.prefetch()}>edit</button>\n\n{#if editor.value}\n    {#await editor.value then { default: MarkdownEditor }}\n        <MarkdownEditor bind:value={source} bind:dirty />\n    {/await}\n{/if}\n```\n\nThe outer `{#if editor.value}` guards the not-yet-fired case. After `prefetch()`, `{#await}` handles pending, resolved, and rejected branches.\n\n### The `<Lazy>` wrapper\n\nThe same example through `<Lazy>`:\n\n```svelte\n<script lang=\"ts\">\n    import { lazy, Lazy } from '@coroama/svelte-lazy';\n\n    let source = $state('# Hello');\n    let dirty = $state(false);\n\n    const editor = lazy(() => import('./MarkdownEditor.svelte'));\n</script>\n\n<Lazy lazy={editor}>\n    {#snippet children(MarkdownEditor)}\n        <MarkdownEditor bind:value={source} bind:dirty />\n    {/snippet}\n    {#snippet pending()}<p>Loading editor...</p>{/snippet}\n    {#snippet error(err)}<p>Failed: {err.message}</p>{/snippet}\n</Lazy>\n```\n\n`<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.\n\nThe `error` snippet is typed `Snippet<[E]>` with `E` defaulting to `Error`, so the snippet body reads `err.message` without a cast in the common case. If your loader can reject with a non-Error value (custom fetch interceptor that throws a string, a plain object, etc.) annotate the wrapper as `<Lazy lazy={editor} ...>` with `LazyProps<typeof MarkdownEditor, unknown>` (or just write the wrapper with `E = unknown` at the call site) and narrow inside the snippet:\n\n```svelte\n{#snippet error(err)}\n    <p>Failed: {err instanceof Error ? err.message : String(err)}</p>\n{/snippet}\n```\n\n### Prefetch on hover\n\n`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:\n\n```svelte\n<button\n    onpointerenter={() => settings.prefetch()}\n    onfocus={() => settings.prefetch()}\n    onclick={() => (showSettings = true)}\n>\n    settings\n</button>\n\n{#if showSettings}\n    <Lazy lazy={settings}>\n        {#snippet children(Settings)}<Settings />{/snippet}\n    </Lazy>\n{/if}\n```\n\nBy the time the user clicks, the chunk is already in cache. The wrapper has nothing to fetch.\n\n### Retry after a failed load\n\nIf 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:\n\n```svelte\n<Lazy lazy={panel}>\n    {#snippet children(Panel)}<Panel />{/snippet}\n    {#snippet error(err)}\n        <p>Failed to load: {err.message}</p>\n        <button onclick={() => panel.reset().prefetch()}>retry</button>\n    {/snippet}\n</Lazy>\n```\n\nInside `<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.\n\nDuring 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.\n\n`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.\n\n### Passing a `LazyComponent` across boundaries\n\nThe 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:\n\n```ts\n// stores/panels.ts\nimport { lazy, type LazyComponent } from '@coroama/svelte-lazy';\n\nexport class PanelStore {\n    settings = lazy(() => import('../panels/Settings.svelte'));\n    profile = lazy(() => import('../panels/Profile.svelte'));\n\n    /** Warm up everything the user is likely to open next. */\n    warmAll() {\n        this.settings.prefetch();\n        this.profile.prefetch();\n    }\n}\n\nexport const panels = new PanelStore();\n```\n\n```svelte\n<!-- App.svelte -->\n<script>\n    import { panels } from './stores/panels';\n    import { Lazy } from '@coroama/svelte-lazy';\n</script>\n\n<button onpointerenter={() => panels.warmAll()}>open menu</button>\n\n<Lazy lazy={panels.settings}>\n    {#snippet children(Settings)}<Settings />{/snippet}\n</Lazy>\n```\n\nSame handle, multiple call sites, one network request.\n\n## API reference\n\n### `lazy(loader)`\n\nFactory. Returns a fresh `LazyComponent<T>` wrapping `loader`. `T` infers from the loader's `default` export.\n\n```ts\ndeclare function lazy<T>(loader: LazyLoaderFn<T>): LazyComponent<T>;\n```\n\n`LazyLoaderFn<T>` and `ComponentCell<T>` are defined under [Types](#types) below.\n\n### `class LazyComponent<T>`\n\nExtends `LazyBox<ComponentCell<T>>` from `@coroama/svelte-box`. The cell holds a cached promise once the loader fires, or `null` until then. One method on top of the inherited surface:\n\n| Member                      | Description                                                                                                                                                                    |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `new LazyComponent(loader)` | Direct construction. Prefer `lazy(loader)`.                                                                                                                                    |\n| `.value`                    | Inherited from `LazyBox`. The cached promise, or `null` if the loader hasn't fired. Reactive.                                                                                  |\n| `.prefetch()`               | Inherited from `LazyBox`. Fire the loader if needed, return the import promise. Idempotent: subsequent calls reuse the same promise. Sync loaders are normalized to a promise. |\n| `.ensure()`                 | Awaits the loader and the resulting `tick()`. Use before measuring the DOM or focusing inside the lazy tree.                                                                   |\n| `.reset()`                  | Inherited from `LazyBox`. Clear the cached promise so the next `prefetch()` / `ensure()` re-fires the loader. Returns `this` for chaining. Used for retry-after-failure.       |\n\n`LazyBox` extends `MutCoreBox` directly, so it does not carry the `BaseBox` helper mixins (`get`, `set`, `del`, `snapshot`, `eager`, `toJSON`, type guards, `toConst`). Read and write through `.value`.\n\n### `<Lazy>`\n\nWrapper 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.\n\nThe prop shape is exported as `LazyProps<T>` for callers who want to write their own wrappers on top:\n\n```ts\nimport type { LazyProps } from '@coroama/svelte-lazy';\n\n// LazyProps is defined as:\n// type LazyProps<T extends AnyComponent = AnyComponent, E = Error> = {\n//     lazy: LazyComponent<T>;\n//     children: Snippet<[T]>;\n//     pending?: Snippet;\n//     error?: Snippet<[E]>;\n// };\n```\n\nThe second type parameter `E` types the value passed to the `error` snippet. It defaults to `Error` because modern browsers reject dynamic `import()` with a `TypeError` (subclass of `Error`), so the snippet body reads `err.message` without a cast in the common case. Callers whose loaders can reject with non-Error values (custom interceptors that throw strings or plain objects) override with `LazyProps<MyPanel, unknown>` and narrow inside the snippet. The default is a caller-asserted contract; the runtime promise rejection is still whatever the platform threw.\n\n- `children` is required and receives the resolved component constructor. Use a normal `<Component>` tag inside the snippet to pass props, `bind:`, slots, and events.\n- `pending` renders during the load. Defaults to nothing.\n- `error` renders if the loader rejects. Defaults to nothing.\n\n### Types\n\n```ts\ntype AnyComponent = Component<any, any>;\ntype ComponentCell<T> = { default: T };\n\n// Specialization of `@coroama/svelte-box`'s `LazyLoaderFn` to a component\n// cell, so the call-site type names the component directly. Accepts sync\n// or async loaders.\ntype LazyLoaderFn<T> = () => ComponentCell<T> | Promise<ComponentCell<T>>;\n```\n\nBoth `() => import('./X.svelte')` and `() => ({ default: X })` satisfy `LazyLoaderFn<typeof X>`.\n\n## Patterns and pitfalls\n\n### One `lazy()` per chunk\n\n`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.\n\n### `bind:` works through the children snippet\n\nThe `<Lazy>` `children` snippet hands you the component constructor, not a pre-bound element. Bind whatever you want:\n\n```svelte\n<script>\n    let dirty = $state(false);\n    let values = $state({});\n</script>\n\n<Lazy lazy={form}>\n    {#snippet children(Form)}\n        <Form bind:dirty bind:values />\n    {/snippet}\n</Lazy>\n```\n\nThis is why `<Lazy>` takes a snippet instead of forwarding props directly: forwarding can't carry `bind:` through.\n\n### Don't `await ensure()` inside a `$derived`\n\n`$derived` is synchronous. Use `$effect` for the await, and always handle rejection because `ensure()` propagates whatever the loader threw:\n\n```svelte\n<script>\n    $effect(() => {\n        if (showPanel.value) {\n            panel\n                .ensure()\n                .then(() => focusInput())\n                .catch((err) => console.error('lazy load failed', err));\n        }\n    });\n</script>\n```\n\n### `reset()` does not cancel an in-flight load\n\nDynamic `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.\n\n### `loaded` is implicit\n\nThere'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.\n\n## TypeScript\n\nInference works through Vite's import types:\n\n```ts\nconst panel = lazy(() => import('./Settings.svelte'));\n// panel: LazyComponent<typeof import('./Settings.svelte').default>\n```\n\nTo annotate a parameter that accepts any lazy component, use the bare class:\n\n```ts\nimport type { LazyComponent } from '@coroama/svelte-lazy';\n\nfunction warm(panel: LazyComponent) {\n    panel.prefetch();\n}\n```\n\nThe default type parameter is `AnyComponent`, so `LazyComponent` without an argument is the right type for \"any code-split component\".\n\n## Performance\n\nThe 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.\n\n### The right comparison\n\nThe honest comparison is **lazy loading vs no lazy loading**, not \"this library vs a hand-rolled loader.\"\n\n- **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.\n- **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.\n\nBoth 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.\n\n### Where the library actually helps\n\nWhat the library gives you over a hand-rolled equivalent is correctness and ergonomics:\n\n- One line per lazy boundary instead of 15. Teams that find hand-rolling too tedious skip lazy loading entirely and ship larger bundles.\n- 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.\n- 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.\n- The cell is a `LazyBox` from `@coroama/svelte-box`. Passing it through plain functions, class fields, and component props keeps reactivity intact without re-wrapping.\n\nIf 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.\n\n### Cost model\n\nThe hot path is `prefetch()`. Each call does:\n\n1. One reactive read of `this.value` for the falsy check.\n2. If the cell is empty, one reactive write that assigns the import promise.\n3. Returns the cell.\n\nAfter 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()`).\n\n`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.\n\n`reset()` is a single reactive write of `null` and a return of `this`. No measurable cost.\n\nThe 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.\n\nMemory per `LazyComponent` is one `LazyBox` instance plus one private loader field. The `LazyBox` itself is a small object with one `$state` cell. There is no proxy layer (this uses `LazyBox` from `@coroama/svelte-box`, which extends `MutCoreBox` directly) so the per-instance overhead is the same as a class with a single `$state` field, which is the cheapest reactive container available.\n\n### What the bench measures\n\n`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)`.\n\nThe scenarios are app-shaped, not micro-shaped.\n\n- **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.\n- **Menu hover, 8 cold prefetches.** User mouses across a menu, each item warms its panel. Fires once per item per session.\n- **Menu re-hover, 8 warm prefetches.** User mouses back, every call is a no-op cache hit. The path that fires most often.\n- **First open, `ensure()` with tick.** User clicks, the panel ensures itself before code that touches the DOM. Includes the `await tick()` step.\n- **Retry, `reset()` + `prefetch()`.** Deploy mid-session leaves a panel rejected. Retry button clears and refires.\n- **Store pattern, 10 fields, `warmAll()`.** A class with ten lazy fields plus a method that prefetches them all. Common for \"preload this section\" buttons.\n- **Cross-boundary call, `warm(panel)`.** A panel is passed into a function from another component. One call per interaction.\n- **Per-render `.value` read.** One read per render, not a tight loop. This is the realistic cost of a `{#if lazy.value}` guard.\n\n### Benchmark results\n\nNumbers below come from a single Chromium run on Linux x86_64 through `@vitest/browser-playwright`, captured 2026-05-18 against `1.0.0` with the cell backed by `LazyBox` (no proxy, no helper mixins). Treat them as ballpark figures. They drift between machines, browsers, and Svelte versions.\n\n**Reading the column**: `Nx slower` means the library completed `1/N` of the baseline's operations in the same wall-clock time.\n\n| Scenario                                   | Baseline                              | LazyComponent |\n| ------------------------------------------ | ------------------------------------- | ------------- |\n| App boot, 20 panels (`lazy()` factory)     | 20 hand-rolled cache objects          | 4.51x slower  |\n| App boot, 20 panels (`new LazyComponent`)  | 20 hand-rolled cache objects          | 4.41x slower  |\n| Menu hover, 8 cold `prefetch()`            | 8 manual `if(!cache) cache = load()`  | 3.15x slower  |\n| Menu re-hover, 8 warm `prefetch()` (no-op) | 8 manual `if(!cache)` no-op checks    | 3.25x slower  |\n| First open, `ensure()` with tick           | `await load(); await Promise.resolve` | 1.42x slower  |\n| Retry, `reset() + prefetch()` round trip   | `cache = null; if(!cache) load()`     | 2.32x slower  |\n| Store pattern, 10 fields, `warmAll()`      | class with 10 hand-rolled caches      | 2.54x slower  |\n| Cross-boundary `warm(panel)` call          | function mutates a cache object       | 2.14x slower  |\n| Sync loader, 8 cold `prefetch()` (new 1.0) | 8 hand-rolled sync `if(!cache)` loads | 3.06x slower  |\n| Per-render `.value` read on a warm cell    | plain object field read               | 2.28x slower  |\n\nThe cluster around 2x to 4.5x is the cost of the 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.42x because `ensure()` is dominated by `await tick()`, which is the same on both sides, so the per-call gap is diluted.\n\nThere 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.\n\n### Reproducing locally\n\n```sh\nbun run bench          # human-readable\nbun run bench:json     # machine-readable, writes bench-results.json\n```\n\nThe bench file is `benchmarking/lazy.svelte.bench.ts`. Edit it to add a scenario that matches your app's call pattern.\n\n### What this means for a real app\n\nThe 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.\n\nA 60Hz frame is 16 ms. The library only does meaningful work during three events:\n\n- The first `prefetch()` on a given `LazyComponent`. Cost is dominated by the network request, not by the library.\n- The first render after the loader resolves. Cost is the inner component's render work, not the wrapper.\n- A `reset()` + `prefetch()` retry. Two reactive writes and a new network request.\n\nCompare 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.\n\nIf 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 reactive `$state` cell.\n\n## SSR and SvelteKit\n\nDynamic `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.\n\nTwo practical notes:\n\n- **Don't put a `LazyComponent` in `load()` return data.** It's a class instance with a `LazyBox` inside, not serializable. Construct lazy handles on the client or in a shared module, not in route data.\n- **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.\n\n## Bundle size\n\nThe 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 `LazyBox`.\n\nTree-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.\n\n## Compatibility\n\n- **Svelte**: declared peer of `^5.0.0`. The library uses the runes API (`$state`, `$props`, snippets) through `@coroama/svelte-box`.\n- **TypeScript**: works under `strict` mode. Loader inference unifies with Vite's `() => import('./X.svelte')` automatically.\n- **Node**: 20.6 or newer for the build toolchain. The compiled package runs anywhere a modern JS runtime does.\n- **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.\n\n## Debugging\n\nA `LazyComponent` prints as a class instance with a `LazyBox` inside, which is rarely what you want in the console. Read `.value` directly for a non-reactive view of the cached promise:\n\n```js\nconsole.log(panel.value); // null, or the cached Promise\nconsole.log($state.snapshot(panel.value)); // detached copy if you need one\n```\n\nIf 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.\n\nFor unit tests, `panel.value` is null or a Promise, both safe to compare with `expect(...).toEqual(...)`. To assert resolved content, await the cell:\n\n```ts\nimport type { Component } from 'svelte';\nimport MarkdownEditor from './MarkdownEditor.svelte';\n\nawait panel.ensure();\nconst { default: Loaded } = await panel.value!;\nexpect(Loaded).toBe(MarkdownEditor);\n// `Loaded` is typed as `Component<...>`, the same shape MarkdownEditor exports.\n```\n\n## Caveats\n\n- **No `get` / `set` / `snapshot` helpers.** `LazyBox` is the thin variant of svelte-box's cell hierarchy. Read and write through `.value`. To replace the cached promise (rare), assign `panel.value = newPromise`.\n- **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.\n- **`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.\n- **`<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.\n- **`structuredClone(panel)` throws.** The cell is a class instance wrapped around reactive state, not a plain object. Clone `panel.value` instead.\n- **The `error` snippet receives `Error` by default.** Modern browsers reject dynamic `import()` with a `TypeError`, so the snippet body reads `err.message` without a cast in the common case. If your loader can reject with a non-Error value (a custom interceptor that throws a string or a plain object), set the second type parameter of `LazyProps` to `unknown` and narrow inside the snippet with `err instanceof Error ? err.message : String(err)`. Svelte's inline `{:catch err}` binding still types `err` more loosely (effectively `any`) regardless of the wrapper's `E`; that is Svelte's choice, not the library's.\n\n## Status and testing\n\nThis is a young, single-maintainer project.\n\nThe repository ships:\n\n- A Vitest browser-mode suite (`@vitest/browser-playwright`) across three files:\n    - [tests/component.svelte.test.ts](tests/component.svelte.test.ts): the core class, `lazy()` factory, idempotent `prefetch()`, `ensure()` + tick semantics, `reset()` chaining, retry after rejection, sync-throw normalization, sync-loader support, inferred type at the factory.\n    - [tests/lazy.svelte.test.ts](tests/lazy.svelte.test.ts): the `<Lazy>` wrapper's pending/error/children snippets and auto re-prefetch on external reset.\n    - [tests/cross-boundary.svelte.test.ts](tests/cross-boundary.svelte.test.ts): passing the cell through plain functions and class fields.\n- A benchmark file at [benchmarking/lazy.svelte.bench.ts](benchmarking/lazy.svelte.bench.ts) comparing the library against a hand-rolled object-literal baseline. Run `bun run bench` to reproduce.\n- A live demo route at [src/routes/+page.svelte](src/routes/+page.svelte) showing the wrapper, the inline pattern with `bind:`, and the retry path with simulated failure.\n- 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.\n\n## Maintenance and support\n\nSingle-maintainer project.\n\n- **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).\n- **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.\n- **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](CONTRIBUTING.md) for the practical workflow.\n- **Security disclosures** go to the channel documented in [SECURITY.md](SECURITY.md). Do not open public issues for vulnerability reports.\n- **Single-maintainer dependency chain.** The runtime depends on [`@coroama/svelte-box`](https://www.npmjs.com/package/@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.\n- **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.\n\n## AI assistance disclosure\n\nParts 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.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\n## Repository\n\nSource, issues, and changelog: <https://github.com/IsaiahCoroama/svelte-lazy>. The changelog follows the [Keep a Changelog](https://keepachangelog.com) format. Anything that changes the public surface gets an entry under that release.\n","readmeFilename":"README.md"}