V8 Internals: How JavaScript Runs Under the Hood
Understanding how the Google V8 engine executes JavaScript is crucial for writing highly performant code. By knowing the engine's compilation pipeline and optimization strategies, developers can avoid common pitfalls that lead to slow execution.
The V8 Engine Pipeline
When you feed JavaScript code to V8, it doesn't just run it line-by-line. It goes through a sophisticated pipeline:
- Parsing: The source code is parsed into an Abstract Syntax Tree (AST).
- Ignition (Interpreter): The AST is converted into unoptimized bytecode by the Ignition interpreter. This bytecode starts executing immediately.
- Profiling: While Ignition executes the bytecode, it collects profiling data (types of variables, frequency of function calls).
- TurboFan (Compiler): Hot functions (frequently executed code) and their profiling data are sent to the TurboFan optimizing compiler, which generates highly optimized machine code.
- Deoptimization: If the assumptions made by TurboFan turn out to be false (e.g., a function suddenly receives a different type of argument), V8 throws away the optimized machine code and falls back to Ignition (a process called deoptimization or bailing out).
Hidden Classes (Shapes)
JavaScript is a dynamic language; you can add or remove properties from objects on the fly. However, dictionary lookups for properties are slow. To optimize property access, V8 uses Hidden Classes (also known as Shapes or Maps).
Whenever you create an object, V8 assigns it a hidden class. If you add a new property, V8 transitions the object to a new hidden class that contains the memory offset for the new property.
function Point(x, y) {
this.x = x; // V8 creates hidden class C1
this.y = y; // V8 creates hidden class C2, transitioning from C1
}
const p1 = new Point(1, 2); // p1 has hidden class C2
const p2 = new Point(3, 4); // p2 has hidden class C2
// ❌ Anti-pattern: Adding properties on the fly breaks hidden class sharing
p2.z = 5; // p2 transitions to a new hidden class C3. p1 and p2 no longer share a shape!
Optimization Tip: Always initialize object properties in the same order, preferably in the constructor, so objects share the same hidden class.
Inline Caching (IC)
Inline caching is an optimization technique that relies on hidden classes. When V8 executes a function that accesses an object's property, it remembers the hidden class of the object and the memory offset of the property.
The next time the function is called with an object of the same hidden class, V8 skips the property lookup entirely and accesses the memory offset directly. If a function consistently receives objects of the same shape, it becomes monomorphic (highly optimized). If it receives 2-4 shapes, it's polymorphic (slightly slower). If it receives more than 4, it becomes megamorphic (unoptimized dictionary lookup).
Garbage Collection (Orinoco)
V8's garbage collector, historically known as Orinoco, manages memory automatically. It uses a Generational Garbage Collection approach, dividing the memory heap into two main regions:
- Young Generation (Nursery & Intermediate): Where new objects are allocated. This space is small and collected frequently using a Scavenger algorithm. Most objects die young.
- Old Generation: Objects that survive a few cycles in the Young Generation are moved here. This space is much larger and collected less frequently using a Mark-Sweep-Compact algorithm.
V8 performs garbage collection concurrently and in parallel with the main thread execution, minimizing "stop-the-world" pauses that cause visible frame drops (jank) in the browser.
Summary
To write V8-optimized JavaScript:
- Keep object shapes static (initialize properties in the same order).
- Avoid changing variable types (keep functions monomorphic).
- Be mindful of object creation in hot loops to reduce Garbage Collection pressure.