Skip to Content
Course content

6: How JavaScript Engines Work: V8 Overview

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, we've treated JavaScript as a magic box: you write code, you hit refresh, and it just works. But if you want to write high-performance code—the kind that doesn't stutter when processing thousands of data points—you have to stop thinking of JavaScript as a purely interpreted language. Many developers still hold onto the naive view that the engine just reads your code line-by-line and executes it on the fly. If that were true, JS would be painfully slow.

The slow path: treating objects like dictionaries

To understand why V8 is brilliant, you first have to understand the "wrong" way to handle objects. In a purely dynamic language, an object is basically a hash map. If you have a point object with x and y properties, the engine would theoretically have to do a dictionary lookup every single time you access point.x. It has to search the memory, find the key "x", and then grab the value. Doing this once is fine. Doing this inside a loop of ten thousand iterations is a performance nightmare.

// The "naive" mental model:
const point = { x: 10, y: 20 };
// Engine thinks: "Let me look through the map of 'point' to see if 'x' exists... okay, it's here."

If we stayed on this path, your apps would feel sluggish because the CPU would spend more time searching for where properties are stored than actually doing any math.

The fast path: hidden classes and inline caching

V8 doesn't actually treat your objects as maps. Instead, it uses something called "Hidden Classes" (or Shapes). The first time V8 sees your point object, it creates a hidden class in the background. This class says: "Property 'x' is always at offset 0, and property 'y' is always at offset 1." Now, the engine doesn't have to search a dictionary anymore; it just jumps straight to the memory offset. This is essentially how C++ or Java handles objects, which is why JS can feel almost as fast as them in optimized loops.

This is where the "gotcha" happens. V8 is essentially gambling that your objects will always have the same shape. As long as you initialize your objects in the same order, V8 uses "Inline Caching" to remember exactly where those properties are. It stops asking "Where is x?" and starts assuming "x is always right here."

When the optimization breaks

Here is where I see a lot of senior devs still trip up: they treat object initialization as an afterthought. If you change the order in which you assign properties, you break the hidden class. You've effectively told V8, "Actually, this is a completely different kind of object," and the engine has to throw away its optimized machine code and fall back to the slow, dictionary-style lookup.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

const p1 = new Point(1, 2); // Shape A: {x, y}
const p2 = new Point(3, 4); // Shape A: {x, y} - Fast!

const p3 = {}; 
p3.y = 5; // Shape B: {y}
p3.x = 6; // Shape C: {y, x} - Slow!

In the example above, p1 and p2 share the same hidden class. V8 is happy. But p3 is a disaster. By adding y before x, you've created a new shape. If you pass a mix of these objects into a function, V8's JIT (Just-In-Time) compiler gets confused. It sees that the "shape" is constantly changing (this is called being "polymorphic"), gives up on the optimization, and drops back to the slow path. I've seen this cause massive performance regressions in data-heavy loops simply because some objects were initialized differently than others.




📋 Practical Task

Benchmarking Object Shape Performance

In this exercise, you will create a script to prove how property initialization order affects performance in V8. You'll build a function that processes a large array of objects and compare two different scenarios.

Requirements:

  • Create a function sumX(points) that iterates through an array and sums the x property of every object.
  • Generate two arrays of 100,000 objects each:
    • Array A (Monomorphic): Every object is created with {x: 0, y: 0}.
    • Array B (Polymorphic): Half the objects are {x: 0, y: 0} and half are {y: 0, x: 0}.
  • Use console.time() and console.timeEnd() to measure how long sumX takes to run for each array.
  • Observe the difference in the console. Even though the objects have the exact same properties, the difference in order should result in a measurable timing difference.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.