This is the third post in a series. Start with the first post if you haven't already, or read the architecture post for the full mental model.
Framework posts love benchmarks. They also love cherry-picking them. I want to be upfront about what these benchmarks measure, what they don't, and what the numbers actually mean for your daily work.
All benchmarks are in the public repo. Run them yourself on your hardware.
If you find a flaw in the benchmark methodology, please open an issue or PR. I'd rather fix the benchmark than win an argument.
Executive Summary
Here's the honest version, up front, instead of buried at the end: Inglorious Web usually doesn't beat Vue or Svelte, and it doesn't beat Solid at all. What it offers instead is comparable performance in these workloads — close enough that users are unlikely to perceive a difference in normal interaction — with a simpler programming model and a testing story that the others don't match. That's the actual pitch of this post. Everything below is the evidence for it, including the parts where Inglorious Web loses.
Three findings, briefly:
- On high-frequency bulk updates (dashboards, tickers, live data grids), Inglorious Web matches Vue and Svelte's frame rates without any optimization work, in a 4–5x smaller bundle than React. Solid is faster still — its compiler gives it a real, measured scripting-time advantage here.
- On sparse, deeply nested updates (the shape of spreadsheets, conditional forms, dependency chains), fine-grained frameworks — Vue's proxies, Svelte's runes, Solid's signals — all converge on doing almost nothing, while Inglorious Web's full-tree walk costs more. Under realistic usage, that cost is a few milliseconds per update and isn't something a user would notice; under a sustained synthetic stress test, it adds up to something a profiler can see.
- On developer experience, Inglorious Web's event-handler model tests without a runtime, a store, or mocking — call the handler, assert the result. That's the thing none of the other frameworks give you in the same form, and it's the actual differentiator, not the frame rate.
If you want the deep-dive tables and the "why" behind each number, read on.
Benchmark 1: The Dashboard
What It Measures
A live-updating dashboard with 1,000 rows of data updating in real time, 10 random rows updated every 10ms (100 updates/second), 4 live charts, filtering and sorting, and an FPS counter. This simulates factory monitoring dashboards, stock tickers, and logistics tracking systems — the kind of UI where high-frequency bulk updates are the norm.
Fairness Rules
All implementations share the same dataset and update frequency, the same chart model and data slicing, the same business logic shape (event-driven handlers responding to the same event names), the same top-level component structure, and the same CSS.
One specific thing worth calling out: the four chart components originally rendered their bars using each framework's default list primitive — Vue, Svelte, and React all key their bar lists by index, but Solid's <For> defaults to keying by item identity/value instead. Since the bar values are plain numbers, that meant Solid's list could skip re-invoking the row function for any bar whose numeric value happened to be unchanged since the last tick — a shortcut the other three don't get with index-based keys. To rule out that this was inflating Solid's numbers, all four Solid variants were switched from <For> to <Index>, which keys by position exactly like the other frameworks. The numbers below reflect that fix.
The Full Results
| Variant | FPS (dev) | FPS (prod) | Bundle (kB) |
|---|---|---|---|
| React | 52 | 113 | 62.39 |
| React Memoized | 112 | 120 | 62.58 |
| React + RTK | 32 | 92 | 72.21 |
| React + RTK Memoized | 87 | 118 | 72.30 |
| React + RTK + Inglorious adapters | 29 | 74 | 79.18 |
| React + RTK + Inglorious adapters Memoized | 69 | 93 | 79.29 |
| React + Inglorious Store | 33 | 95 | 71.98 |
| React + Inglorious Store Memoized | 87 | 120 | 72.05 |
| Vue | 116 | 117 | 26.80 |
| Vue + Pinia | 117 | 117 | 28.56 |
| Svelte | 112 | 119 | 16.02 |
| Svelte + Store | 110 | 119 | 16.04 |
| Svelte + Runes | 102 | 118 | 14.13 |
| Solid | 120 | 119 | 18.38 |
| Solid Memoized | 120 | 120 | 18.34 |
| Solid + Store | 119 | 118 | 23.02 |
| Solid + Store Memoized | 120 | 119 | 23.03 |
| Inglorious Web | 118 | 120 | 16.29 |
| Inglorious Web Memoized | 115 | 120 | 16.35 |
Measured on MacBook Pro 16" 2023, Chrome 144, macOS Tahoe.
What the Numbers Mean
Solid is the fastest thing in this benchmark, even after the keying fix. It hits 120 FPS in dev and 118–120 in prod with zero optimization work. React's dev/prod gap is the compiler story: React 19.2 ships an automatic compiler that adds memoizations at build time, closing most of the gap manual React.memo/useMemo used to require — though the compiler only optimizes patterns it recognizes, so there are still cases where you're back to reasoning about the component tree by hand. The Inglorious adapter layer is the real overhead in the RTK variants (29 FPS dev, 74 FPS prod) — the cost is in the adapter bridging two state models, not in RTK's middleware. Vue, Svelte, and Inglorious Web all stay above 100 FPS in development and reach the monitor refresh limit in production, making them effectively indistinguishable in normal interaction.
The bundle size gap is structural. React core alone is 62KB; React + RTK reaches 72KB. Inglorious Web is 16KB total — framework, state manager, and renderer. Vue is 27KB, Svelte is 14–16KB, Solid is 18–23KB. For global audiences on slower connections, that's a meaningful difference.
Breaking Down the Resource Overhead
FPS numbers show visual smoothness; they don't show how hard the CPU is working. The profiler over a 10-second window fills that in:
| Variant | Main Thread (ms) | Scripting (ms) | Rendering (ms) | JS Heap (MB) |
|---|---|---|---|---|
| React | 8389 | 3596 | 5029 | 10.4-44.6 |
| React Memoized | 8345 | 1694 | 6464 | 11.1-33.0 |
| React + RTK | 8722 | 4359 | 4539 | 21.9-102.0 |
| React + RTK Memoized | 8666 | 4261 | 4630 | 11.5-45.9 |
| React + RTK + Inglorious Adapters | 8964 | 5466 | 3638 | 22.0-61.2 |
| React + RTK + Inglorious Adapters Memoized | 8974 | 4355 | 4448 | 11.8-50.2 |
| React + Inglorious Store | 8640 | 4209 | 4612 | 10.9-44.9 |
| React + Inglorious Store Memoized | 8504 | 2241 | 6103 | 11.5-45.8 |
| Vue | 8645 | 2231 | 6297 | 9.1-40.3 |
| Vue + Pinia | 8608 | 2226 | 6251 | 9.1-38.3 |
| Svelte | 8488 | 1974 | 6412 | 6.8-44.0 |
| Svelte + Store | 8440 | 2026 | 6289 | 6.8-43.1 |
| Svelte + Runes | 8557 | 2115 | 6312 | 8.3-47.3 |
| Solid | 7764 | 531 | 6457 | 6.5-9.5 |
| Solid Memoized | 8041 | 692 | 6637 | 6.6-10.8 |
| Solid + Store | 8174 | 1052 | 6498 | 8.8-21.9 |
| Solid + Store Memoized | 7870 | 747 | 6495 | 8.4-18.4 |
| Inglorious Web | 8564 | 1924 | 6489 | 5.2-43.7 |
| Inglorious Web Memoized | 7463 | 1680 | 4235 | 5.3-36.8 |
Why Solid Wins
Solid's scripting time is in a different league, and the keying fix barely moved it. Switching from <For> to <Index> — removing the value-based skip-unchanged shortcut, forcing Solid to revisit every bar every tick like everyone else — took scripting time from ~510–530ms to a still-tiny 531–1052ms. That rules out the keying-artifact hypothesis: even keyed identically to the rest, Solid spends a third to a fifth of Svelte's scripting time (~2,000ms).
It's not a bigger cache, it's no cache to check. lit-html's caching (what Inglorious Web relies on) re-runs your render function in full every tick, re-evaluating every expression, then skips the DOM write per part if the value's unchanged — it saves the write, not the recompute. Solid's compiler instead reads your JSX at build time and emits a direct binding per signal —
signal.subscribe(v => node.style.height = v)— with no template to walk at all. A signal change invokes exactly its one subscriber. That's why Solid's scripting time pulls away while its rendering time (the actual DOM/layout cost) stays in the same range as everyone else's — the gap is in the JS work before the write, not the write itself. The tradeoff: this requires a compile step over statically analyzable JSX; a runtime loop over arbitrary JS can't be unrolled the same way without one.
Could this gap be reduced without adding a compiler? Possibly — an opt-in runtime binding directive is something I'm exploring, but that's future work rather than something measured here.
Solid's memory footprint follows the same pattern. At 6.5–21.9MB, it uses roughly a fifth of Inglorious Web's peak (43.7MB) and Svelte's (49.8MB), because there's no intermediate "all the rows" representation sitting around to diff — updates touch exactly the affected nodes.
Where Inglorious Web Wins
Memoized selectors give Inglorious Web its own large win. Comparing the unmemoized baseline to Inglorious Web Memoized, Main Thread time drops over 1,100ms and rendering time falls from 6,489ms to 4,235ms, because the table and charts stop re-computing, sorting, and slicing raw data 100 times a second.
What This Means
Vue and Svelte are efficient in scripting (~2,000ms) but tied to DOM churn — without selector memoization for this workload, their rendering times stay pinned at 6,200–6,400ms, similar to Solid's own rendering time. That tells you raw DOM writes, not JS execution, are the shared bottleneck once scripting cost is out of the way. React pays a heavy rendering tax even with the compiler — scripting drops, but rendering time still skyrockets past 6,000ms, since virtual DOM reconciliation and layout passes are unaffected by cutting JS overhead.
Put plainly: for this workload, Solid's compiled, fine-grained model has a real structural advantage over React's virtual DOM, Vue/Svelte's uncompiled reactivity, and Inglorious Web's full-tree walk — and that holds up after specifically checking for and ruling out one likely confound (list-keying strategy).
Benchmark 2: The Deep Tree
What It Measures and Why It Matters
A tree of depth 8, branching factor 3, where one random leaf updates every 300ms. React, Vue, Svelte, and Solid all localize the update through their dependency graph — a proxy or signal knows exactly what changed. Inglorious Web walks the whole tree. This is the worst case for the full-tree model, chosen deliberately.
The Results
| Variant | FPS (dev) | FPS (prod) | Bundle (kB) |
|---|---|---|---|
| React | 120 | 120 | 62.16 |
| Vue | 120 | 120 | 25.84 |
| Svelte | 120 | 120 | 14.68 |
| Solid | 120 | 120 | 15.99 |
| Inglorious Web | 120 | 120 | 15.00 |
All five hit the monitor cap in both dev and production. FPS parity is complete.
Where the Difference Shows Up
| Variant | Main Thread (ms) | Scripting (ms) | Rendering (ms) | JS Heap (MB) |
|---|---|---|---|---|
| React | 251 | 37 | 124 | 23.9–24.5 |
| Vue | 196 | 38 | 89 | 28.8–29.6 |
| Svelte | 206 | 34 | 74 | 49.3–49.8 |
| Solid | 107 | 36 | 84 | 23.6–24.1 |
| Inglorious Web | 339 | 135 | 122 | 11.4–28.1 |
Inglorious Web uses 135ms of scripting time versus 34–38ms for the other four, over the full 10-second run — with no perceptible effect on FPS, which stays locked at 120 throughout.
Solid's number here is the interesting one, and it's not what Benchmark 1 predicts. At 36ms, Solid lands right next to React, Vue, and Svelte — not in a different league. Its dramatic scripting advantage in Benchmark 1 wasn't "signals are always this much faster" — it was specific to a workload where full-tree walking has real cost to avoid. Here, once an update is localized, there's almost nothing left to skip, so proxies, runes, and compiled signals all converge to roughly the same low cost — the compiler still buys Solid the lowest Main Thread total (107ms vs. 196–251ms), just not the same order-of-magnitude story.
Why full-tree specifically loses here comes down to scale. A depth-8, branching-3 tree has 3⁸ ≈ 6,561 leaf-level nodes; a dependency graph never visits the other 6,560 when one leaf changes, while render() walks from the root every time because there's no graph telling it what to skip. Bulk updates touch a meaningful fraction of the tree anyway, so walking isn't much extra work there — a single sparse update makes "walk everything" a ~6,561x larger task by comparison, which is the 135ms vs. ~35ms you see above.
When It Would Matter
Worth being precise about what this benchmark simulates: one update every 300ms, sustained continuously for 10 seconds — about 33 updates back to back with no pause. That's a deliberately adversarial, synthetic pattern, not how sparse updates arrive in a real app. A person expanding tree nodes does it in short bursts and stops; nothing about normal interaction sustains a continuous multi-times-per-second cadence for ten seconds straight. And even under that adversarial pattern, the per-update cost is small: 135ms over ~33 updates is about 4ms each — under a single 60fps frame budget, let alone 120fps.
So the honest conclusion isn't "full-tree rendering has a weak spot to watch out for." It's: this is a deliberately worst-case synthetic stress test, built to surface a cost, and even there the per-update cost is imperceptible. If something genuinely does sustain rapid, continuous, sparse updates — a live data feed driving a deep hierarchy, not occasional user clicks — the fine-grained frameworks' converging advantage (34–38ms vs. 135ms in total) is real and worth knowing about. For the far more common case of occasional, user-driven sparse updates, the difference won't be felt.
Put both benchmarks together: for bulk updates, full-tree rendering costs about the same scripting time as proxies and runes, and only compiled signals buy you meaningfully more. For sustained, rapid sparse updates, full-tree rendering costs more in aggregate — proxies, runes, and signals converge — though the per-update cost in both cases is small enough that it mostly matters for continuous, machine-driven update streams rather than everyday interaction.
Inglorious Web's case isn't that full-tree rendering is faster in either scenario. It's that the mental model stays simple regardless of workload shape: plain event handlers, no signal graph, no compiler step, and (see Testing, below) a simple story for writing and running tests. This fits real-time dashboards, financial tickers, IoT monitoring panels, and collaborative/multiplayer state sync particularly well — @inglorious/server uses the same event system over WebSockets, so adding real-time collaboration to an existing app is a middleware line, not an architectural change.
Testing: The Developer Performance Benchmark
Runtime benchmarks measure one dimension. Developer performance — how fast you can write, change, and verify code — matters at least as much over the life of a project.
React Hooks
import { renderHook, act } from "@testing-library/react";
const { result } = renderHook(() => useTaskList());
act(() => {
result.current.addTask('Write documentation');
});
expect(result.current.tasks).toHaveLength(1);
You need @testing-library/react, renderHook, and act() wrappers, and the React runtime. The ceremony is high enough that many teams skip unit-testing hooks entirely.
React + Redux
import { tasksReducer, taskAdded } from './taskSlice';
const state = tasksReducer([], taskAdded('Write documentation'));
expect(state).toHaveLength(1);
Redux reducers test in complete isolation — no store setup, no runtime. This is genuinely good, and it's the same instinct behind Inglorious Web's handler design. Where it gets harder is async: testing a thunk requires mocking dispatch, getState, and side effects, and the setup grows with complexity.
Inglorious Web
import { trigger } from "@inglorious/web/test";
const { entity } = trigger(
{ tasks: [] },
TaskList.taskAdd,
'Write documentation',
);
expect(entity.tasks).toHaveLength(1);
No runtime. No store setup. No action creators. Call the handler, assert the result — and because handlers can be async and notify further events, the same pattern extends to async: trigger returns a promise when the handler is async, and the events array captures any notifications fired during it.
Conclusions
| Aspect | React | Vue | Svelte | Solid | Inglorious Web |
|---|---|---|---|---|---|
| Dev FPS (bulk updates) | 32–112 (optimization required) | 116–117 | 102–112 | 119–120 | 105–118 |
| Prod FPS (bulk updates) | 92–120 (optimization required) | 117 | 118–119 | 118–120 | 120 |
| FPS (sparse deep tree) | 120 | 120 | 120 | 120 | 120 |
| Scripting overhead (bulk updates) | Medium–High | Low | Low | Lowest | Medium |
| Scripting overhead (sparse updates) | Low | Low | Low | Low (converges with others) | Higher in aggregate, still <1 frame per update |
| Memory footprint (bulk updates) | Medium–High | Medium | Medium | Lowest | Medium–High |
| Bundle size | 62–79KB | 27–29KB | 14–16KB | 18.3–23KB | 16KB |
| Optimization required | Not in prod (compiler); yes in dev | No | No | No | No |
| Testing complexity | High (hooks) / Low (reducers) / Hard (async thunks) | Medium | Medium | Low | Low |
Three tiers fall out of the data. Svelte, Vue, and Inglorious Web are all sufficiently fast for the large majority of applications — 116–120 FPS across bulk updates, 120 FPS on the sparse deep tree, none of the three ever close to a visibly choppy frame rate. Within this tier, the differentiator isn't speed; it's what else you get, and for Inglorious Web that's the testing story above. Solid is the right choice specifically for cascading dependency chains and resource-constrained devices — spreadsheet-shaped problems and constrained hardware, where its lowest-in-class scripting time and memory footprint are decisive rather than incidental. Full-tree rendering's cost shows up only under sustained, rapid, sparse updates, and mostly matters for continuous, machine-driven streams rather than everyday clicking; under bulk updates it costs about the same as proxies and runes, with only compiled signals pulling meaningfully ahead. For teams coming from React and Redux, the migration is incremental — Redux DevTools still work — and the testing story holds regardless of which tier your workload falls into.
Inglorious Web isn't trying to win the performance benchmark. It's trying to be fast enough that performance stops being the deciding factor, so the decision comes down to what you actually spend your time on day to day — reasoning about state, writing tests, and debugging — where a plain event-driven model offers a simpler testing and reasoning story than hooks, signal graphs, or reactive proxies.
Appendix: Further Notes
The following two points came up during review. They're worth recording, but they're a different kind of claim than the benchmarks above — one is a narrower, less rigorous comparison, and the other is architectural philosophy rather than a measured result. Both are flagged as such.
A note on chart rendering. An earlier draft of this post compared @inglorious/charts against Recharts and found Inglorious's charts hitting 115–120 FPS versus Recharts' 85–95 FPS. That comparison is weaker than Benchmark 1 or 2: Recharts and @inglorious/charts are chart libraries, not equivalent implementations of the same spec in different frameworks, so the gap could reflect library design as much as framework architecture. A fair version of this comparison — same chart spec, implemented natively across frameworks — is a big enough undertaking that it probably deserves its own follow-up post rather than a paragraph here.
A note on game engines and ECS. Inglorious Web's entity-centric model is drawn from game development, and the sparse-vs-bulk split in this post maps onto that origin: game engines don't reach for signals for their simulation core, because physics, animation, and AI tick nearly every active entity every frame — a bulk-update workload where almost nothing is stable frame-to-frame for a signal graph to skip. ECS's actual payoff there is cache locality and parallelism, not localized change tracking. Signals do show up inside games, just not in the simulation loop — HUDs and menus are sparse-update problems, structurally similar to Benchmark 2. This is a reasonable architectural parallel, not a benchmarked claim, and it's worth reading as color rather than evidence.
What Comes Next
Performance is one side of the picture. The other is server state — how you fetch data, handle loading and error states, and keep your UI in sync with the backend.
The next post looks at how the entity-centric model interacts with async data fetching, why handleAsync covers most of what TanStack Query provides, and when you'd still want a dedicated data-fetching library.
Top comments (0)