跪拜 Guibai
← All articles
Frontend · JavaScript · Interview

20 V8 Internals That Explain Why Your JavaScript Runs Slow

By 不简说 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

JavaScript performance bottlenecks rarely show up in synthetic benchmarks. They surface when object shapes diverge across thousands of iterations, when a single `delete` or `arguments` reference deopts a hot function, or when a closure pins a large array in memory. Knowing these V8 internals turns guesswork optimization into a checklist of patterns to avoid.

Summary

V8's compilation pipeline moves code from parser to AST, through Ignition's bytecode interpreter, and into TurboFan's optimized machine code only when functions run hot. That pipeline breaks whenever code changes object shapes, mixes types, or uses legacy patterns like `delete`, `arguments`, `for-in`, and `eval`. Each break triggers a deopt that drops execution back to bytecode, often 10x slower.

Hidden classes are the engine's way of tracking object property layouts. Two objects with the same properties added in different orders get different hidden classes, which defeats inline caching and forces repeated lookups. The same mechanism makes `delete` a performance killer: removing a property creates a new hidden class transition instead of freeing memory, so setting to `undefined` or `null` keeps the shape stable.

Arrays have their own optimization tiers. Packed, single-type arrays stay in the fastest SMI or DOUBLE modes; sparse arrays or mixed-type arrays fall into HOLEY modes that slow every operation. For large dynamic key-value stores, Map outperforms Object because it avoids hidden class churn entirely. WeakMap and WeakRef provide garbage-collector-friendly alternatives to strong-reference caches that would otherwise leak memory.

Takeaways
V8 runs cold functions as bytecode via Ignition and only compiles hot functions to machine code through TurboFan; a deopt drops execution back to bytecode, often 10x slower.
Hidden classes track object property layout; adding properties in different orders or using `delete` creates divergent shapes that break inline caching.
Setting a property to `undefined` or `null` keeps the hidden class stable, whereas `delete` triggers a shape transition without freeing memory.
Rest parameters (`...args`) are real arrays that V8 can optimize; the `arguments` object is array-like and modifying it forces a deopt.
`for-in` traverses the prototype chain and uses an enumerator protocol that V8 must simulate for plain objects; `Object.keys()` and `Object.entries()` avoid this overhead.
Arrays perform best when packed and single-type; sparse arrays or mixed-type arrays drop into HOLEY modes that slow every subsequent operation.
Map outperforms Object for large sets of dynamic keys because it bypasses hidden class transitions entirely.
Closures that capture large objects prevent garbage collection; WeakMap and WeakRef let the engine reclaim memory when the key object is no longer referenced.
`eval` prevents V8 from optimizing across its boundary and forces interpretation at the call site; nearly every `eval` usage signals a design problem.
V8's `--trace-deopt` and `--trace-opt` flags, plus Chrome DevTools Memory and Performance panels, surface exactly which functions deopt and where memory leaks live.
Conclusions

The performance advice around `try/catch` blocking TurboFan optimization is largely obsolete since V8 6, yet it persists in developer folklore. The real takeaway is narrower: avoid wrapping the hottest loop in a try/catch, but don't restructure error handling around a constraint that no longer exists.

String concatenation with `+` is a solved problem in modern V8 thanks to the rope data structure. The old advice to use `Array.join()` for performance is now a micro-optimization that rarely matters, yet it still circulates as a best practice.

The hidden class model explains why TypeScript and typed property declarations improve runtime performance even though types are erased: consistent field initialization order in classes produces uniform hidden classes across all instances, maximizing inline cache hits.

V8's array mode transitions are one-way. Once an array becomes HOLEY or PACKED_ELEMENTS, it never upgrades back to a faster mode, so a single sparse assignment or type mix permanently degrades performance for that array.

Concepts & terms
Hidden Class
V8's internal representation of an object's property layout. Objects with the same properties added in the same order share a hidden class, enabling fast property access through inline caching. Different property orders or `delete` operations create new hidden classes and break the optimization.
Inline Cache
A V8 optimization that remembers the hidden class and memory offset of a property access. When the same property is accessed on objects with the same hidden class, V8 skips the lookup and reads directly from the cached offset.
Deoptimization (deopt)
When TurboFan's assumptions about types or object shapes are violated, V8 discards the optimized machine code and falls back to the Ignition bytecode interpreter. Common triggers include type changes, `delete`, modifying `arguments`, and passing objects with different hidden classes to the same function.
SMI / PACKED / HOLEY Array Modes
V8 categorizes arrays into performance tiers. PACKED_SMI_ELEMENTS (packed small integers) is fastest; HOLEY modes (sparse arrays with gaps) are slowest. Transitions between modes are one-way: an array never upgrades back to a faster mode once downgraded.
Rope String
V8's internal data structure for efficient string concatenation. Instead of copying strings on every `+` operation, V8 builds a tree of string fragments and flattens it only when needed, making `+` concatenation competitive with `Array.join()` in modern engines.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗