Dev

V8 doesn't run your JavaScript. It replaces it.

Most devs think V8 'runs' their JavaScript. It doesn't. It reads your code, throws most of it away, and rebuilds something completely different. Here's what's actually happening inside the engine that runs half the internet.

V8 doesn't run your JavaScript. It replaces it.

Your JavaScript doesn't run. Not really.

V8 reads it, decides it's too slow to actually execute, and replaces it with machine code it made up based on guesses about what you probably meant. And when its guesses are wrong, it throws all that work away and starts over.

This isn't a bug. It's the whole trick.

The pipeline nobody draws correctly

Here's the actual flow: your source hits Ignition first. That's V8's interpreter. It compiles your JS to bytecode, not machine code. Bytecode. Like the JVM kinda, but gnarlier.

Ignition isn't dumb either. It's watching everything. Every function call, every variable assignment, every time you pass a string where you usually pass a number. It's building a profile of how your code actually behaves at runtime.

Then TurboFan takes that profile and makes a bet.

It says: "this function has been called 10,000 times and x has always been an integer, so I'm gonna compile it assuming x is always an integer." That produces brutally fast machine code. No type checks. No fallbacks. Just raw execution.

Until you pass a float. Then the whole thing blows up and V8 deoptimizes. Back to bytecode. Back to square one.

The bug that cost us $15k in AWS bills

2021. My SaaS had about 8k users. I was running a Node 14 backend doing some gnarly data transformation on invoice objects. Performance was fine for months.

Then one customer uploaded invoices with a null in a field that was always a number. One. Customer.

Response times went from 40ms to 1.8 seconds overnight. Datadog was screaming. I spent two days thinking it was a database issue, a memory leak, a bad deploy. Nope.

V8 had compiled my transformation function assuming that field was always a number. The null triggered a deoptimization cascade. Every subsequent request hit bytecode instead of optimized machine code. The fix was one line: a null check before the loop.

$15k in compute bills over 6 weeks before I figured it out. Fun times.

What "hidden classes" actually mean for your code

V8 builds something called a hidden class for every object you create. Think of it as a secret schema it invents on the fly.

When you do const obj = { x: 1, y: 2 }, V8 creates a hidden class that says "this shape has x then y, both numbers." Fast property access. Tight memory layout.

When you do obj.z = 3 later, V8 has to create a new hidden class. Every object that was sharing the old class now diverges. That's a shape transition. Enough of them and your tight memory layout turns into a hash table lookup. Slow.

This is why every "write consistent object shapes" rule exists. Not because of some abstract best practice. Because V8 literally has a fast path for predictable objects and a slow path for everything else.

The stuff that actually matters for your app

Don't initialize object properties in random order. Don't add properties after construction. Keep your function arguments consistent in type.

You can check V8's decisions yourself. Run Node with --trace-deopt and watch what it complains about. Run --trace-opt to see what it's optimizing. It's noisy but it's real.

Big company advice says "don't micro-optimize." That works at Google where they have 40 engineers per service. For you, one bad deopt loop is $15k and two weeks of confusion.

Know your engine. It's guessing about your code. Make it easier to guess right.

OPEN IN REEDL_ FEED →← Back to feed