Field notes

How V8 executes JavaScript?

JavaScript engine is the program responsible for executing the code coming from script blocks. One of the most popular engines is V8 developed by Google. On a very high level here is how it works:

  1. Content of script is fetched by Html Parser as a sequence of bytes which then is transformed into the tokens by stream decoder.
  2. Decoded tokens are transformed into the AST. AST is a representation of the code’s structure.
  3. AST is transferred to interpreter called Ignition. It acts as a bytecode compiler and as a high-performance bytecode interpreter. Ignition compiles the AST to bytecode and then interprets it, collecting type feedback along the way.
    1. During the initial processing of JavaScript code, Ignition takes the parsed code and transforms it into a concise bytecode representation. The process is called Just-In-time (JIT) compilation. It’s not specific to JavaScript, for example it’s also used in .NET. In general it is the process of translating code written in a programming language to machine code at runtime.
    2. Bytecode is an intermediate code and suitable for cases when it needs to be run once. It is an abstraction of machine code. It’s a bit slow but JavaScript engine can run it by itself. Compared to the original JavaScript code, bytecode is more compact and easier for the interpreter to process.
    3. When the process is done AST can be deleted.
  4. Bytecode is then passed to optimizing compiler called TurboFan together with the type feedback collected by Ignition interpreter. TurboFan uses that feedback to create highly optimized machine code which runs directly on the CPU. If the assumptions it made turn out to be wrong, the optimized code is thrown away and execution falls back to bytecode. This is called deoptimization.
    1. At runtime, certain dynamic information is available, such as type identification. While Ignition runs the bytecode it detects functions or loops of code that are run multiple times and collects type feedback. If they’re quite commonly executed, TurboFan will optimize them and also store the optimized compiled code for execution.
    2. TurboFan compiles the hottest parts of your code into machine code. By “hot” we mean the code that is executed again and again.
    3. It takes bytecode from Ignition and compiles architecture-independent instructions into machine code for the underlying hardware. It performs low-level instruction selection and machine register allocation in the process. It employs various optimization techniques to squeeze out the most performance from your JavaScript code. These include:
      1. Removing unused code sections
      2. Replacing function calls with the actual function body for better performance. This is called inlining
      3. Assigning variables to processor registers for faster access
      4. Making assumptions about data types to generate more efficient code

Since this pipeline was introduced V8 gained two middle tiers sitting between Ignition and TurboFan. Sparkplug (2021) is a fast non-optimizing compiler which turns bytecode into machine code without optimizations. Maglev (2023) is a mid-tier optimizing compiler, faster to run than TurboFan but producing less optimized code.

The logic behind the tiering is a cost/benefit ladder. Most functions run only a handful of times, so spending milliseconds optimizing them would never pay off. Each tier up costs more compile time and buys faster execution, and V8 promotes a function only when its run count proves the investment is worth it.

Bytecode is an intermediate representation for virtual machines or compilers. It is processed by the software, not hardware.

Machine code is a low-level code that can be run and understood directly by machine. It means it can be run directly on the CPU.

The main difference between compiler and interpreter is that the interpreter goes through the code line-by-line and executes it. The compiler prepares code for execution at first.

V8 approach to executing JavaScript

References