Why It's Fast
The pitch for mx is easy to misread. "790 bytes, no build step, four globals" sounds like a toy - something you'd use for a widget and outgrow by the second feature. The benchmarks say the opposite: it beats every larger framework on nearly every measurable axis - allocation, DOM overhead, style recalc, frame budget, battery - on real application workloads, while shipping ~26× less JavaScript than the React build of the same app, and less even than uhtml, the next-lightest of the bunch.
The argument the rest of these docs make quietly, this page makes loudly: the platform is the framework. The DOM already reconciles, the browser already manages layout, custom elements already have a lifecycle. mx is the thin layer that makes the platform ergonomic, and then gets out of the way. The numbers below are what "getting out of the way" buys.
How these were measured
Three suites: a paginated 10k-row table plus a depth-12 tree (4,095 nodes), a real-time dashboard (30 and 200 assets, 20-50 Hz feeds), and a multi-view SPA (lazy routes, 10 components). Production builds, identical data and CSS across frameworks, Playwright headless Chromium, M1 / M4 Pro / M5 Pro hardware. Where a competitor wins, it's listed - see where mx is not the best choice.
The size story
mx's engine is 790 bytes standalone - but the number that matters is what a real app actually ships. The same app (a paginated 10k-row table) built in each framework, total JS sent to the browser, brotli (one consistent methodology - no bare-runtime sleight of hand):
| Framework | Same app, total JS (brotli) | vs mx |
|---|
| mx | ~2.0 KB | 1× |
| uhtml | ~6.8 KB | ~3.4× |
| Svelte | ~11.5 KB | ~5.7× |
| Vue | ~22.5 KB | ~11× |
| Angular | ~46.6 KB | ~23× |
| React + ReactDOM | ~51.9 KB | ~26× |
Why total-app-JS, not "bare runtime"
"Runtime size" is slippery: Svelte and Angular have no standalone runtime (it compiles / tree-shakes into the app), and a tree-shaking framework's real footprint differs from its full prebuilt bundle (Vue's full runtime is ~35 KB brotli but tree-shakes to ~22 KB in this app; React + ReactDOM is ~40 KB standalone but ~52 KB once app code is added). Measuring the same app in every framework sidesteps all of that - and for the compiled ones, a minimal app like this is effectively the runtime floor (a Svelte hello-world build lands at ~10.7 KB, right beside the ~11.5 KB here).
mx's engine plus terse app code is the entire ~2.0 KB - smaller than even uhtml, the next-lightest. And the framework share is a fixed floor that never amortizes away: on a full SPA in the suite, framework + app JS came to 6,926 B for mx vs 26,479 B for Svelte - 3.8× smaller (the ratio compresses as apps grow and app code dominates, but on the small, framework-heavy end above it's 5-26×). As a share of deployed JavaScript, the framework was 9.5% of the mx app and 58.4% of the Svelte one: more than half of what the user downloads is machinery, not product.
The build runs the other direction
mx's pipeline is subtractive - concatenate and minify, and the output is ~12% smaller than the source, byte-for-byte reproducible, zero dependencies. A compiler-based pipeline is additive: the runtime is woven into every compiled view, so output runs ~24% larger than source (worst case measured: a 2 KB component compiling to 3.9 KB).
# mx: concatenate, minify, compress. Reproducible byte-for-byte. 0 deps.
cat src/*.js | minify > app.min.js # output is ~12% SMALLER than source
# A compiler-based framework: bundler + compiler + 25 MB of node_modules.
# Output is ~24% LARGER than source - the runtime is woven into every view.
Wire-to-source ratio: mx 0.32, Svelte 1.10. Every mx view shrinks through the build; every compiled view expands.
The speed story
Smaller doesn't mean slower. On the workloads that dominate real apps, mx matches or beats frameworks tens of times its size:
- Route navigation: 4.29 ms (mx) vs 4.26 ms (Svelte) - indistinguishable, while shipping 3.8× less JavaScript.
- Sort 10k rows: mx 2.5 ms, rock-steady, vs Svelte's 7-9 ms - roughly 3× faster (the multiple drifts with hardware; mx's 2.5 ms doesn't). mx patches in place; the others run a keyed longest-increasing-subsequence pass.
- Per-node diff: 0.78 µs (~3,100 CPU cycles on M4 Pro) - near the hardware floor for touching a DOM node in JavaScript. Angular 0.83, Svelte 1.15, React 1.39.
Consistency beats peak
mx's slowest operation is 1.92× its fastest. Svelte's is 7.75× (a compiled fast path for navigation, a much slower path for sort). Users feel variance - a jarring stall - more than they feel a slightly lower average. mx is uniform across operations; that's a deliberate property of brute-force reconciliation over a fast diff.
Where does the speed come from? Not cleverness - the opposite. The attribute cache skips the DOM for anything unchanged, so a full re-render mostly costs a tree walk and a string of === checks. The expensive browser work - style recalc, layout - simply never gets triggered for the ~97% of nodes that didn't change.
The memory story
This is the most lopsided result, and the most architectural. Heap allocated per 500-operation run:
| Framework | Heap delta / 500 ops |
|---|
| mx | ~1.1 KB |
| Angular | 474 KB |
| React | 926 KB |
| Vue | 1.22 MB |
| Svelte | 6.98 MB |
The mx-to-Svelte gap is roughly 1,500×, and it holds across hardware (M4 Pro: 4.3 KB vs 6.96 MB; M5 Pro: 1.1 KB vs 6.98 MB). That invariance is the tell: it's a property of reconciler design, not of a particular V8 version. mx allocates ~45 MB of throwaway descriptions per run and retains ~1.1 KB of it - a survivor rate near zero, which means effectively no major garbage collection.
Memory is time
Allocation isn't free even when it's collected - the GC has to scan it. Subtract script, style, and layout from total task time and what's left is mostly minor-GC: mx 6.9 s, React 10.8 s, Svelte 13.3 s, Vue 15.1 s on the paginated suite. The 6.5 s Svelte spends beyond mx is largely the cost of scanning what it allocated. On an 8-hour trading dashboard the trajectories diverge for good: mx's steady-state heap shrinks (allocation ≈ 0, GC reclaims earlier bloat); Svelte's grows.
Zero framework DOM tax
On a deep tree of 16,389 application nodes, mx's total DOM count is 1.5× the app nodes - and that 0.5 is pure browser chrome, not framework nodes. mx adds zero nodes of its own. Svelte's total is 3.5× (it inserts ~2.5 phantom marker nodes per app node); React and Angular 2.0×; Vue 2.5×. Per-node heap tells the same story: mx 275 B/node (the bare V8 DOM-wrapper floor) vs React 500, Vue 639, Svelte 1,397.
The deep-tree story
A virtual DOM diffs the whole tree; its cost scales with tree size, not with how much changed. mx's cost is the same tree walk, but the cache makes the mutation count irrelevant. Toggling a deep tree (4,095 nodes, depth 12):
| Cascade | mx | Svelte | Angular | Vue |
|---|
| 1 leaf changes | 3.2 ms | 4.7 | 3.4 | 22.3 |
| 500 leaves change | 3.5 ms | 5.4 | 3.9 | 22.9 |
mx is flat across a 500× change in workload - 3.2 ms to 3.5 ms. The cost is walking the tree, not applying the mutations. Two things fall out of this table:
- Angular converges with mx on tree-local updates (3.4 vs 3.2 ms, within 6%) - because Angular is, in effect, ~158 KB of machinery wrapped around the same algorithm mx implements in well under a kilobyte. It only pays for its size on broad updates.
- Vue is ~7× slower here: forward-propagating reactivity through 12 levels costs more than mx's full diff.
The real-time story
A live dashboard - 30 assets, a 20 Hz price feed, sparklines and movers updating continuously - is where allocation and frame budget bite. mx holds 60 fps with zero dropped frames and zero long tasks, matching Svelte frame-for-frame, at a 97% attribute-cache hit rate: of ~1,543 nodes walked per tick, only ~44 produce a real DOM mutation.
120 Hz reveals what 60 Hz hides
At 60 Hz, vsync rounding hides framework differences - everyone hits 60 fps. At 120 Hz the 8.33 ms budget is unforgiving, and the gap reappears. On a paint-synchronized sort:
| Framework | Paint-sync sort | vs 8.33 ms budget |
|---|
| mx | 6.24 ms | fits |
| React | 7.32 ms | fits |
| Angular | 7.28 ms | fits |
| Vue | 10.94 ms | over |
| Svelte | 11.66 ms | over by 3.3 ms |
On a 120 Hz display, Svelte and Vue drop roughly every other frame on a sort; mx has ~2 ms of headroom to spare. Try the live reconciler benchmark: render() vs append().
The full-application story
Bytes and microseconds compound into a single figure: performance density - work done per kilobyte of JavaScript shipped. On the paginated suite, milliseconds of main-thread time per KB of deployed JS: mx 0.217, Svelte 0.036, React 0.016. mx does 13.6× more work per byte than React, 6.0× more than Svelte. (On the SPA suite, measured as wire bytes per millisecond of route nav, mx is 3.9× denser than Svelte - a different cut of the same story.)
- 27% less main-thread time on the paginated suite (19.5 s vs React 26.6 s, Svelte 27.1 s) - which is ~27% less battery drain on mobile.
- 46% fewer DOM nodes on a real SPA home view (241 vs 449); 22% fewer on the 200-asset dashboard.
- Load complete 84 ms vs 116 ms (3.4× less JS to parse); on a 3G connection, first paint ~284 ms sooner.
The headline, honestly
Same runtime speed as a leading compiled framework, at 3.8× less wire weight, 1,500× less heap churn, and a fraction of the DOM. Not "fast for its size" - fast, and small, at the same time, because the small is the fast: less code to parse, less to allocate, less for the browser to recompute.
Where mx is not the best choice
A benchmark page that only flatters the home team isn't worth reading. mx loses some races, and it's worth knowing which:
- Raw keyed-list creation (create-1,000-rows, krausest-style): uhtml's
html.for(obj) wins. mx's identity-move shines on reordering existing rows, not on bulk first creation. - First-paint LCP if you ship client-only: a client-rendered mx dashboard paints at ~200 ms vs a Svelte SSG build at 44 ms. Pre-rendered HTML wins first paint - but that's a deployment choice, not a ceiling: mx precompiles to static HTML and upgrades in place, so precompile the shell when LCP matters. The sample just happened to ship client-only.
- List-heavy declarative templates: Svelte's
{#each} + {#snippet} is genuinely terser for pure-presentational rows (one component measured 45% smaller). mx wins on stateful, imperative components; Svelte wins on declarative lists. - Page navigation micro-benchmark: Svelte edges mx by ~8% on isolated page-nav (a compiled binding fast path) - 1.3 ms vs 1.2 ms. It's 0.1 ms; you won't feel it, but it's real.
- Batch paint: on a 10-page burst, Svelte's compiled batch-paint path is ~8% faster (mx 10.7 ms vs Svelte 9.4 ms). mx wins single sorts decisively but concedes this one.
The real axis isn't a compiler versus mx - it's static precompilation versus a dynamic app, and mx does both. It renders to static HTML through any minimal server DOM (happy-dom, or linkedom from uhtml's author) - this very site is built that way - and then upgrades the same markup in place on the client, with no hydration handshake. So the usual consensus applies without picking a framework: precompile content and marketing pages for instant first paint; ship a plain SPA for SaaS and anything behind a login (first paint matters less there, and the shell caches); and for a hybrid - a social feed, say - prerender the shell and let mx take over, which is exactly what parasitic hydration is for. The places mx genuinely loses are the narrow ones above - bulk keyed-list creation, the terseness of a pure declarative list - not "dynamic vs static," which it handles either way.
Going deeper
The mechanism behind these numbers is the attribute cache - read that for how no-op renders stay free. For how the same patterns hold up across a 200-component app, see Scaling. For the philosophy behind it, What mx Is Not.
Run it yourself
The render() vs append() benchmark is live and runnable in your browser - no setup, no build. It isolates the reconciler against naive DOM construction so you can see the cache working.