How it works
The explanation, with examples you can run.
The common front end
Whatever happens later, an implementation starts the same way:
- Lexing turns characters into tokens:
let x = a + 1;becomeslet, identifierx,=, identifiera,+, number1,;. - Parsing builds a tree from the tokens according to the grammar, so that
a + 1 * 2groups asa + (1 * 2). - Analysis resolves names, checks types (in typed languages) and reports errors with line numbers.
Then comes the back end: either generate code for a real CPU, or generate code for a virtual machine.
Ahead-of-time compilers
C, C++, Rust, Go and Swift compile to native machine code before the program ships. The compiler (typically through an intermediate representation, as in LLVM) can spend seconds or minutes optimising: inlining, unrolling loops, vectorising, allocating registers. The program starts instantly at full speed and needs no runtime translator. The costs are slow builds, one binary per CPU and operating system, and optimisations limited to what can be proven from the source, without knowing what the real inputs will be.
Interpreters
An interpreter executes the program without producing machine code for it. Simple ones walk the syntax tree; most real ones first compile to bytecode, a compact instruction set for a virtual machine, and then run a loop that fetches, decodes and executes one bytecode at a time. CPython works this way (the .pyc files are cached bytecode). Interpreters start immediately and run anywhere the interpreter runs, but every bytecode costs dozens of machine instructions of dispatch overhead, which is why pure-Python loops are much slower than the same loop in C.
Just-in-time compilers
A JIT combines the two. V8 (Chrome, Node.js) starts JavaScript in the Ignition bytecode interpreter, compiles warm functions quickly with Sparkplug and Maglev, and sends the hottest to TurboFan, its optimising compiler. HotSpot (Java) and RyuJIT (.NET) follow the same tiered idea, as the Java and .NET paths explain.
The advantage of waiting is information. While the program runs, the JIT records which types actually appear and which branches are taken, then compiles code specialised for that behaviour, guarded by cheap checks. If an assumption breaks (a function that has only ever seen integers receives a string), the runtime deoptimises back to the interpreter and may compile again later. The disadvantages are warm-up time, memory for the compiler and its profiles, and less predictable performance.
Choosing in practice
- Start-up time matters most (command-line tools, serverless functions): prefer ahead-of-time output, such as Go or Rust binaries, GraalVM native-image for Java, or .NET Native AOT.
- Long-running services: a JIT's warm-up is paid once and its profile-driven optimisations keep paying.
- Benchmarks of JIT-compiled code must warm up first, or they measure the interpreter.
WebAssembly shows that the line is blurry: it is a portable bytecode that browsers compile to machine code, sometimes ahead of time and sometimes in tiers, just like a JIT.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.