Immutability vs. Mutability in JavaScript: Performance Trade-offs and the Role of JIT
Originally published April 2023, in a series with Parts 1 and 2 of the perf gotchas posts. Modern V8 / Maglev / Turbofan have made the original framing a bit misleading — what's below is updated for what actually matters in 2026. Same headline rule: measure first.
Two different things called "immutability"
Conflating these is the first source of bad advice:
- Binding immutability (
const x = ...): the variable can't be reassigned. Says nothing about whether the object it points to can change. - Structural immutability (frozen objects, persistent data structure libraries): the object itself can't change.
The JIT only cares about the shape of the object, not which kind of immutability you used to get there.
What actually helps the JIT
Modern V8 optimizes hot code via hidden classes and inline caches: it learns the shape of the objects passing through a call site and generates fast paths for them. The lever you have is shape stability, not immutability:
- Initialize all properties at construction, in the same order.
- Don't add or delete properties later.
- Don't store mixed types at the same property (sometimes a number, sometimes a string).
- Keep call sites monomorphic — feeding the same function ten different object shapes turns it megamorphic and the fast path is gone.
You can do all of this with mutable objects. Mutating obj.x = obj.x + 1 is as fast as anything; mutating obj.newProperty = … after construction is the part that hurts.
Where the immutability tax actually shows up
The dominant cost model for "spread everything" patterns in 2026 isn't the JIT — it's GC pressure. Each { ...state, foo: 1 } allocates a new object that lives long enough to be referenced and then dies. Hot reducers, deep update chains, and useState setters that fire on every input keystroke fill the young generation and trigger Scavenger pauses.
Two practical mitigations:
- For deeply nested updates, Immer uses a proxy + structural sharing so only the changed paths get new objects. Cheaper than hand-rolled spread chains, and easier to read. The older Immutable.js library does the same with persistent data structures, but is largely unmaintained — Immer is the modern default.
- React's
useMemo/React.memo/useCallbackwork on referential equality, not deep immutability. Returning a new object each render defeats them; returning the same reference (or a structurally-shared one via Immer) lets them skip work.
What to ignore
Object.freezefor performance. It's a correctness/intent tool. Historically V8 has had pessimisations for frozen objects; even today it transitions the hidden class and isn't a free win. Freeze when you want the runtime check, not for speed.- "Immutable objects are inherently thread-safe." JavaScript Workers don't share the heap; values are deep-cloned across
postMessage(orSharedArrayBufferfor explicit sharing). Whether the original was frozen doesn't enter into it. - "
constenables constant folding / DCE." It doesn't. SSA-level constants and dead code already fall out of the optimizer's own analysis; user-levelconstadds nothing. - Records & Tuples. Still TC39 Stage 2 as of early 2026 — not in the language yet, despite years of "coming soon" posts. Worth tracking but not worth designing around.
Rule of thumb
Pick mutability or immutability based on what makes the code easier to reason about and test. Reach for Immer where you need referential equality for memoization. Profile before assuming Object.freeze or "go all-immutable" buys you anything in the JIT.