Articles

What Is JIT Compilation? Just-In-Time Compiling Explained

Just-in-time (JIT) compilation translates code to native machine instructions while a program runs, trading startup time for peak execution speed.

The Lycoris Team The Lycoris Team · · 5 min read
Mathematical equations and diagrams written on a chalkboard

Just-in-time (JIT) compilation is a technique where a program’s code is compiled to native machine instructions while the program is running, rather than entirely before it starts. It’s the middle ground between a pure interpreter, which re-reads and re-executes source (or bytecode) every time it runs, and a pure ahead-of-time (AOT) compiler, which turns everything into machine code once, before execution ever begins. JIT compilers get the fast startup of interpretation and the raw execution speed of compiled code, by doing the expensive part only for the code that turns out to matter.

Interpreting, compiling, and the gap between them

An interpreter walks through source code or bytecode and executes it step by step, translating instructions on the fly. That makes startup instant — there’s no separate compile step — but it’s slow for anything that runs repeatedly, since the same line gets re-interpreted every single time through a loop.

An AOT compiler does the opposite: it analyzes the entire program up front and emits optimized machine code before the program runs at all, the way a C compiler produces a binary you then execute. Startup is fast once you have the binary, and the resulting code is fast, but you pay the compilation cost eagerly, for every function, whether or not it ever actually gets called.

JIT compilation splits the difference by starting as an interpreter (or a bytecode interpreter) and promoting code to compiled machine instructions only once it’s proven itself worth the cost — typically because it’s been executed often enough to be “hot.”

How a JIT compiler decides what to compile

Most production JIT compilers use tiered compilation and runtime profiling:

  1. Interpret first. Code starts running through a bytecode interpreter, which is slow per-instruction but has zero compilation overhead — good for code that only runs once or twice, like most startup logic.
  2. Profile as you go. The runtime counts how often each function or loop executes and tracks the actual types and shapes of the values flowing through it.
  3. Compile the hot paths. Once a function crosses an execution threshold, the JIT compiles it to native machine code — often through multiple tiers, with a quick, lightly optimized compile first and a slower, more aggressive one if the function stays hot.
  4. Speculate, then deoptimize if wrong. Because the compiler has runtime information an AOT compiler never sees — “this function has only ever received small integers” — it can generate code that assumes that pattern holds, inlining and skipping checks accordingly. If a later call breaks the assumption, the JIT falls back to a safer compiled tier or the interpreter, a process called deoptimization.

That last point is what makes JIT compilation genuinely different from AOT, not just “compilation done later.” A JIT can specialize code for the actual data a program sees in production, something an AOT compiler working from static source alone cannot do.

JIT vs ahead-of-time compilation

JIT compilationAOT compilation
When code is compiledWhile the program runsBefore the program runs
Startup timeFast (interpret first)Fast, but only after the build step
Peak throughputVery high, once hotHigh, and consistent from the start
Uses runtime dataYes — types, branch patterns, hot loopsNo — only static analysis
PredictabilityWarm-up period; can deoptimizeConsistent performance from instruction one
Typical useManaged runtimes: JS engines, JVM, .NET CLRSystems languages: C, Rust, Go

Neither approach is strictly better — they’re optimizing for different constraints. A long-running server process benefits enormously from a JIT’s ability to specialize to real workload data over time. A short-lived CLI tool or a system where predictable latency matters more than peak throughput often favors AOT, since there’s no warm-up period and no risk of a deoptimization pause.

Where JIT compilation actually shows up

JavaScript engines are the JIT most web developers interact with daily without thinking about it — every time a loop in your code runs faster the longer a page stays open, that’s tiered JIT compilation promoting hot functions, a big part of why modern JavaScript’s event loop can push through so much work without visibly stalling. The Java Virtual Machine popularized the technique for server workloads, and the .NET CLR uses the same tiered approach. Some Python implementations add JIT compilation on top of the standard interpreter for similar reasons, though the reference CPython interpreter runs purely interpreted by default.

It’s worth contrasting JIT with two adjacent ideas that get confused with it. WebAssembly is typically compiled ahead-of-time (or with a very fast baseline JIT) precisely because it’s already a low-level, statically-typed bytecode — most of the work a JIT would normally do to infer types has already been done by the source compiler. And a JIT-compiled runtime is not the same as tail-call optimization, which is a specific transformation of recursive calls into loops; a JIT can apply that optimization, but plenty of AOT compilers do too.

The tradeoffs

JIT compilation isn’t free. Profiling and multiple compilation tiers cost CPU cycles and memory that a purely interpreted or purely AOT-compiled program doesn’t spend. Warm-up matters: a JIT-heavy runtime is often at its slowest in the first few seconds of execution, which is exactly the wrong property for a serverless function invoked cold thousands of times. And deoptimization, while rare in steady state, introduces a small amount of unpredictability — a function that’s been running fast for an hour can suddenly hit a case that invalidates its compiled assumptions and briefly fall back to a slower tier.

None of this makes JIT compilation the wrong choice for its target workloads — it’s the reason most managed-language runtimes are competitive with statically compiled languages on long-running server code at all. It’s simply a design that trades startup predictability for adaptive peak throughput, which is exactly the right trade for a language runtime that doesn’t know in advance which functions will matter.

The takeaway

JIT compilation defers the expensive part of compiling to native machine code until runtime, using real execution data to decide what’s worth optimizing and how. That lets a program start as fast as an interpreter and, once its hot paths are identified and compiled, run close to as fast as code compiled ahead of time — at the cost of a warm-up period and the occasional deoptimization when a speculative assumption turns out wrong.

The Lycoris Team The Lycoris Team · · 6 min read

Copy-on-Write Explained: How Lazy Copying Works

Copy-on-write shares data until someone modifies it, then copies only what changed. How CoW powers fork(), snapshots, containers, and immutable data.

#Computer Science #Data Structures #Performance
The Lycoris Team The Lycoris Team · · 5 min read

TCP Congestion Control Explained: Slow Start to BBR

TCP congestion control decides how fast a sender transmits without overloading the network. Slow start, AIMD, CUBIC, and BBR explained.

#Networking #Computer Science #Performance
Chisato Chisato · · 4 min read

Thermal Interface Materials Explained

Thermal interface material fills microscopic gaps between a chip and its heatsink so heat can actually transfer to the cooler.

#Hardware #Computer Science #Performance