Academy
ProgrammingJS AdvancedPerformance & the JavaScript Engine

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_SMIPACKED_DOUBLEHOLEY) — once downgraded, never promoted back
  • delete turns objects into slow dictionary mode in hot paths — prefer obj.prop = undefined
  • arguments is less optimised than rest parameters
  • Measure firstperformance.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.