The Memory Cost of Reactivity

By Jared Leonard · Senior Engineer, AWS · June 28, 2026

A companion to Part 7: Series index

The last post was about how memory leaks. This one is about a question underneath it: do frameworks have meaningfully different default memory behavior, and how do vdom and fine-grained reactive frameworks actually compare?

Short answer: yes, on two different axes, and they trade against each other. The longer answer has a surprise in it. One of the "fine-grained" frameworks behaves like a vdom on the axis where it was supposed to win.

The bench

The same app in React, Solid, and Svelte: a paginated table over a million rows, served a page at a time by a mock POST API, with identical DOM output in all three. Each one is written the way you would actually write it, which matters more than it sounds and I will come back to it.

I built a first version and threw it away. It held a big array of records and measured the heap, and it "found" that Solid used the most memory. That was an artifact: React was holding plain objects while Solid wrapped each record in a reactive proxy, so I was comparing data structures, not frameworks. Worse, it measured retained memory, which is not even the axis the vdom debate is about. The debate is about churn, the garbage created on every update, and that first bench could not see churn at all.

How I measured it

I tracked two numbers, measured two different ways, and never blended them.

Retained memory is what the page is holding right now: JS heap plus DOM. I read it with measureUserAgentSpecificMemory(), the real cross-origin-isolated API, not the old performance.memory guess. The catch is timing: the number depends on when the collector last ran. So I launch Chrome with garbage collection exposed and force a collection before every reading. Now it is repeatable, and it is what the app actually retains, not whatever the collector had not gotten to yet.

Churn is the garbage an update creates and immediately discards. You cannot see that in a retained snapshot, because it is gone by the time you look. So I bracket it with GC. Force a collection, record the heap, run 200 updates that each mutate every one of 1,000 visible rows, force another collection, record the heap again. The memory that ballooned during the storm and then vanished on the final collection is the garbage those updates produced. Divide by the number of updates and you have churn per update.

The part that does the real work is "mutate every row, the way you would actually write it." That is not the same operation in each framework, and that is the point:

I am measuring the cost of each framework's idiom rather than a synthetic identical write. The idiom is what you ship.

Churn: where fine-grained is supposed to win

framework garbage per update
React 62 KB
Solid 5.6 KB
Svelte 62 KB
Garbage created per update of 1,000 rows (KB). Lower is less GC pressure.

Solid creates about a tenth of the garbage of the other two. That is the fine-grained payoff, and it is real: it touches the changed cells in place and allocates almost nothing. React allocates because the idiomatic update builds new objects for the diff, which is what you would expect.

The surprise is Svelte. It is also a fine-grained framework, so it should sit next to Solid, and it doesn't. Svelte 5's deep $state proxies churn about as much as React's vdom. "Fine-grained means low garbage" turns out to be a property of Solid's store, not of fine-grained reactivity in general, so the implementation matters more than the label here.

Retained: where it flips

framework retained at 1,000 rows
React 5.9 MB
Solid 10.5 MB
Svelte 12.9 MB
Retained memory after a forced GC, by scenario: empty app, 50 rows, 1,000 rows rendered.

Now the order inverts. React, the churniest one, holds the least. Solid and Svelte hold roughly twice as much. The same machinery that makes fine-grained frameworks cheap to update, a graph of small reactive nodes wired to each piece of the DOM, is the machinery they then have to keep in memory. A vdom is comparatively cheap to hold.

So the two axes pull opposite ways. The framework that churns least retains most, and vice versa.

A couple of other readings fill out the picture. Bundle size, gzipped: Solid 8.7 KB, Svelte 16 KB, React 47 KB. And the reassuring one: nobody leaks. I paged through the table a thousand times in each and retained memory grew by a fraction of a megabyte. The detached-node leaks from the last post are not a default-framework problem. You have to write those yourself.

JavaScript shipped, gzipped (KB).
Retained growth after paging 1,000 times (MB). Near zero is no leak.

So which one wins

Neither, and that is the honest answer. If your app updates constantly, churn and the GC pressure behind it is the enemy, and Solid's store is in a different league. If your app holds a lot on screen, retained memory is the enemy, and the vdom is quietly the lean one. Svelte 5 sits in the middle on its defaults, and you can move it with $state.raw and how you structure updates. What you are picking is which bill you would rather pay.

And keep the scale honest. These are single-digit megabytes and tens of kilobytes. In a real app the gigabyte is your data and the megabytes are your rendered rows, and the framework is the rounding error on top. That is the same lesson the last post kept landing on. Pick the reactivity model you like to work in, and spend your actual memory budget where it lives, which is the data you hold and the rows you put on screen.

The bench is small and reproducible, and I will put it where you can run it yourself, because you should not take a benchmark's word for anything, including mine. Especially the first version.


Bonus post: back to the series index →