V8, JIT Compilation, and How Node Actually Runs Your JavaScript

Your JavaScript is not the real program. The real program is Node.js, and V8 is how it turns your code into something a CPU can actually run.

June 4, 20267 min read2 / 2

I used to wonder why Node.js needed to be "installed." Python too. Java too. Why can't I just run the script directly? A compiled C program is one file -- you run it. Why does JavaScript need this extra thing?

The answer explains everything about how V8 works and why it makes the choices it does.

Your Script Is Just Input

CPUs only understand machine code: binary instructions specific to their architecture. An Intel chip and an ARM chip have completely different instruction sets. Code compiled for one will not run on the other.

Compiled languages like C commit to a target at compile time. You compile for Intel x86. The resulting binary runs on Intel. It will not run on ARM, and it will not run on a different OS even if the CPU is the same -- the executable format is different (ELF on Linux, PE on Windows, Mach-O on Mac).

This is the portability problem.

Interpreted languages solved it with a trick: the runtime.

Node.js is a compiled C++ program. Python is a compiled C program. Someone at the Node.js team compiled Node for Intel Linux, ARM Linux, Intel Mac, M-series Mac, Windows, and so on. Those binaries are what you download from nodejs.org.

Your JavaScript file is not a program. It is an input to node -- the same way a text file is an input to cat. As far as the OS is concerned, when you run node app.js, the OS is executing Node.js. Node.js happens to read your file and figure out what to do with it.

Plain text
OS executes: node (compiled binary) node reads: app.js (your "program", just a string) V8 interprets/compiles: your JavaScript CPU runs: machine code

Write once, run everywhere is not magic. It is just someone else doing the compilation work for every platform so you do not have to.

Why Interpreted Languages Are Slower (And How V8 Fights Back)

The portability trick has a cost. Interpretation means two steps for every line of code: read what it says, then execute what it means. A compiled program skips the first step -- the CPU just runs the instructions directly.

V8 solves this with bytecode and JIT compilation.

Step 1: Bytecode

V8 does not interpret your raw JavaScript source. It first compiles it to a more compact intermediate format called bytecode. Bytecode is smaller and faster to process than text. The string const x = a + b becomes a short, efficient sequence of bytecodes that says "add these two values and store the result."

Bytecode is still not machine code -- V8 still needs to interpret it. But interpretation of compact bytecode is significantly faster than parsing JavaScript source.

Step 2: JIT Compilation

Here is the key insight: not all code runs equally often.

If a function is called once, it is not worth compiling it to machine code. The overhead of compilation would cost more than the savings. Interpret it and move on.

If a function is called thousands of times in a tight loop, the interpretation overhead compounds. Every call pays the "two steps" tax. This is the worst case for interpreted languages.

V8 identifies these "hot" functions and compiles them -- at runtime, just in time -- to actual native machine code for the CPU it is currently running on. That compiled version is stored in the heap. When the function is called again, V8 detects that it has a compiled version and jumps directly to the native code. No interpretation, no bytecode walking. Just raw CPU execution.

V8 pipeline: JavaScript source to bytecode to JIT-compiled machine code stored in the heap ExpandV8 pipeline: JavaScript source to bytecode to JIT-compiled machine code stored in the heap

This is why V8 benchmarks can compete with compiled languages on tight numeric loops. The hot paths are no longer interpreted -- they are running as native code.

How V8 Stores Your Data

Understanding memory layout matters for Node.js performance. V8 separates memory into two regions.

The Stack

Simple primitive values -- numbers, booleans, null, undefined -- live on the stack as part of the function call frame. In JavaScript, all numbers are stored as 64-bit doubles (even integers). const x = 5 puts 8 bytes on the stack.

Stack memory is cheap: allocation is just moving a pointer, and deallocation happens automatically when the function returns.

The Heap

Objects and arrays live on the heap. A heap allocation is more expensive -- V8 needs to find an available region, track the allocation, and eventually garbage collect it.

Every object in V8 is actually two arrays:

  • Properties -- named key-value pairs (obj.name, obj.age)
  • Elements -- integer-indexed values (arr[0], arr[2])
JavaScript
const person = { name: 'Denver', age: 28 }; // two properties, zero elements const scores = [90, 85, 92]; // zero properties, three elements const mixed = { 0: 'a', key: 'b' }; // one element, one property

When you access person.name, V8 cannot just index directly into the properties array -- it does not know which position name is in. So V8 maintains a hidden structure called a hidden class that maps property names to their index positions. Accessing person.name means: look up "name" in the hidden class, find it is at index 0, retrieve properties[0].

This lookup has a cost. The first time person.name is accessed, V8 walks the hidden class. But V8 caches the result -- this is called inline caching. Subsequent accesses use the cached offset directly, turning the lookup into an O(1) jump.

The practical implication: avoid changing the shape of objects after creation. If you define an object with properties {a, b} and then add property c later, V8 has to create a new hidden class. The inline cache for the old shape is invalidated. Calling code that expected the old shape now pays the lookup cost again.

Garbage Collection and Why It Pauses Your Server

V8 manages memory automatically using mark and sweep:

  1. Mark -- trace all object references starting from the root (global scope, stack frames). Any object reachable from a root is "live."
  2. Sweep -- free everything not marked.

The pause problem: during the sweep, V8 needs to be sure no code is modifying the object graph. It may pause JavaScript execution briefly while it scans. These are GC pauses -- and in a Node.js server handling thousands of requests, they are visible as latency spikes.

Modern V8 has incremental and concurrent GC that reduces pause durations significantly. But the pauses do not disappear -- they just get shorter and distributed.

The implication for Node.js code: object allocation pressure matters. Creating millions of short-lived objects inside a hot path forces frequent GC cycles. Reusing objects, using typed arrays for numeric data, and avoiding unnecessary allocations all reduce GC pressure.

What This Changes

With this model in mind, some JavaScript behaviors become predictable.

Why does adding a property to an object inside a hot function slow it down? Because V8 had to create a new hidden class, invalidating the inline cache.

Why do typed arrays (Uint8Array, Float64Array) outperform regular arrays for numeric work? Because typed arrays have fixed element types -- V8 does not need the dynamic property/elements machinery, and JIT-compiled code can work with them directly.

Why does code in setTimeout(fn, 0) sometimes run slower than equivalent synchronous code? Because it runs in a different phase with a cold JIT -- the compiled version of the function may not yet exist.

The next post covered the event loop phases. Now that V8's execution model is clear, the phases become more concrete: the timers phase schedules V8 bytecode execution, the poll phase suspends V8 while the OS handles I/O, and GC pauses can delay phase transitions.

The Essentials

  1. Your JavaScript is input, not the program. Node.js is the program. V8 reads your script as a string and turns it into machine code. The OS only knows about Node, not your app.
  2. Bytecode first, JIT for hot paths. V8 compiles JavaScript to compact bytecode, then identifies frequently called functions and compiles those to native machine code at runtime.
  3. Objects are two arrays. Properties (named keys) and elements (integer indexes) are stored separately. Hidden classes map property names to positions. Inline caching avoids repeating that lookup.
  4. Changing an object's shape kills the cache. Adding properties to an object after its initial creation forces V8 to build a new hidden class and invalidate cached lookups.
  5. GC pauses are real. Mark-and-sweep garbage collection briefly pauses JavaScript execution. High object allocation rates in hot paths cause more frequent pauses and more visible latency.

Further Reading and Watching