sveltelean
Versionv0.2.0 GitHub

Quantities and files

Each quantity has one script and one output file. The artifact files, the fixture results and the published benchmark and stress runs are committed. A bench or stress run writes to the playground's test-results/ directory, which is not tracked; yarn bench:publish runs both projects in one invocation and copies the two files to apps/playground/results/ with the run's environment block and a publication date.

QuantityScriptOutputCommitted
Kernel entry and one consumer behaviorpackages/core/scripts/size.mjspackages/core/artifacts/size.jsonyes
Tier 1 registrations (tabs, menu) including the kernelpackages/primitives/scripts/size.mjspackages/primitives/artifacts/size.jsonyes
CSS per file and the resolved index.csspackages/styles/scripts/size.mjspackages/styles/artifacts/size.jsonyes
Table entries (renderer, headless, each module)packages/table/scripts/size.mjspackages/table/artifacts/size.jsonyes
What a consumer application shipsfixtures/run.mjsfixtures/results.jsonyes
Listener counts at one and a thousand rootsapps/playground/tests/primitives/listeners.spec.tsPlaywright report, annotation "listeners" (an exact assertion, not a file)no
Startup and interaction timingsapps/playground/tests/bench, published by yarn bench:publishapps/playground/results/bench.jsonyes
Heap, nodes and listeners after stress cyclesapps/playground/tests/stress, published by yarn bench:publishapps/playground/results/stress.jsonyes

Compression

Every size is reported three times: raw bytes, gzip and brotli. gzip is level 9 and brotli is quality 11, both from node:zlib, through one helper (fixtures/_shared/measure.mjs) shared by the package scripts and the fixture runner, so numbers across files are comparable. A CDN's compressor may differ by a few bytes; budgets leave room for that.

Production builds only

  • Package sizes: the dist/ output of the package, bundled with Vite in production mode (Rollup tree-shaking, esbuild minify, ES2022, no module preload) for the numbers that carry a budget, and with esbuild under the production export condition for comparison. The two differ because esbuild tree-shakes before it inlines cross-module constants and keeps helpers behind folded development gates; the Vite figure is what applications ship and is the one quoted.
  • Consumer fixtures: a plain vite build per fixture in its own process, Vite defaults, the workspace packages consumed through node_modules links exactly as an installed consumer would. A plugin records the rendered module graph and isolates every @svelte-lean/* module into one chunk and Svelte's runtime into another, so Svelte Lean bytes are never mixed with Svelte's.
  • Browser tests and benchmarks: the playground is built with adapter-static and served with vite preview. A development-server mode exists for debugging; bundle and benchmark specs are skipped or marked development in that mode and are never quoted.

Environment

A number is quoted together with the environment it was taken in. The values below are read from the measurement files of this build, not typed.

Fixtures (fixtures/results.json)
node v22.23.3vite 7.3.6svelte 5.57.1@sveltejs/vite-plugin-svelte 6.2.4
Benchmarks (apps/playground/results/bench.json)
chromium 153.0.8010.53 darwin 27.0.0 arm64 Apple M2 8 cores · 16 GiB node v22.23.3 · production · headless 2026-09-27T20:46:17.407Z
Size file, @svelte-lean/core
ESM, target es2022, gzip level 9, brotli quality 11 (node:zlib). "production" and "default" are dist/index.js with every export, esbuild bundle + minify, under the production export condition and the runtime-detected env module respectively. "consumer" is one Tier 1 behavior using parts and emit, built with Vite in production mode; it must list no diagnostics.
Size file, @svelte-lean/primitives
ESM, target es2022, gzip level 9, brotli quality 11 (node:zlib). Tier 1 and Tier 2 entries are dist/<name>/register.js with @svelte-lean/core from the workspace. "vite" builds the public specifier with Vite in production mode; the brotli budget applies to it and it must list no diagnostics and keep the registration. "esbuild" bundles the same file with esbuild under the production condition; it keeps development helpers core references from folded DEV gates and is recorded for comparison. "core-only" is a registration with an empty handler. "native-only" imports the root entry and a Tier 0 subpath and must contain no runtime.
Size file, @svelte-lean/styles
bytes = shipped source; minified = comments and redundant whitespace removed by scripts/check.mjs minify (a bundler minifier may be slightly smaller); gzip level 9 and brotli quality 11 of the minified text. standalone = the file with the files it @imports inlined. "index.css (resolved)" inlines every @import once, depth first, and carries the budget.
Size file, @svelte-lean/table
Each entry bundled independently with esbuild (bundle + minify, ESM); shared code is counted per entry here and de-duplicated by consumer bundlers. Svelte peer runtime excluded. gzip level 9 / brotli quality 11 (node:zlib). Budgets are gzip.

Toolchain versions come from yarn.lock and are listed in docs/compatibility.md. The bench environment block records date, Node, platform and OS release, architecture, CPU model, core count, memory, browser name and version from Playwright, mode and the headless flag.

Sampling and statistics

  • Sizes are deterministic for a given toolchain; one build is one result.
  • Timings: each benchmark entry is the median of five fresh page loads (RUNS in the bench specs). Every sample is recorded next to the median, the minimum, the maximum and the nearest-rank p95, with a one-sentence method per entry stating exactly what was timed.
  • Startup and interaction are separate entries and are never combined into one score.
  • Benchmarks assert only generous sanity thresholds. The numbers are the result; a threshold is never tightened to a machine-specific value.

Listener counts

The listener spec compares two independent accounts: the runtime's own listeners() and a count taken by wrapping EventTarget.prototype.addEventListener before any page script runs. A listener is attributed to Svelte Lean when its source reads the protocol attribute; the whole per-type count on window, document, <html> and <body> must be identical for one root and for a thousand, so SvelteKit's own listeners cannot mask a regression. The proof page runs the same two accounts on this site.

What is and is not compared

  • Every published number is Svelte Lean compared with itself: one root against a thousand, a native-only page against a tabs page, a basic table against the same table with a module imported. That is the architecture proof.
  • No number compares Svelte Lean with another library. A comparison would have to implement the same behavior with approximately equivalent markup and similar styling complexity, build both sides for production, publish its source, disclose versions, browser and hardware, repeat runs with median and variance, and hide no losing scenario. None exists yet.
  • Never compared: a development build against a production build; an unstyled Svelte Lean page against a styled page of anything; a single primitive against another library's full stack.

Losing results

If a measurement gets worse, or a comparison is lost, the number is published with the same method and environment as any other. Budgets fail CI so a regression is noticed; a budget is raised only with the reason in the commit and a changeset. Removing an inconvenient number from a report is not an option.

Reproducing each number

All commands run from the repository root with the Node version in .nvmrc and Yarn 4. The playground's Playwright configuration launches the locally installed Google Chrome when it exists and Playwright's Chromium otherwise; bench.json records which one ran.

Terminal
yarn install --immutable
yarn build:packages                       # dist/ for every package; the fixtures consume it

yarn size                                 # every packages/<name>/artifacts/size.json
yarn workspace @svelte-lean/primitives size   # one package

yarn test:fixtures                        # every fixture → fixtures/results.json
node fixtures/run.mjs tabs-only           # one fixture; results.json is left alone
node fixtures/_shared/assert.mjs fixtures/tabs-only   # re-assert an existing build

yarn playwright install --with-deps chromium          # CI; locally the installed Chrome is used
yarn workspace @svelte-lean/playground test:e2e --project=chromium tests/primitives/listeners.spec.ts
yarn workspace @svelte-lean/playground bench          # → apps/playground/test-results/bench.json (not tracked)
yarn workspace @svelte-lean/playground stress         # → apps/playground/test-results/stress.json (not tracked)
yarn bench:publish                        # bench + stress in one run → apps/playground/results/ (tracked)

Publication rule for timings

apps/playground/test-results/ is ignored by git, so a benchmark run is not published by running it. To publish timings, run yarn bench:publish on an idle machine: it runs the bench and stress projects in one Playwright invocation and copies bench.json and stress.json to apps/playground/results/, each with the run's environment block kept and publishedAt set to the run's date; then commit results/. The site and the README read those two files; where a file or key is absent, timings are "not measured". The nightly workflow runs the same command and uploads the result as an artifact without committing it. Numbers from another machine will differ: a published timing is a statement about the recorded environment, not a promise.

Regression thresholds

  • Sizes: the budgets in each size script, set a small margin above the measured value, fail yarn size and therefore yarn verify (Budgets).
  • Fixtures: the expect.json assertions (module graph, code substrings, manifest) fail yarn test:fixtures; the sizes in results.json are recorded, not gated.
  • Listeners: an exact equality on the per-type counts, and identical whole-page counts for one and a thousand roots, fails the browser suite.
  • Timings: sanity thresholds only. A timing regression is caught by comparing the published bench.json with a new run by hand or through the nightly workflow's uploaded artifacts, not by a gate.

The canonical text of this page is docs/proof/methodology.md; the definition of done for a performance claim is in CONTRIBUTING.md.