20 V8 Internals That Explain Why Your JavaScript Runs Slow
JS Code Tips vol.10 — 20 V8 Engine Principles Explaining "Why Writing It This Way Is Fast"
Xiao Bu's Code Tips Series, Issue 10 Theme: V8 Engine Principles This issue kicks off the "Sudden Enlightenment" series — after learning this, you'll think "How will V8 optimize this?" with every line of code you write 😎 20 tips bundled together, divided into five groups: Compilation Pipeline, Hidden Class, Memory, and Debugging.
Hello! I'm Xiao Bu, the "Bu" from Bu Jian Shuo~
As someone who has "crashed" countless times in the coding world, I've figured it out: Pitfalls, you either don't step into them, or you step into a big one 😂
Today's issue focuses on V8 Engine Principles — no fluff, all real mechanisms of V8's internals. Read it or not, it's up to you~ Anyway, crash logs don't cost anything.
Ps: There's a surprise at the end (not an ad!)
🚨 Crash Scenes (The Cost of Not Knowing V8)
The following are counter-examples written "bare" in real projects — the author has fled, V8 has wept
Scene 1: delete, the Performance Killer
// ❌ Looks fine after writing, but performance is 100x worse
function process(obj) {
delete obj.temp; // Triggers a hidden class transition every time
// ...
}
for (let i = 0; i < 10000; i++) process({ a: 1, b: 2, temp: 0 });
// V8: I can't optimize this thing either
Scene 2: arguments triggers deopt
// ❌ Modifying arguments directly deopts the function
function sum() {
let s = 0;
for (let i = 0; i < arguments.length; i++) {
s += arguments[i];
}
arguments[0] = 'x'; // This line = deopt
return s;
}
// V8: After deopt, I fall back to bytecode interpretation, 10x slower
Scene 3: for-in is really slow
// ❌ for-in feels great to write, V8 weeps looking at it
for (const key in obj) {
doSomething(key);
}
// V8's inner thought: This is for maps, you're using it to iterate over plain objects?
After seeing these 3, are you thinking "My code does this too" — After learning this issue, you can start fixing it.
I. Compilation Pipeline (3 tips)
1. V8 Compilation Pipeline: Four Steps
V8 goes through these 4 steps to run your code:
Source Code (JS)
↓ Parser
AST (Abstract Syntax Tree)
↓ Ignition Interpreter
Bytecode
↓ TurboFan Compiler
Optimized Machine Code
Where the pitfalls are:
- Cold functions run bytecode first (fast startup)
- Hot functions get compiled to machine code by TurboFan (fast execution)
- deopt triggers = fallback to bytecode (potentially 10x slower)
The essence of writing code is "letting V8 run faster after going through this pipeline".
2. Ignition: The Bytecode Interpreter
In one sentence: V8 doesn't "directly compile" your code; it first interprets it into bytecode.
// V8 Internals: Translating this code into bytecode
function add(a, b) { return a + b; }
// Bytecode:
// Ldar a // Load a
// Add b // Add b
// Return // Return
Advantages:
- Fast startup (no full compilation needed)
- Small memory footprint (bytecode is much smaller than machine code)
- Simple deopt (just fall back to bytecode)
Where the pitfalls are: V8 doesn't compile all functions — only "hot functions" (called multiple times) enter TurboFan. Write cold startup code with a bytecode mindset.
3. TurboFan: The Optimizing Compiler
In one sentence: Compiles "hot code" into highly optimized machine code.
What it does:
- Inlining: Embedding the code of called functions
- Escape Analysis: Determining if variables "escape" the function
- Type Specialization: Assuming types based on hidden classes
// Assume V8 sees add always being passed numbers
add(1, 2);
add(3, 4);
add(5, 6);
// TurboFan: I'll assume you always pass numbers, directly generate AddInt machine code
add('a', 'b'); // String! deopt warning ⚠️
Where the pitfalls are: V8 assumes type stability — once the type changes, deopt. Writing code with consistent types is helping V8.
II. Hidden Class / Inline Cache (5 tips)
4. Hidden Class: The "Shape" of an Object
V8 creates a "hidden class" for each object — describing what properties the object has and in what order.
const a = {}; // HiddenClass: C0
a.x = 1; // HiddenClass: C0 → C1 (x)
a.y = 2; // HiddenClass: C1 → C2 (x, y)
const b = { x: 1, y: 2 }; // HiddenClass: C2 (ultimately the same as a)
// V8 finds a and b have the same shape, inline cache reused
Where the pitfalls are:
// ❌ Two ways of writing, different Hidden Classes
const a = {};
a.x = 1; a.y = 2; // Shape C0 → C1 → C2
const b = {};
b.y = 2; b.x = 1; // Shape C0 → C2' → C3' (different from a)
// Same named fields, different order = different hidden classes
// V8 doesn't know they are the same shape, inline cache fails
Correct approach: Use object literals, keep property order consistent.
5. Inline Cache: The "Accelerator" for Property Access
When V8 sees an "object.property" access, it remembers the object's hidden class — next access directly looks up the table.
function getX(obj) { return obj.x; }
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
getX({ x: 5, y: 6 });
// V8: These three objects have the same shape, I'll remember "obj is C2 shape, x is at offset 0"
// Next getX call directly reads offset 0
Where the pitfalls are:
// ❌ Passing objects of different shapes mixed together
getX({ x: 1 });
getX({ x: 1, y: 2 });
getX({ x: 1, z: 3 }); // Shapes are all different
// V8: inline cache fails, must look up hidden class every time
6. delete, the Performance Killer
Mechanism: delete obj.x makes the object's shape become the "shape after deleting x" — V8 no longer reuses the previous hidden class.
// ❌ Slow
function process(obj) {
delete obj.temp;
// ...
}
for (let i = 0; i < 10000; i++) process({ a: 1, b: 2, temp: 0 });
// Every process call rebuilds the hidden class
Correct approach:
// ✅ Set to undefined (doesn't change shape)
function process(obj) {
obj.temp = undefined;
// Shape unchanged
}
// ✅ Or use Map to store temporary data
const temps = new Map();
function process(obj) {
temps.set(obj, undefined);
}
Where the pitfalls are: delete doesn't actually free memory (V8 won't reclaim it) — setting to undefined is better. In production code, use obj.x = null or undefined, don't use delete.
7. Inconsistent Order Triggers deopt
// ❌ Two orders, V8 considers them different shapes
const a = {}; a.x = 1; a.y = 2;
const b = {}; b.y = 2; b.x = 1;
// V8: a and b have different shapes, inline cache fails
Correct approach:
// ✅ Use literals, consistent order
const a = { x: 1, y: 2 };
const b = { x: 3, y: 4 };
// V8: Same shape, inline cache hits
How to use in business logic:
- JSON data returned by APIs already has a "unified shape" by V8, no problem
- When constructing objects yourself, use literals
- Field order in classes should be fixed
8. Class Field Initialization Order
// ❌ Random order, V8 gets confused
class Point {
constructor() {
this.y = 0;
this.x = 0;
}
}
// ✅ Consistent order
class Point {
constructor() {
this.x = 0;
this.y = 0;
}
}
// All Point instances have the same shape
Where the pitfalls are: Class field initialization order should be fixed — all instances have the same shape, inline cache reused.
III. Performance Killers (5 tips)
9. arguments Triggers deopt
// ❌ Using arguments + modifying = deopt
function sum() {
let s = 0;
for (let i = 0; i < arguments.length; i++) s += arguments[i];
arguments[0] = 'x'; // This line triggers deopt
return s;
}
Correct approach:
// ✅ Use rest parameters
function sum(...args) {
let s = 0;
for (const x of args) s += x;
return s;
}
// Rest parameters are real arrays, V8 can optimize
Where the pitfalls are: arguments is an array-like object, V8 has to deopt to bytecode to handle it. Always use rest parameters in modern code.
10. try/catch Performance Pitfall (Pre-V8 6)
Before V8 6: Code inside try/catch could not be optimized by TurboFan.
// ❌ Code inside try/catch cannot be optimized
function process(arr) {
try {
for (let i = 0; i < arr.length; i++) {
// ... 1000 lines of optimized code
}
} catch (e) {
console.error(e);
}
}
// V8: Everything inside try/catch runs as bytecode
V8 6+ Improvement: Functions inside try/catch can be optimized — as long as the function itself has try/catch, V8 compiles it separately.
Where the pitfalls are: Avoid wrapping super hot paths in try/catch. Use try/catch for exceptions, not for normal paths.
11. eval, the Performance Hell
// ❌ eval drives V8 crazy
function process(code) {
eval(code); // V8 has absolutely no way to predict what's inside
// ...
}
Why it's slow:
- Code inside eval is interpreted at the call site (not pre-compiled)
- V8 cannot optimize across eval boundaries
- Cold startup performance + security risk
Correct approach:
- Use
new Function(code)instead of eval (slightly better) - If you really need dynamic execution, use a sandbox library (like
vm2)
Where the pitfalls are: 99% of eval usages can be rewritten in other ways. eval appearing in business logic is usually a design problem.
12. with Statement (Deprecated)
// ❌ with makes V8 not know where the variable is
with (obj) {
x = 1; // Is this obj.x = 1 or global x = 1? V8 is also confused
}
Current status: Strict mode directly forbids it, all modern code should avoid it.
Where the pitfalls are: Use destructuring instead of with:
const { x, y } = obj; // Faster and more readable than with
13. The Truth About for-in Being Slow
// ❌ Using for-in on objects, V8 is slow
for (const key in obj) { /* ... */ }
Why it's slow:
- for-in uses the enumerator protocol (
Symbol.iterator) - Objects don't have a built-in iterator, V8 has to "simulate"
- Modern code uses
Object.keys()/Object.entries()
Correct approach:
for (const [key, val] of Object.entries(obj)) { /* ... */ }
for (const key of Object.keys(obj)) { /* ... */ }
Where the pitfalls are: for-in traverses properties on the prototype chain — potentially iterating over polluted properties on Object.prototype. Don't use for-in in production code.
IV. Arrays and Data Structures (4 tips)
14. Array SMI / PACKED / HOLEY Modes
V8 divides arrays into 3 modes:
| Mode | Description | Performance |
|---|---|---|
PACKED_SMI_ELEMENTS |
Packed small integers | Fastest |
PACKED_DOUBLE_ELEMENTS |
Packed floats | Fast |
PACKED_ELEMENTS |
Packed mixed | Slower |
HOLEY_SMI_ELEMENTS |
Sparse small integers | Slow |
HOLEY_DOUBLE_ELEMENTS |
Sparse floats | Slower |
HOLEY_ELEMENTS |
Sparse mixed | Slowest |
// ❌ Sparse array = HOLEY mode = slow
const arr = [1, 2, , 4, 5]; // arr[2] is a hole
// V8: Enters HOLEY mode, all operations become slower
// ❌ Different types = PACKED_ELEMENTS
const arr = [1, 'a', 2, 'b', 3]; // V8: Downgraded
// ✅ Packed + same type = optimal
const arr = [1, 2, 3, 4, 5];
Where the pitfalls are:
- Assign from front to back (don't do
arr[100] = 1) - Keep types consistent (if it's a number array, make it all numbers)
- Don't use
delete arr[0](becomes HOLEY)
15. Map vs Object for Large Data
// ❌ 1 million dynamic keys, Object is slow
const obj = {};
for (let i = 0; i < 1000000; i++) obj[`key${i}`] = i;
// V8: Creates a new hidden class every time
// ✅ Map is well-optimized
const map = new Map();
for (let i = 0; i < 1000000; i++) map.set(`key${i}`, i);
// V8: Map has dedicated internal storage, doesn't depend on hidden classes
Comparison:
| Scenario | Object | Map |
|---|---|---|
| Few static keys | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Many dynamic keys | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| Need to iterate | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Need size | ⭐ (manual) | ⭐⭐⭐⭐⭐ |
Where the pitfalls are: Use Object for few static keys, Map for many dynamic keys. This is determined by V8 optimizations.
16. String Concatenation Methods
// ❌ + concatenation (actually V8 has optimized it, the difference is small)
let s = '';
for (let i = 0; i < 1000; i++) s += i;
// V8: Will optimize into a rope structure, the difference is small
// ✅ Array + join
const parts = [];
for (let i = 0; i < 1000; i++) parts.push(i);
const s = parts.join('');
// Faster in some scenarios
// ✅ Template strings
let s = '';
for (let i = 0; i < 1000; i++) s = `${s}${i}`;
// Not recommended: Creates intermediate strings every time
V8 Current Status: V8 optimizes + very well (rope data structure), the measured difference is small. Just use +, don't overthink it.
17. The Real Cost of Closures
// ❌ Closure holds a large object
function createHandler() {
const bigData = new Array(1000000);
return function (e) {
console.log(bigData.length); // Closure doesn't release bigData
};
}
How V8 implements closures:
- V8 uses a Context chain to store closure variables
- Closure not released = Variables on the Context chain not released
- Large objects entering closures = Large objects not released
Correct approach:
// ✅ Put large objects in WeakMap
const bigData = new WeakMap();
function createHandler(obj) {
bigData.set(obj, new Array(1000000));
return function (e) {
console.log(bigData.get(obj).length);
};
}
// When obj is GC'd, the corresponding entry in bigData is automatically released
Where the pitfalls are: Don't create closures inside large loops. Use WeakMap to release closures.
V. Memory and Debugging (3 tips)
18. V8 Memory Model: New Space / Old Space
V8 divides the heap into 2 parts:
| Space | Size | Algorithm | Use |
|---|---|---|---|
| New Space | ~1-8 MB | Scavenge (fast) | Short-lived objects |
| Old Space | Larger | Mark-Sweep + Mark-Compact | Long-lived objects |
Condition for objects moving from New Space → Old Space: Survived one GC cycle.
Where the pitfalls are:
- New Space GC is fast but frequent (triggered every few MB)
- Old Space GC is slow but infrequent (Full GC pause time is long, Stop-The-World)
- Large objects go directly to Old Space (especially large arrays, strings)
In production:
- Monitor Old Space size
- Avoid one-time large arrays (> 100MB)
- Create fewer long-lived objects
19. Real Use Cases for WeakMap / WeakRef
// ❌ Strong reference = memory leak
const cache = new Map();
function process(obj) {
cache.set(obj, computeExpensive(obj));
return cache.get(obj);
}
let obj = { data: 'big' };
process(obj);
obj = null; // obj is still in cache, leak
Correct approach:
// ✅ WeakMap weak reference
const cache = new WeakMap();
function process(obj) {
if (!cache.has(obj)) cache.set(obj, computeExpensive(obj));
return cache.get(obj);
}
let obj = { data: 'big' };
process(obj);
obj = null; // obj in cache is automatically released 🎉
Where the pitfalls are:
- WeakMap cannot be iterated (no size/keys/values)
- WeakRef allows objects to be "flexibly referenced" — suitable for caching, metadata association
- FinalizationRegistry listens for GC — rarely used, just know it exists
20. V8 Performance Debugging Tools
# 1. node --trace-opt sum.js
# Outputs which functions V8 optimized
# 2. node --trace-deopt sum.js
# Outputs which functions were deopted (performance killer list)
# 3. Chrome DevTools → Performance
# View function execution time, Call Stack
# 4. Chrome DevTools → Memory
# Heap snapshot to find memory leaks
# 5. node --prof
# Generates v8.log, process with --prof-process
node --prof-process isolate-*.log > processed.txt
Practical routine:
- Use
trace-deoptto find "deopted functions" - Use DevTools Memory to find "memory leaks"
- Use Performance panel to find "long tasks"
Where the pitfalls are: Don't blindly optimize — V8 is smarter than you think. Profile first, find the bottleneck, then optimize.
📦 Wrapping Up
Condensing these 20 V8 tips:
- Most frequent pitfalls:
deletetriggers deopt,argumentstriggers deopt, no optimization inside try/catch (pre-V8 6) - Most worth bookmarking: Hidden Class + Inline Cache mechanism, Map vs Object selection, WeakMap releasing closures
- Core idea: The essence of writing code is "letting V8 run faster after going through the optimization pipeline" — stable types, consistent shapes, avoid deopt
If you learned it, you earned it. Hesitating and wavering means you came for nothing~
Written at the End
Want some tips? Drop a topic in the comments~
- A pitfall you just stepped into
- Code you write repeatedly in your project
- An API you want to figure out but have been too lazy to look up
Xiao Bu might see it… might not reply 😂 After all, there are too many crashes in the code, can't spare a hand~
Ps: Likes, bookmarks, and follows are optional, those who rush for updates will be hit 😂