JavaScript JIT Compilation Advanced Tutorial

JIT compilation is how JavaScript engines achieve near-native performance. Learn how profiling, multi-tier compilation, and speculative optimization work together.

7 min read

JavaScript JIT compilation, short for Just-In-Time compilation, is the technique that all modern JavaScript engines use to achieve near-native performance. Instead of compiling everything before execution or interpreting everything line by line, a JIT compiler observes what your code actually does at runtime and compiles only the parts that matter most.

Ahead-of-time compilers translate the entire program before it runs. This gives predictable performance but slow startup. Interpreters start instantly but run code slowly.

JIT compilation combines the best of both: fast startup through initial interpretation, then progressively better performance through selective compilation of hot code paths.

The Core Idea: Profile Before You Optimize

JIT compilation follows a simple but powerful rule: do not optimize what you do not understand. Before generating specialized machine code, the engine watches the code run and collects data about its actual behavior.

This data, called profiling or type feedback, answers questions like: what types flow through each operation, which branches are taken most often, which functions are called from where, and what object shapes appear at each property access. The optimizing compiler uses this data to make informed decisions about how to specialize the code.

Without profiling, a compiler must handle every possible type and code path. With profiling, it can generate code that only handles what actually happens, skipping the generality that makes interpreted code slow.

Multi-Tier Compilation

Every modern JavaScript engine uses multiple compilation tiers. A function moves up the tiers as it proves itself worth the investment.

Multi-tier JIT compilation pipeline

Tier 1 is the interpreter, which starts executing immediately. Tier 2 is a fast baseline compiler that produces unoptimized machine code for moderately warm functions. Tier 3 is the optimizing compiler that generates highly specialized code for hot functions.

Each tier costs more compilation time but produces faster code.

In V8, the tiers are Ignition (interpreter), Sparkplug (baseline), Maglev (mid-tier), and TurboFan (optimizing). SpiderMonkey uses a similar model with its interpreter, Baseline compiler, and Warp. JavaScriptCore has LLInt, Baseline JIT, DFG (Data Flow Graph), and FTL (Faster Than Light).

For V8's specific approach, see /javascript/how-the-google-v8-engine-compiles-javascript.

How Engines Decide What to Compile

Engines do not compile every function. Compilation has a cost: it takes CPU time and memory. Engines use heuristics to pick the functions where the investment pays off.

The primary signal is execution count. A function called thousands of times is a better candidate than one called once. Loop iteration count also matters because a function with a tight loop may execute millions of operations in a single call.

Engines also consider function size. A very small function may be inlined into its callers rather than compiled independently. A very large function may be skipped because the compilation cost is high and the optimization benefit is spread thin.

Speculative Optimization

Speculative optimization is the defining technique of JIT compilation. The compiler generates code that assumes certain conditions are true.

If the conditions hold, the code runs fast. If not, the engine falls back to a safe but slower path.

The most common speculation is about types. If profiling data shows that a variable is always a number, the compiler generates integer arithmetic instructions that skip JavaScript's normal type coercion.

A guard instruction checks the type before the optimized code runs. If the type is wrong, the guard triggers deoptimization.

Property access is another speculation target. If an object always has the same hidden class, the compiler generates a direct memory load at a fixed offset.

The guard checks the hidden class. If it matches, the load is a single CPU instruction.

Speculation works because JavaScript programs tend to be type-stable in hot paths. A loop that adds numbers usually keeps adding numbers. A function that accesses a property on one kind of object usually keeps accessing the same kind of object.

Deoptimization and Bailout

Deoptimization is the safety net that makes speculation safe. When a speculative assumption fails, the engine discards the optimized code and reconstructs the unoptimized state.

Deoptimization is not a crash. It is a controlled transfer back to the interpreter. The optimized code includes metadata that maps every optimization point back to the corresponding interpreter state.

When a guard fails, the engine reads this metadata, builds interpreter frames, and resumes execution in the interpreter.

The cost of deoptimization varies. A single deoptimization is cheap enough to be invisible.

But if a function deoptimizes on every call, the combined cost of optimizing and deoptimizing exceeds the cost of just interpreting. Engines track this and stop optimizing functions that deoptimize too often.

Inline Caching as Lightweight JIT

Inline caching is a related technique that sits between interpretation and full JIT compilation. It is simpler and faster to deploy than full method compilation.

The first time the engine encounters a property access at a particular call site, it performs a full lookup through the object's hidden class and prototype chain. It caches the result: the hidden class and the property offset.

Next time the same call site executes, the engine checks if the hidden class matches the cached one. If it does, it uses the cached offset directly.

A monomorphic inline cache handles one hidden class. A polymorphic cache handles two to three. A megamorphic cache has given up and performs a full lookup every time.

Inline caches are the most common form of JIT optimization in JavaScript engines because property access is the most frequent operation.

Profile-Guided vs Tracing JIT

JavaScript engines and other JIT compilers do not all pick the same unit of compilation. The two main approaches are profile-guided (method-based) JIT and tracing JIT.

Profile-Guided JITTracing JIT
Unit of compilationA whole functionA recorded hot path, including calls made along it
Used byV8, SpiderMonkey, JavaScriptCoreLuaJIT and some research VMs
Works well whenScoping, closures, and this-binding align with function boundariesHot paths often cross function boundaries in predictable ways

JavaScript engines prefer method-based JIT because JavaScript functions are the natural unit of optimization, and the language's scoping rules align with method boundaries rather than arbitrary trace paths.

How JavaScript's Dynamic Nature Challenges JIT

JavaScript is a challenging language for JIT compilation. Types change at runtime. Objects gain and lose properties.

Prototype chains are mutable. Functions can be called with any number of arguments. Eval can introduce new variables into any scope.

JIT compilers handle this with the combination of speculation at the fast path and deoptimization at the slow path. The fast path assumes the common case. Deoptimization handles the exceptions.

The insight is that hot code paths in real JavaScript programs tend to be stable, even though the language allows anything to change at any time.

Understanding this tradeoff helps you write optimizable code. Stable types, consistent object shapes, and monomorphic call sites let the JIT compiler stay on the fast path. For guidance on writing engine-friendly code, see /javascript/javascript-v8-engine-internals-complete-guide.

Rune AI

Rune AI

Key Insights

  • JIT compilation compiles code during execution rather than before, enabling both fast startup and high throughput.
  • All modern engines use multiple tiers: interpreter, baseline compiler, and optimizing compiler.
  • Type feedback from the interpreter drives speculative optimizations in the top tier.
  • Deoptimization falls back when assumptions break, trading correctness for speed on the fast path.
  • Writing monomorphic, predictable code helps JIT compilers produce the fastest output.
RunePowered by Rune AI

Frequently Asked Questions

Is JIT compilation unique to JavaScript?

No. JIT is used by Java (HotSpot), .NET (RyuJIT), Lua (LuaJIT), and Python (PyPy). The concept is the same across languages: compile hot paths at runtime based on observed behavior.

Can JIT compilation ever make code slower?

Yes. If a function deoptimizes frequently, the cost of compiling and deoptimizing can exceed the cost of just interpreting. Engines track optimization success rates and stop trying to optimize functions that deoptimize too often.

Conclusion

JIT compilation is the reason modern JavaScript runs at remarkable speeds. Engines observe, profile, compile, and recompile as your program runs. The tiered approach gives instant startup through interpretation and peak performance through aggressive optimization. Understanding JIT helps you write code that the engines can optimize.