Frontend has exactly one hard problem: keeping the DOM consistent with state. Every framework era is a different answer to who does that bookkeeping. I wanted to see the eras side by side, so I built Tiny Kanban seven times: same markup, same CSS, same behavior contract enforced by a browser-driven conformance suite, different synchronization strategy each time.
- Vanilla JS, naive full re-render
- Vanilla JS, keyed DOM reuse
- jQuery, render loop
- jQuery, hand-targeted incremental patches
- React 19
- Svelte 5 with runes
- SolidJS 1.9
Most public framework comparisons are unfalsifiable because nobody holds the app constant. Here the seven boards are behaviorally identical down to the acceptance checklist, so differences in the numbers are the strategies themselves, not app-design noise. Everything below comes from the repo's bench suite: production builds only, a vendored jQuery, headless Chromium, one machine.
Count the writes, not the vibes
The most useful instrument in the whole suite is also the cheapest: a MutationObserver that counts every DOM write each strategy issues per user action. It is deterministic - the suite runs every operation twice and the ledgers match byte for byte - so there is no benchmark noise to argue about. This is what each era actually touches on a 300-card board:

Committing a single edited card title costs Svelte and Solid 3 writes, jQuery's hand-patched version 11, React 14, and the rebuild-everything versions 6,067. That is a 2,000x amplification factor for the same user intent:

The row that taught me the most is vanilla B, the "keyed patch" version - the one that reuses DOM nodes to preserve editing focus, the way you hand-rolled identity before frameworks existed. It issues more writes than the naive version (8,904 per commit), because reusing a node is not the same as diffing it: it re-stamps every attribute with the same value on every render. Worse, its first draft had a genuine data-loss bug I only found in a real browser: replaceChildren detaches even the nodes it is about to re-insert, Chromium fires focusout on the detach, and the blur handler committed your half-typed title on every keystroke. Identity without diffing is the worst of both worlds, and the gap between them is precisely the part nobody should hand-write.
Frameworks sell floors, not ceilings
Here is the uncomfortable result. The fastest structural updates in the entire suite belong to jQuery B, the hand-patched version - and it is the only implementation whose latency stays flat as the board grows:

Adding a card among 10,000 takes jQuery B 27ms. Solid takes 121ms, React 288ms, and the rebuild versions 366-1,182ms. Framework list reconciliation is O(N) too - it just has a far smaller constant than rebuilding. The one place fine-grained reactivity is genuinely flat is the draft-keystroke panel: typing in the edit field holds at about 1ms at every board size for five of the seven implementations, while React climbs to 112ms (a re-render per keystroke is the model working as designed) and vanilla B to 485ms.
So if hand-patching wins the benchmarks, why did the industry abandon it? Because the same jQuery B file is a museum of manually maintained invariants: re-sync the column metadata on move, re-check the empty state on delete, escape the selector, commit the open edit before starting the next one. Every operation is fast because a person did the diffing at development time, and every future change re-runs that person. What React, Svelte, and Solid actually sell is not speed - jQuery B proves the speed was always available - but a correctness floor: focus, identity, and state coherence survive the change you make six months from now. Frameworks are insurance premiums, and the premium is visible in the numbers below.
Where the CPU actually goes
A fixed 110-action editing session, identical on every implementation, with CPU time split by what the browser was doing:

The rebuild versions pay in layout - vanilla A spends 1.9 seconds in layout against 0.25 for the three frameworks - while the frameworks pay in script. That split settles an old argument: "the DOM is slow" was always the wrong diagnosis. DOM API calls were never the cost; recomputing layout for thousands of nodes you did not change is. The virtual DOM was not invented because the DOM was slow. It was invented so that the correctness-friendly model - UI as a pure function of state - stayed affordable.
Bytes are time, but waterfalls beat bytes
On an emulated Fast-3G connection with everything gzipped, the byte column does not predict the ranking:

The jQuery versions ship 33 kB and spend 501ms transferring it; React ships 62 kB and spends 464ms. The difference is shape, not size: jQuery is a blocking library fetch followed by a module waterfall, each hop paying the 150ms round trip, while React is one bundle. Request topology beats raw bytes at mobile latencies. Solid ships 9.7 kB in one bundle and boots the same board in 462ms total.
The bundle-composition split (via sourcemap attribution) puts the payload story plainly: the application is ~4 kB everywhere; React wraps it in 185 kB of framework, Svelte in 35 kB, Solid in 17 kB. You ship the framework, or you ship the discipline it replaces:

Author cost vs user cost. All seven implementations are 188-315 lines; what varies is what rides along.
Memory is the tax on the other side of the ledger. Fine-grained reactivity keeps a signal graph per row: Solid retains 4.4 kB and Svelte 4.2 kB per card against ~140 bytes for the imperative versions, Svelte adds 4,000 anchor comment nodes to an otherwise identical DOM, and React's synthetic event system registers 4,159 listeners where the delegated implementations use about 20.
What each one felt like to build
| Implementation | The one-line verdict |
|---|---|
| Vanilla, naive | Honest and legible; its real costs (a swallowed click after blur-commit, wiped form text) are invisible until you probe them in a browser. |
| Vanilla, keyed | The instructive failure: identity bookkeeping without diffing is slower, noisier, and it still ate my keystrokes. |
| jQuery, rebuild | Pleasant APIs over the wrong architecture; same bugs as naive vanilla, three times the node-construction cost. |
| jQuery, patched | Best numbers in the suite, worst maintenance story; every invariant is a promise a future editor has to keep. |
| React 19 | The most rules to hold (reducer purity, key discipline), the most institutional support, zero tooling surprises. |
| Svelte 5 | The nicest final code - after I threw away my first version, which the compiler happily accepted as React-in-runes with none of the benefit. |
| SolidJS | The steepest single idea (components run once), then almost no ceremony; the friction lives in tooling edges, not app code. |
Two of those deserve a sentence more. My first Svelte version reassigned immutable state through $bindable prop chains - architecturally React - and it compiled with zero warnings while silently discarding the fine-grained reactivity that is Svelte's entire pitch. Paradigm errors are not type errors; syntax transfers between frameworks instantly, architecture does not, and nothing in the toolchain tells you which one you wrote. And React's adversarial review finding was the same lesson inverted: crypto.randomUUID() inside a reducer looked idiomatic and violated purity in a way only StrictMode's deliberate double-invocation exposes.
What I actually take away
The durable skill is state modeling. One line in the spec - editing state is explicit app state, never something inferred from the DOM - did more for correctness across all seven implementations than any framework feature. Where truth lives, what is durable versus transient, what is derived versus stored: those decisions transferred unchanged across five paradigms and will outlive all of them.
Measure, because folklore does not survive instrumentation. My own claims kept dying on contact with data: "targeted patching stays flat" (framework reconciliation is O(N) too), "bytes dominate cold start" (the 33 kB waterfall lost to the 62 kB bundle), "reset should favor frameworks" (naive rebuild won; reconcilers pay for teardown). A 30-line MutationObserver ledger settled arguments that a decade of blog posts could not, and it runs deterministically in CI.
The complexity moved to the meta-layer. All seven apps are a few hundred lines and roughly equally pleasant. The genuine friction of this project was between npm and the bundler: a Vite plugin claiming the global JSX transform, a resolve alias bypassing package export conditions and silently loading a server build, lint rules that assume every JSX file is React. The ecosystem's tooling quietly presumes React, and every other choice pays a small permanent toll.
The eras converge. Svelte 5's runes and Solid's stores produce nearly identical application code - module state, direct mutation, one persistence effect - and React is compiling its way toward the same destination. The ledger explains why: 6,067 writes, then 14, then 3, for the same click. There was only ever one problem.