跪拜 Guibai
← Back to the summary

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:

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:

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:

// 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:


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:

Correct approach:

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:

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:


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:

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:

In production:


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:


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:

  1. Use trace-deopt to find "deopted functions"
  2. Use DevTools Memory to find "memory leaks"
  3. 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:

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~

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 😂