Performance & the JavaScript Engine
JavaScript Is Not Interpreted
The mental model most developers carry — "JavaScript is a slow, interpreted scripting language" — has been wrong for fifteen years. Modern engines like V8 (Chrome, Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) compile your code to native machine code at runtime. This is Just-In-Time (JIT) compilation.
The pipeline in V8 looks roughly like this:
Your JS source
↓ parse
Abstract Syntax Tree (AST)
↓ Ignition (bytecode compiler)
Bytecode — runs immediately, collects profiling data
↓ TurboFan (optimising JIT, triggered on "hot" code)
Optimised native machine code — runs at near-C++ speed
↓ (if assumptions break)
Deoptimisation — back to bytecode
The engine makes assumptions about your code to optimise it. If those assumptions are violated — wrong type, unexpected shape — the engine throws away the compiled code and starts over. This is called deoptimisation and it is the most common source of unexpected slowness.
Hidden Classes — The Shape of an Object
V8 does not treat objects as hash maps. It assigns each object a hidden class (also called a "shape" or "map") that describes its property layout. Objects with the same hidden class share compiled code. Changing the shape of an object forces V8 to create a new hidden class.
This is why initialising all properties in the constructor matters — not just for readability, but for performance. Every class instance created by the same constructor will share one hidden class.
The Cost of Dynamic Property Addition
Run this a few times. The shaped literal and class constructor will consistently beat the dynamic version — sometimes dramatically. The gap widens with more properties and more iterations.
Monomorphic vs Polymorphic Call Sites
An inline cache is V8's mechanism for remembering what type it saw at a particular operation. If a property access always sees the same hidden class, the cache is monomorphic and V8 compiles it to a direct memory offset — essentially a C struct access. If it sees multiple shapes, the cache becomes polymorphic and the engine must do more work each time.
The rule: the fewer distinct shapes a function sees at any call site, the faster it runs. This is why mixing objects of different shapes in the same array — even visually similar ones — can silently hurt performance.
Type Consistency — Don't Change Types Mid-Flight
V8 specialises compiled code for the types it has observed. If a variable changes type, the specialised code is thrown away:
A single type change — even happening once every ten thousand iterations — can prevent V8 from ever specialising that function. The engine cannot trust its assumption, so it falls back to generic code for every call.
Arrays: Stay Monomorphic
Arrays in V8 have element kinds — a classification of what type of values they contain. A dense integer array (PACKED_SMI_ELEMENTS) is the fastest possible. As soon as you mix in a float, a hole, or a non-number, the array is reclassified and never promoted back up:
The Cost of arguments and try/catch
Some patterns prevent TurboFan from optimising a function at all. Historically, arguments, try/catch inside hot loops, and eval were full deoptimisation triggers. Modern V8 has improved but these patterns still carry measurable cost:
Rest parameters (...args) are the modern replacement for arguments. They are a real array, they are better optimised, and they compose cleanly with destructuring and spread.
Practical Rules
Everything above distils into a short list of habits:
Measuring: The Only Truth
Rules and intuitions are starting points. The only way to know what is actually slow in your code is to measure it. In a browser, use performance.now() for microsecond precision. In Node.js, use process.hrtime.bigint(). In production, use a profiler (Chrome DevTools Performance tab, --prof flag in Node).
Always warm up before measuring — V8 needs several hundred calls before TurboFan kicks in. Cold measurements tell you about bytecode performance, not JIT performance, which is rarely what you care about.
What You Have Learned
Writing fast JavaScript is not about avoiding the language — it is about writing code that lets the engine do its job.
The key ideas:
- V8 compiles hot code to native machine code via TurboFan — this is why JS can be fast
- Hidden classes describe object shape; keeping shapes consistent lets V8 share compiled code across all objects of that shape
- Inline caches remember the type seen at each operation; monomorphic (one type) = fast, polymorphic (many types) = slower
- Changing variable types mid-function forces deoptimisation — the compiled code is discarded
- Arrays have element kinds (
PACKED_SMI→PACKED_DOUBLE→HOLEY) — once downgraded, never promoted back deleteturns objects into slow dictionary mode in hot paths — preferobj.prop = undefinedargumentsis less optimised than rest parameters- Measure first —
performance.now()with warmup; intuitions without measurement are guesses
In the next lesson: Memory Management & Garbage Collection — how V8 decides what to keep and what to collect, what causes memory leaks in long-running applications, and how to find and fix them.