Bundle sizes
sigmx has no runtime dependencies, so the numbers below are the whole cost. They come from building the real source with esbuild and are regenerated with npm run size -- --json website/src/data/sizes.json; the headline figure everywhere in these docs is brotli, which is what a CDN serves.
Configurations
Section titled “Configurations”| what you register | brotli | gzip | minified |
|---|---|---|---|
| core only | 4.5 KB | 4.9 KB | 10.6 KB |
| core, precompiled expressions (no runtime compiler) | 3.8 KB | 4.2 KB | 9.0 KB |
| core + text | 4.7 KB | 5.0 KB | 10.9 KB |
| minimal preset (signals, text, show, on) | 5.1 KB | 5.6 KB | 12.2 KB |
| essentials preset (state, rendering, forms, requests, morph) | 9.0 KB | 9.8 KB | 22.7 KB |
| minimal + CSP compiler | 5.3 KB | 5.8 KB | 12.6 KB |
| minimal + fetch + server handlers (morph) | 8.2 KB | 8.9 KB | 20.3 KB |
| everything (standalone build) | 12.2 KB | 13.4 KB | 32.1 KB |
| everything, shipped dist/sigmx.standalone.js (esbuild + terser) | 11.5 KB | 12.6 KB | 31.5 KB |
Measured 2026-09-06 with esbuild (minify, mangle internals), brotli quality 11 and gzip level 9. Regenerate with npm run size -- --json website/src/data/sizes.json.
For comparison, Datastar 1.0.3’s bundle (datastar.js, 23 plugins) is 11.8 KB brotli, 13.1 KB gzip and 32.8 KB minified, measured 2026-09-06 by npm run compare. sigmx’s “everything” build carries 50 plugins.
Per plugin
Section titled “Per plugin”Each row is the increase over the core-only build when that plugin alone is registered. Plugins that share helpers cost less together than the sum of these rows.
| plugin (added on top of the core) | brotli | gzip | minified |
|---|---|---|---|
| @delete | 1630 B | 1780 B | 3874 B |
| @get | 1626 B | 1780 B | 3874 B |
| @patch | 1611 B | 1781 B | 3874 B |
| @post | 1592 B | 1781 B | 3874 B |
| @put | 1592 B | 1781 B | 3874 B |
| patch-elements (morph) | 1345 B | 1484 B | 3942 B |
| bind | 714 B | 793 B | 1806 B |
| boost | 569 B | 633 B | 1412 B |
| on | 472 B | 537 B | 1178 B |
| animate | 466 B | 551 B | 1046 B |
| on-signal-patch | 462 B | 498 B | 1160 B |
| on-intersect | 445 B | 515 B | 1139 B |
| query-string | 429 B | 465 B | 955 B |
| collapse | 411 B | 456 B | 964 B |
| transition | 396 B | 428 B | 837 B |
| @ws | 394 B | 463 B | 999 B |
| on-raf | 387 B | 425 B | 968 B |
| on-resize | 372 B | 414 B | 941 B |
| init | 347 B | 384 B | 858 B |
| mask | 281 B | 344 B | 701 B |
| remove-me | 239 B | 269 B | 533 B |
| class | 214 B | 238 B | 563 B |
| on-interval | 212 B | 230 B | 459 B |
| style | 206 B | 258 B | 573 B |
| match-media | 204 B | 235 B | 471 B |
| indicator | 200 B | 234 B | 483 B |
| persist | 190 B | 209 B | 432 B |
| teleport | 189 B | 198 B | 401 B |
| scroll-into-view | 171 B | 234 B | 433 B |
| custom-validity | 166 B | 191 B | 397 B |
| ref | 155 B | 171 B | 361 B |
| attr | 148 B | 193 B | 407 B |
| json-signals | 146 B | 162 B | 327 B |
| signals | 142 B | 153 B | 309 B |
| computed | 141 B | 148 B | 302 B |
| replace-url | 137 B | 158 B | 305 B |
| show | 135 B | 156 B | 343 B |
| html | 120 B | 137 B | 259 B |
| text | 117 B | 134 B | 261 B |
| @intl | 114 B | 135 B | 254 B |
| cloak | 110 B | 118 B | 232 B |
| @fit | 105 B | 119 B | 204 B |
| effect | 101 B | 115 B | 225 B |
| @clipboard | 96 B | 119 B | 200 B |
| @toggleAll | 87 B | 98 B | 221 B |
| patch-signals | 67 B | 117 B | 245 B |
| @setAll | 47 B | 94 B | 212 B |
| @dispatch | 34 B | 73 B | 166 B |
| @peek | 19 B | 69 B | 163 B |
| @confirm | 16 B | 57 B | 106 B |
Measured 2026-09-06 with esbuild (minify, mangle internals), brotli quality 11 and gzip level 9. Regenerate with npm run size -- --json website/src/data/sizes.json.
Reading the table
Section titled “Reading the table”- Core is
createSigmxwith no plugins: the signal graph, store, expression compiler and mutation observer. With precompiled expressions the compiler goes and the core is 3.8 KB. - The request functions share one client, so registering all five costs about the same as one.
- patch-elements carries the DOM morph. Register it only if the server sends HTML; auto mode adds it whenever a request function is used.
- Import from
sigmx/pluginsfreely: the barrel is side-effect free and tree-shakes to what you name. Auto mode picks the list from your source for you. - The shipped
dist/sigmx.standalone.jsis passed through terser after esbuild, which is worth 6% brotli against the plain esbuild build of the same file. When you bundle sigmx yourself, your minifier decides; repeated plain statements compress better than clever compaction, which is why the source is written that way.
What real projects ship
Section titled “What real projects ship”The four example apps under examples/ use auto mode for real, measured from their own npm run build on 2026-09-06:
| example | plugins | brotli | gzip | minified |
|---|---|---|---|---|
express and hono (sigmxAuto, runtime compiler) |
14 | 9.1 KB | 9.9 KB | 23.2 KB |
vite (sigmxAuto with a custom plugin, runtime compiler) |
12 | 9.2 KB | 10.1 KB | 23.6 KB |
astro (auto mode and precompiled expressions, no fallback) |
13 | 8.2 KB | 8.9 KB | 21.8 KB |
The overview walks through how those lists were chosen.
Going smaller
Section titled “Going smaller”The kernel is close to its floor for what it does; build targets from es2021 to esnext make no measurable difference. The remaining levers were each measured by removing the feature from a scratch copy of the 0.1.x source and rebuilding; the table is in gzipped bytes saved on that build and has not been re-measured since, so read it for the order of magnitude. Rows for features 0.2.0 has since removed are gone.
| lever | core | everything | what you give up |
|---|---|---|---|
| drop the last-statement value in expressions | 104 | 100 | data-text="$a = 1; $a * 2" would yield undefined; single expressions unaffected |
drop namespace enumeration on $ |
95 | 102 | Object.keys($.user), 'x' in $.user, delete $.user.x; JSON.stringify keeps working |
drop data-ignore |
60 | 65 | no way to fence off a subtree |
| drop per-attribute error isolation | 28 | 37 | one broken expression aborts the scan; onError goes away |
| plain arrays (no mutation notify) | 22 | 16 | $items.push(x) would need $items = [...$items, x] |
| request client without form encoding | – | 194 | contentType: 'form' |
| request client without retry, backoff, reconnect | – | 153 | streams end on the first failure |
| morph without moving elements by id | – | 150 | elements with ids cannot move across ancestors; they are recreated |
| patch-elements without full-document responses | – | 95 | responses containing <!doctype>, <html>, <head> or <body> |
Two levers change nothing for people who do not use the feature and are the natural next step: making form encoding a plugin the request client looks up (about 190 bytes only when registered), and splitting the retry policy the same way. The rest trade documented behaviour for a few dozen bytes, which is not worth it by default; the numbers are here so the trade is explicit if you ever need it. The larger lever already taken is precompiling expressions, which removes 749 bytes brotli from any build.
Exotic options, measured
Section titled “Exotic options, measured”Transport matters more than code at this size. Three rows below were re-measured on the shipped 31.5 KB standalone file on 2026-09-06; the rest were measured on the 0.1.x standalone file (30.1 KB minified, 11.6 KB gzip, 10.5 KB brotli) and are marked as such.
| approach | result | verdict |
|---|---|---|
| brotli instead of gzip | 12.6 KB → 11.5 KB (−9%), current build | do it: precompress and serve Content-Encoding: br |
zstd (Content-Encoding: zstd, Chrome and Firefox) |
12.3 KB at level 19, current build | between gzip and brotli; no reason to prefer it here |
| Precompiled expressions (a build plugin turns every attribute expression into a function table) | core 4.5 KB → 3.8 KB (−16%), current build, and no new Function at all |
implemented: see precompiled expressions |
Shared-dictionary brotli (Compression Dictionary Transport, Use-As-Dictionary) |
a new release compressed against the previous one: 10.5 KB → 4.9 KB (−54%), 0.1.x build | real for returning visitors on Chromium; a server header, not a code change |
| Lazy directive chunks (core first, each directive loaded on first sight) | initial payload 4.4 KB gzip; a typical directive chunk 200 B, the request client 1.9 KB, 0.1.x build | practical with HTTP/2 and modulepreload; trades bytes on first paint for round trips |
| Roadroller (demoscene self-extracting packer) | raw 30.1 KB → 13.8 KB, but 10.5 KB → 10.3 KB under brotli, 0.1.x build | only for raw-byte limits such as 13 KB game jams; pointless behind HTTP compression |
| Closure Compiler SIMPLE / ADVANCED | SIMPLE grew the file to 14.8 KB gzip (it re-lowered syntax); ADVANCED refused the code, 0.1.x build | whole-program renaming also cannot survive a Proxy-based store, where property names are data |
| Bytecode (V8 code cache, Hermes, QuickJS) | not measurable: browsers do not accept bytecode from the network; shipping an engine such as QuickJS in WebAssembly costs 200 KB and more before any of our code | dead end on the web |
| WebAssembly (Rust, Zig, AssemblyScript) | not measured: WebAssembly cannot touch the DOM, so every attribute, event and mutation becomes a JavaScript import call; the smallest DOM-binding toolchains start above 20 KB, and Wasm bytecode compresses worse than JavaScript because brotli’s built-in dictionary is tuned for text | wrong tool for a DOM library |
The short version: serve brotli, add a dictionary header if your users come back, and precompile expressions if your templates are static. Bytecode and WebAssembly cannot help a library whose entire job is talking to the DOM.