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.
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.
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 JIT | Tracing JIT | |
|---|---|---|
| Unit of compilation | A whole function | A recorded hot path, including calls made along it |
| Used by | V8, SpiderMonkey, JavaScriptCore | LuaJIT and some research VMs |
| Works well when | Scoping, closures, and this-binding align with function boundaries | Hot 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
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.
Frequently Asked Questions
Is JIT compilation unique to JavaScript?
Can JIT compilation ever make code slower?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.