# Results

> Measured numbers only: sizes per entry point, listener counts, table render and sort timings and fixture assertions, each with environment and command.

Page: https://svelte-lean.vercel.app/performance/results · Markdown: https://svelte-lean.vercel.app/performance/results.md · Index: https://svelte-lean.vercel.app/llms.txt

Measured numbers only, each read from a file a script in this repository writes; a missing file or key reads not measured.

## Sizes per entry point

Production output of each package from its own `yarn size`. Rows without a budget are recorded for comparison; the esbuild figures are not the quoted number ([Methodology](https://svelte-lean.vercel.app/performance/methodology.md)).

### @svelte-lean/primitives

| Subject | Entry or tool | raw | gzip | brotli | Budget | Status | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `core-only` | vite | 2157 B | 1078 B | 949 B | none | – | – |
| `core-only` | esbuild | 3147 B | 1558 B | 1381 B | none | – | – |
| `tabs` | vite | 4509 B | 2052 B | 1839 B | 1900 B brotli | within budget | no diagnostics · registration kept |
| `tabs` | esbuild | 5904 B | 2691 B | 2422 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `menu` | vite | 7156 B | 3107 B | 2773 B | 2800 B brotli | within budget | no diagnostics · registration kept |
| `menu` | esbuild | 13412 B | 5474 B | 4967 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `listbox` | vite | 5866 B | 2616 B | 2357 B | 2600 B brotli | within budget | no diagnostics · registration kept |
| `listbox` | esbuild | 7224 B | 3235 B | 2915 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `select` | vite | 14587 B | 5565 B | 5022 B | 5300 B brotli | within budget | no diagnostics · registration kept |
| `select` | esbuild | 21116 B | 8034 B | 7299 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `autocomplete` | vite | 11498 B | 4425 B | 3962 B | 4000 B brotli | within budget | no diagnostics · registration kept |
| `autocomplete` | esbuild | 17876 B | 6838 B | 6192 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `calendar` | vite | 12534 B | 5012 B | 4594 B | 5000 B brotli | within budget | no diagnostics · registration kept |
| `calendar` | esbuild | 13872 B | 5635 B | 5164 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `date-picker` | vite | 27281 B | 10358 B | 9475 B | 10200 B brotli | within budget | no diagnostics · registration kept |
| `date-picker` | esbuild | 33720 B | 12791 B | 11755 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `toast` | vite | 3977 B | 1809 B | 1616 B | 1800 B brotli | within budget | no diagnostics · registration kept |
| `toast` | esbuild | 5408 B | 2452 B | 2202 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `tree` | vite | 5391 B | 2384 B | 2140 B | 2400 B brotli | within budget | no diagnostics · registration kept |
| `tree` | esbuild | 6636 B | 2951 B | 2651 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `slider` | vite | 3498 B | 1738 B | 1549 B | 1700 B brotli | within budget | no diagnostics · registration kept |
| `slider` | esbuild | 4710 B | 2331 B | 2101 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `input` | vite | 4545 B | 2046 B | 1808 B | 2000 B brotli | within budget | no diagnostics · registration kept |
| `input` | esbuild | 5884 B | 2678 B | 2391 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `checkbox-group` | vite | 3488 B | 1638 B | 1456 B | 1600 B brotli | within budget | no diagnostics · registration kept |
| `checkbox-group` | esbuild | 4736 B | 2230 B | 1991 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `field/validation` | vite | 4453 B | 1942 B | 1714 B | 1900 B brotli | within budget | no diagnostics · registration kept |
| `field/validation` | esbuild | 5570 B | 2457 B | 2185 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `toggle-group` | vite | 4874 B | 2168 B | 1932 B | 2200 B brotli | within budget | no diagnostics · registration kept |
| `toggle-group` | esbuild | 6359 B | 2817 B | 2514 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `toolbar` | vite | 4026 B | 1920 B | 1706 B | 1800 B brotli | within budget | no diagnostics · registration kept |
| `toolbar` | esbuild | 5256 B | 2487 B | 2214 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `splitter` | vite | 4922 B | 2137 B | 1908 B | 2000 B brotli | within budget | no diagnostics · registration kept |
| `splitter` | esbuild | 6231 B | 2756 B | 2470 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `sortable` | vite | 9758 B | 4279 B | 3833 B | 4300 B brotli | within budget | no diagnostics · registration kept |
| `sortable` | esbuild | 11217 B | 4918 B | 4448 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `menu/contextmenu` | vite | 3845 B | 1840 B | 1631 B | 1800 B brotli | within budget | no diagnostics · registration kept |
| `menu/contextmenu` | esbuild | 4977 B | 2385 B | 2133 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `file-drop` | vite | 7507 B | 3324 B | 2983 B | 3300 B brotli | within budget | no diagnostics · registration kept |
| `file-drop` | esbuild | 8934 B | 3936 B | 3552 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `hover-card` | vite | 5078 B | 2247 B | 1986 B | 2000 B brotli | within budget | no diagnostics · registration kept |
| `hover-card` | esbuild | 11257 B | 4605 B | 4163 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `menubar` | vite | 5058 B | 2326 B | 2064 B | 2100 B brotli | within budget | no diagnostics · registration kept |
| `menubar` | esbuild | 10952 B | 4596 B | 4173 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `hint` | vite | 7090 B | 2986 B | 2687 B | 2900 B brotli | within budget | no diagnostics · registration kept |
| `hint` | esbuild | 8444 B | 3603 B | 3246 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `copy` | vite | 2930 B | 1464 B | 1291 B | 1300 B brotli | within budget | no diagnostics · registration kept |
| `copy` | esbuild | 4158 B | 2049 B | 1809 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `findable` | vite | 4628 B | 2117 B | 1887 B | 1900 B brotli | within budget | no diagnostics · registration kept |
| `findable` | esbuild | 6011 B | 2733 B | 2460 B | none | – | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, WeakSet |
| `select+search` | vite | 16141 B | 6045 B | 5461 B | 5500 B brotli | within budget | no diagnostics · registration kept |
| `select+search+create` | vite | 16675 B | 6175 B | 5573 B | 5600 B brotli | within budget | no diagnostics · registration kept |
| `anchor-position` | vite | 4395 B | 1928 B | 1741 B | 1800 B brotli | within budget | – |
| `popover/anchor` | vite | 3239 B | 1598 B | 1401 B | 1450 B brotli | within budget | – |
| `native-only` | vite | 12700 B | 3547 B | 2983 B | none | – | no runtime |

Command: `yarn workspace @svelte-lean/primitives size`. Sizes are deterministic for the toolchain in `docs/compatibility.md`.

Method

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.

### @svelte-lean/core

| Subject | Entry or tool | raw | gzip | brotli | Budget | Status | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `production` | esbuild | 5643 B | 2637 B | 2375 B | 2750 B gzip | within budget | diagnostics: \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, validate, WeakSet, is not a routed event type |
| `default` | esbuild | 5856 B | 2726 B | 2453 B | 2750 B gzip | within budget | diagnostics: console.warn, \_\_SLEAN\_DEBUG\_\_, Svelte Lean Runtime, validate, WeakSet, is not a routed event type |
| `consumer` | vite | 2503 B | 1239 B | 1089 B | 2750 B gzip | within budget | no diagnostics |
| `reorder` | esbuild | 3675 B | 1752 B | 1575 B | 1950 B gzip | within budget | no diagnostics |

Command: `yarn workspace @svelte-lean/core size`. Sizes are deterministic for the toolchain in `docs/compatibility.md`.

Method

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. "reorder" is dist/reorder.js (the @svelte-lean/core/reorder subpath), esbuild bundle + minify.

### @svelte-lean/table

| Subject | Entry or tool | raw | gzip | brotli | Budget | Status | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `renderer` | dist/index.js | 78741 B | 24816 B | 21768 B | 24900 B gzip | within budget | – |
| `headless` | dist/core.js | 5794 B | 2665 B | 2440 B | 2700 B gzip | within budget | – |
| `engine` | dist/table.svelte.js | 8488 B | 3538 B | 3244 B | 3600 B gzip | within budget | – |
| `grouping` | dist/grouping.js | 4675 B | 2149 B | 1962 B | 2400 B gzip | within budget | – |
| `pivot` | dist/pivot.js | 948 B | 526 B | 470 B | 600 B gzip | within budget | – |
| `editing` | dist/editing.js | 1367 B | 707 B | 617 B | 800 B gzip | within budget | – |
| `csv` | dist/export.js | 1220 B | 741 B | 646 B | 850 B gzip | within budget | – |
| `clipboard` | dist/clipboard.js | 1721 B | 895 B | 803 B | 1000 B gzip | within budget | – |
| `server` | dist/server.js | 561 B | 369 B | 310 B | 425 B gzip | within budget | – |
| `state` | dist/state.js | 1793 B | 796 B | 711 B | 900 B gzip | within budget | – |
| `styles` | dist/style.css | 42297 B | 9086 B | 7896 B | 9100 B gzip | within budget | – |

Command: `yarn workspace @svelte-lean/table size`. Sizes are deterministic for the toolchain in `docs/compatibility.md`.

Method

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.

### @svelte-lean/styles

| Subject | Entry or tool | raw | gzip | brotli | Budget | Status | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `accordion.css` | – | 6055 B | 974 B | 817 B | none | – | minified before compression |
| `alert.css` | – | 7115 B | 984 B | 814 B | none | – | minified before compression |
| `autocomplete.css` | – | 8262 B | 1010 B | 867 B | none | – | minified before compression |
| `avatar.css` | – | 3264 B | 578 B | 464 B | none | – | minified before compression |
| `badge.css` | – | 5081 B | 927 B | 763 B | none | – | minified before compression |
| `base.css` | – | 5738 B | 673 B | 563 B | none | – | minified before compression |
| `breadcrumb.css` | – | 3023 B | 648 B | 532 B | none | – | minified before compression |
| `button-group.css` | – | 3612 B | 511 B | 405 B | none | – | minified before compression |
| `button.css` | – | 7338 B | 1032 B | 871 B | none | – | minified before compression |
| `calendar.css` | – | 13362 B | 1719 B | 1486 B | none | – | minified before compression |
| `card.css` | – | 12972 B | 1218 B | 1007 B | none | – | minified before compression |
| `carousel.css` | – | 6785 B | 986 B | 839 B | none | – | minified before compression |
| `checkbox-group.css` | – | 2632 B | 460 B | 364 B | none | – | minified before compression |
| `checkbox.css` | – | 8771 B | 1192 B | 1001 B | none | – | minified before compression |
| `control.css` | – | 30523 B | 2736 B | 2383 B | none | – | minified before compression |
| `copy.css` | – | 1688 B | 360 B | 279 B | none | – | minified before compression |
| `date-picker.css` | – | 11419 B | 1417 B | 1225 B | none | – | minified before compression |
| `descriptions.css` | – | 3265 B | 555 B | 450 B | none | – | minified before compression |
| `dialog.css` | – | 15968 B | 1799 B | 1537 B | none | – | minified before compression |
| `empty.css` | – | 4113 B | 690 B | 583 B | none | – | minified before compression |
| `field.css` | – | 5613 B | 703 B | 590 B | none | – | minified before compression |
| `file-drop.css` | – | 17070 B | 1898 B | 1640 B | none | – | minified before compression |
| `findable.css` | – | 2787 B | 423 B | 343 B | none | – | minified before compression |
| `hint.css` | – | 2749 B | 633 B | 517 B | none | – | minified before compression |
| `hover-card.css` | – | 2027 B | 386 B | 315 B | none | – | minified before compression |
| `index.css` | – | 1571 B | 325 B | 266 B | none | – | minified before compression |
| `input.css` | – | 29624 B | 2887 B | 2527 B | none | – | minified before compression |
| `kbd.css` | – | 1268 B | 372 B | 298 B | none | – | minified before compression |
| `listbox.css` | – | 5273 B | 923 B | 747 B | none | – | minified before compression |
| `menu.css` | – | 9058 B | 1192 B | 993 B | none | – | minified before compression |
| `menubar.css` | – | 2588 B | 598 B | 489 B | none | – | minified before compression |
| `meter.css` | – | 3659 B | 492 B | 392 B | none | – | minified before compression |
| `nav.css` | – | 13793 B | 1789 B | 1521 B | none | – | minified before compression |
| `pagination.css` | – | 5918 B | 970 B | 824 B | none | – | minified before compression |
| `popover.css` | – | 8781 B | 1011 B | 845 B | none | – | minified before compression |
| `progress.css` | – | 5368 B | 869 B | 723 B | none | – | minified before compression |
| `radio-group.css` | – | 17083 B | 1860 B | 1615 B | none | – | minified before compression |
| `rating.css` | – | 5532 B | 991 B | 794 B | none | – | minified before compression |
| `scroll-area.css` | – | 4010 B | 566 B | 453 B | none | – | minified before compression |
| `select.css` | – | 22717 B | 2395 B | 2105 B | none | – | minified before compression |
| `separator.css` | – | 1135 B | 286 B | 219 B | none | – | minified before compression |
| `skeleton.css` | – | 2953 B | 566 B | 481 B | none | – | minified before compression |
| `slider.css` | – | 11471 B | 1258 B | 1047 B | none | – | minified before compression |
| `sortable.css` | – | 6669 B | 984 B | 801 B | none | – | minified before compression |
| `splitter.css` | – | 4612 B | 791 B | 658 B | none | – | minified before compression |
| `steps.css` | – | 6743 B | 1102 B | 930 B | none | – | minified before compression |
| `tabs.css` | – | 5164 B | 915 B | 745 B | none | – | minified before compression |
| `timeline.css` | – | 3995 B | 718 B | 599 B | none | – | minified before compression |
| `toast.css` | – | 8747 B | 1254 B | 1073 B | none | – | minified before compression |
| `toggle-group.css` | – | 5363 B | 880 B | 726 B | none | – | minified before compression |
| `toggle.css` | – | 6509 B | 914 B | 766 B | none | – | minified before compression |
| `tokens.css` | – | 39559 B | 4041 B | 3369 B | none | – | minified before compression |
| `toolbar.css` | – | 2370 B | 482 B | 399 B | none | – | minified before compression |
| `tree.css` | – | 4483 B | 860 B | 722 B | none | – | minified before compression |
| `index.css (resolved)` | – | 435500 B | 31124 B | 24738 B | 31300 B gzip | within budget | minified before compression |

Command: `yarn workspace @svelte-lean/styles size`. Sizes are deterministic for the toolchain in `docs/compatibility.md`.

Method

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.

## What a consumer ships

Small applications that install the packages like a consumer, built in production mode, with Svelte Lean modules and Svelte's runtime in separate chunks. Assertions: each fixture's `expect.json` (module graph, forbidden strings, manifest).

| Fixture | Uses | Svelte Lean chunk, brotli | gzip | raw | Svelte runtime, brotli | CSS, brotli | Svelte Lean modules | Assertions |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `autocomplete-only` | owned autocomplete and a bare input with a datalist | 5350 B | 5929 B | 15770 B | 9852 B | 6361 B | 19 | 52 passed |
| `date-picker-only` | date picker and calendar markup | 11092 B | 12127 B | 32238 B | 12758 B | 7746 B | 26 | 57 passed |
| `fields-native` | bare inputs, checkbox, switch, segmented radio group | 0 B | 0 B | 0 B | 8710 B | 8647 B | 0 | 32 passed |
| `fields-owned` | input shell, validated field, checkbox group, slider group, toggle | 3803 B | 4220 B | 10983 B | 8710 B | 8222 B | 29 | 49 passed |
| `listbox-only` | listbox markup | 2357 B | 2612 B | 5866 B | 8710 B | 4450 B | 15 | 49 passed |
| `manual-registration` | tabs markup, register module imported by hand | 1839 B | 2048 B | 4509 B | 8710 B | 4427 B | 14 | 45 passed |
| `native-only` | button, dialog, popover markup and styles | 0 B | 0 B | 0 B | 8710 B | 6149 B | 0 | 34 passed |
| `query-native-page` | the query package installed, nothing imported | 0 B | 0 B | 0 B | 9480 B | 0 B | 0 | 20 passed |
| `query-production` | reads, writes, http() and laravel() | 21109 B | 23268 B | 62652 B | 11602 B | 0 B | 31 | 50 passed |
| `query-read-only` | defineQuery and createQuery only | 14416 B | 15705 B | 41989 B | 12013 B | 0 B | 27 | 42 passed |
| `query-unused-endpoints` | 500 query endpoints, two imported | 14412 B | 15706 B | 41989 B | 10081 B | 0 B | 27 | 40 passed |
| `select-only` | select markup | 6749 B | 7455 B | 20096 B | 12687 B | 7374 B | 23 | 55 passed |
| `select-search` | selects with search and create | 7323 B | 8067 B | 22186 B | 14104 B | 7374 B | 27 | 56 passed |
| `sortable-only` | – | 3833 B | 4285 B | 9758 B | 11397 B | 4503 B | 15 | 51 passed |
| `table-basic` | \<DataTable /\> and its stylesheet | 20841 B | 23613 B | 65404 B | 15980 B | 7783 B | 10 | 55 passed |
| `tabs-and-menu` | tabs and menu markup | 4600 B | 5094 B | 13033 B | 8710 B | 6138 B | 20 | 46 passed |
| `tabs-only` | tabs markup, discovered by the plugin | 1839 B | 2048 B | 4509 B | 8710 B | 4427 B | 14 | 48 passed |

Environment: node v22.23.3,vite 7.3.6,svelte 5.57.1,@sveltejs/vite-plugin-svelte 6.2.4. Command: `yarn test:fixtures`.

Method

Production \`vite build\` per fixture with Vite defaults (esbuild minify, modulepreload polyfill). Sizes are the emitted JavaScript and CSS files: raw bytes, gzip level 9, brotli quality 11 (node:zlib). "svelteLean" is the svelte-lean chunk that the fixture plugin isolates for every @svelte-lean/\* module and "svelteRuntime" the svelte-runtime chunk for Svelte's runtime (both include chunk import/export bindings); 0 B means no such chunk was emitted. "modules.svelteLean" lists rendered @svelte-lean/\* modules with their pre-minification rendered length.

## Listeners

The claim is an exact assertion: a thousand tabs roots install the same per-type listeners as one. The panel reads the runtime's account and an independent count of every `addEventListener` call on this page; mount a hundred roots and compare.

Tabs roots on this page: 1

Root 1, overview panel.

Arrow keys, Home and End work in every root; the behavior writes aria-selected, tabindex and hidden to the markup and holds no state.

- **Runtime**: Read from the page after it loads; requires JavaScript.

Gate: listeners.spec.ts (`yarn workspace @svelte-lean/playground test:e2e --project=chromium tests/primitives/listeners.spec.ts`). [Proof](https://svelte-lean.vercel.app/proof.md) mounts up to a thousand roots. Related counts in files:

| Subject | Value | Method | File |
| --- | --- | --- | --- |
| DOM nodes with a thousand tabs roots | 8050 | document.getElementsByTagName("\*").length after load | `apps/playground/results/bench.json` |
| JavaScript event listeners on the page after five mount and unmount cycles | 30 | Performance.getMetrics JSEventListeners at baseline and after the last cycle | `apps/playground/results/stress.json` |

## Timings

Median of five fresh page loads, every sample recorded; min and max show the spread, and another machine gives other numbers. Startup and interaction are separate entries.

| Subject | Median | Min | Max | p95 | Samples | Method |
| --- | --- | --- | --- | --- | --- | --- |
| `table.1000-rows.mount` | 287.3 ms | 235.1 ms | 303 ms | 303 ms | 5 | component init to the second animation frame after onMount, client render of the whole table (ssr=false); median of 5 loads |
| `table.1000-rows.render` | 202.7 ms | 195.9 ms | 219.9 ms | 219.9 ms | 5 | unmount + mount of the DataTable via {#key}, tick() then two animation frames; median of 5 |
| `table.1000-rows.sort` | 156.1 ms | 149.9 ms | 161 ms | 161 ms | 5 | header button click (ascending by revenue) to the second animation frame; median of 5 |
| `table.10000-rows.mount` | 3088.2 ms | 2272 ms | 3139.1 ms | 3139.1 ms | 5 | component init to the second animation frame after onMount, client render of the whole table (ssr=false); median of 5 loads |
| `table.10000-rows.render` | 2200.9 ms | 2101.9 ms | 2223.9 ms | 2223.9 ms | 5 | unmount + mount of the DataTable via {#key}, tick() then two animation frames; median of 5 |
| `table.10000-rows.sort` | 1599.5 ms | 1571.2 ms | 1618.5 ms | 1618.5 ms | 5 | header button click (ascending by revenue) to the second animation frame; median of 5 |
| `tabs.1000-roots.dom-content-loaded` | 36.4 ms | 17.8 ms | 38.2 ms | 38.2 ms | 5 | PerformanceNavigationTiming.domContentLoadedEventEnd of a fresh page.goto; median of 5 loads |
| `tabs.1000-roots.load` | 36.8 ms | 18.4 ms | 38.7 ms | 38.7 ms | 5 | PerformanceNavigationTiming.loadEventEnd of a fresh page.goto; median of 5 loads |
| `tabs.1000-roots.time-to-interactive` | 68.6 ms | 64.5 ms | 73.7 ms | 73.7 ms | 5 | performance.now() in onMount of the client-rendered page: 1000 roots in the document and the behavior registered; median of 5 loads |
| `tabs.1000-roots.first-interaction` | 1.9 ms | 1.7 ms | 2.1 ms | 2.1 ms | 5 | performance.now() around trigger.click() on root 500 right after load (synchronous routing + DOM writes); median of 5 loads |
| `tabs.1000-roots.repeat-interaction` | 0.9 ms | 0.8 ms | 1.7 ms | 1.7 ms | 5 | performance.now() around a second trigger.click() on root 900; median of 5 loads |
| `tabs.1000-roots.dom-nodes` | 8050 | 8050 | 8050 | 8050 | 5 | document.getElementsByTagName("\*").length after load |

Environment: chromium 153.0.8010.12, linux 6.12.107+deb13-cloud-amd64 x64, AMD EPYC 9555P 64-Core Processor, 16 cores, 31.3 GiB, Node v22.23.3, production build, headless. Date: 2026-10-05T07:11:03.593Z. Command: `yarn bench:publish`.

## Memory after stress cycles

Heap after forced garbage collection, DOM nodes and listeners against a warm-up baseline; the growth tolerance is in each entry's method. Runs nightly, not on every pull request.

- A thousand tabs roots mounted and unmounted five times.
- A menu opened and closed two hundred times.
- One Tier 2 popup opened with ArrowDown and closed with Escape two hundred times, a session per cycle; the scoped document `pointerdown` listeners installed are recorded next to the JSEventListeners count, for the autocomplete, the owned select and the date picker.

| Subject | Median | Min | Max | p95 | Samples | Method |
| --- | --- | --- | --- | --- | --- | --- |
| `stress.tabs.mount-unmount.heap-fresh` | 2104624 bytes | 2104624 bytes | 2104624 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs on /primitives/tabs/many?n=1 right after load |
| `stress.tabs.mount-unmount.heap-baseline` | 2625984 bytes | 2625984 bytes | 2625984 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs, n=1, after one warm-up cycle 1 → 1000 → 1 |
| `stress.tabs.mount-unmount.heap-peak` | 11767108 bytes | 11700384 bytes | 11816348 bytes | 11816348 bytes | 5 | JSHeapUsedSize after two forced GCs with 1000 roots mounted and interacted with, one sample per cycle |
| `stress.tabs.mount-unmount.heap-per-cycle` | 8861436 bytes | 8823824 bytes | 9141124 bytes | 9141124 bytes | 5 | heap-peak minus the heap after the previous cleanup: what 1000 roots cost, one sample per cycle |
| `stress.tabs.mount-unmount.heap-after-cleanup` | 2945448 bytes | 2819596 bytes | 2992524 bytes | 2992524 bytes | 5 | JSHeapUsedSize after two forced GCs back at n=1, one sample per cycle |
| `stress.tabs.mount-unmount.heap-growth` | 375372 bytes | 375372 bytes | 375372 bytes | not measured | 1 | heap after 5 cycles minus baseline; tolerance max(2 MiB, 15 % of baseline) = 2097152 B |
| `stress.tabs.mount-unmount.nodes-baseline` | 332 | 332 | 332 | not measured | 1 | Performance.getMetrics Nodes at n=1 after the warm-up cycle |
| `stress.tabs.mount-unmount.nodes-after-cleanup` | 332 | 332 | 332 | 332 | 5 | Performance.getMetrics Nodes back at n=1, one sample per cycle; allowance 100 above baseline |
| `stress.tabs.mount-unmount.js-event-listeners` | 30 | 30 | 30 | 30 | 2 | Performance.getMetrics JSEventListeners at baseline and after the last cycle |
| `stress.menu.open-close.heap-baseline` | 2056892 bytes | 2056892 bytes | 2056892 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs on /primitives/menu after one open/close cycle |
| `stress.menu.open-close.heap-after` | 2116344 bytes | 2116344 bytes | 2116344 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs after 200 open/close cycles |
| `stress.menu.open-close.heap-growth` | 59452 bytes | 59452 bytes | 59452 bytes | not measured | 1 | heap after minus baseline; tolerance max(2 MiB, 15 % of baseline) = 2097152 B |
| `stress.menu.open-close.nodes` | 292 | 292 | 292 | 292 | 2 | Performance.getMetrics Nodes at baseline and after the cycles |
| `stress.menu.open-close.cycles-focused-inside` | 200 | 200 | 200 | not measured | 1 | cycles (of 200) in which the first item had focus when Escape was sent |
| `stress.autocomplete.open-close.heap-baseline` | 2487228 bytes | 2487228 bytes | 2487228 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs on /primitives/autocomplete after one open/close cycle |
| `stress.autocomplete.open-close.heap-after` | 2715436 bytes | 2715436 bytes | 2715436 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs after 200 open/close cycles (ArrowDown, Escape) of one Tier 2 root |
| `stress.autocomplete.open-close.heap-growth` | 228208 bytes | 228208 bytes | 228208 bytes | not measured | 1 | heap after minus baseline; tolerance max(2 MiB, 15 % of baseline) = 2097152 B |
| `stress.autocomplete.open-close.nodes` | 1446 | 1446 | 1446 | 1446 | 2 | Performance.getMetrics Nodes at baseline and after the cycles |
| `stress.autocomplete.open-close.js-event-listeners` | 47 | 47 | 47 | 47 | 2 | Performance.getMetrics JSEventListeners at baseline and after the cycles; a retained session listener would raise it |
| `stress.autocomplete.open-close.session-listeners-installed` | 200 | 200 | 200 | not measured | 1 | document pointerdown installations recorded by the wrapped addEventListener during 200 cycles: one scoped listener per open session |
| `stress.autocomplete.open-close.cycles-active-while-open` | 200 | 200 | 200 | not measured | 1 | cycles (of 200) in which an option was active while the popup was open |
| `stress.select.open-close.heap-baseline` | 2452392 bytes | 2452392 bytes | 2452392 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs on /primitives/select after one open/close cycle |
| `stress.select.open-close.heap-after` | 2671640 bytes | 2671640 bytes | 2671640 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs after 200 open/close cycles (ArrowDown, Escape) of one Tier 2 root |
| `stress.select.open-close.heap-growth` | 219248 bytes | 219248 bytes | 219248 bytes | not measured | 1 | heap after minus baseline; tolerance max(2 MiB, 15 % of baseline) = 2097152 B |
| `stress.select.open-close.nodes` | 965 | 965 | 965 | 965 | 2 | Performance.getMetrics Nodes at baseline and after the cycles |
| `stress.select.open-close.js-event-listeners` | 51 | 51 | 51 | 51 | 2 | Performance.getMetrics JSEventListeners at baseline and after the cycles; a retained session listener would raise it |
| `stress.select.open-close.session-listeners-installed` | 200 | 200 | 200 | not measured | 1 | document pointerdown installations recorded by the wrapped addEventListener during 200 cycles: one scoped listener per open session |
| `stress.select.open-close.cycles-open` | 200 | 200 | 200 | not measured | 1 | cycles (of 200) in which an option was active while the popup was open |
| `stress.date-picker.open-close.heap-baseline` | 2645220 bytes | 2645220 bytes | 2645220 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs on /primitives/date-picker after one open/close cycle |
| `stress.date-picker.open-close.heap-after` | 2893408 bytes | 2893408 bytes | 2893408 bytes | not measured | 1 | JSHeapUsedSize after two forced GCs after 200 open/close cycles (ArrowDown into the grid, Escape) of one Tier 2 root |
| `stress.date-picker.open-close.heap-growth` | 248188 bytes | 248188 bytes | 248188 bytes | not measured | 1 | heap after minus baseline; tolerance max(2 MiB, 15 % of baseline) = 2097152 B |
| `stress.date-picker.open-close.nodes` | 2438 | 2438 | 2438 | 2438 | 2 | Performance.getMetrics Nodes at baseline and after the cycles |
| `stress.date-picker.open-close.js-event-listeners` | 52 | 52 | 52 | 52 | 2 | Performance.getMetrics JSEventListeners at baseline and after the cycles; a retained session listener would raise it |
| `stress.date-picker.open-close.session-listeners-installed` | 200 | 200 | 200 | not measured | 1 | document pointerdown installations recorded by the wrapped addEventListener during 200 cycles: one scoped listener per open session |
| `stress.date-picker.open-close.cycles-open` | 200 | 200 | 200 | not measured | 1 | cycles (of 200) in which focus was on a day of the grid while the popup was open |

Environment: chromium 153.0.8010.12, linux 6.12.107+deb13-cloud-amd64 x64, AMD EPYC 9555P 64-Core Processor, 16 cores, 31.3 GiB, Node v22.23.3, production build, headless. Date: 2026-10-05T07:11:35.687Z. Command: `yarn bench:publish`.

## Table with 1000 rows

The specimen of the table timings, every row in the document: no pagination, no virtualization. The readings under it are taken in your browser, not published numbers.

Mounts on demand and requires JavaScript.

Sort by clicking a header; the benchmark's sort entry times a header click to the second animation frame; published values are in the timings table above.

`Rows.svelte`

```svelte
<script lang="ts">
	import { DataTable, type Column } from '@svelte-lean/table';
	import '@svelte-lean/table/style.css';
	import { rows, type Row } from './rows'; // one thousand generated rows

	const columns: Column<Row>[] = [
		{ id: 'name', header: 'Name' },
		{ id: 'email', header: 'Email' },
		{ id: 'team', header: 'Team' },
		{ id: 'region', header: 'Region' },
		{ id: 'status', header: 'Status' },
		{ id: 'revenue', header: 'Revenue', align: 'end' },
		{ id: 'joined', header: 'Joined' }
	];
</script>

<!-- Every row is in the document: no pagination, no virtualization. -->
<DataTable
	data={rows}
	{columns}
	getRowId={(row) => row.id}
	label="One thousand rows"
	pagination={false}
	stickyHeader
	height="24rem"
/>
```

## Source

- Size scripts: primitives, core, table, styles.
- Fixtures: fixtures/ with the runner run.mjs and the assertion helper assert.mjs.
- Benchmarks: tabs.bench.spec.ts, table.bench.spec.ts; stress: tests/stress.
- Listener gate: listeners.spec.ts; the independent counter in packages/test-utils.
- Claim registry: docs/proof/claims.md; the audit of the checklist: docs/proof.
