Everything You Need to Know About the JavaScript V8 Engine
The JavaScript V8 engine, is an open-source JavaScript and Web Assembly engine known for its high performance and efficiency. It is developed by Google and is used in Chrome browser and NodeJs.
Key Components of V8 engine:
Parser:
Parsing is the first in the V8 engine. Parser takes the JavaScript code and convert it into AST(Abstract Syntax Tree). AST is a hierarchical tree like representation of the code. Let's understand working of parser with an example. Consider the below JavaScript code.
function add(a, b){
const sum = a+b;
return sum;
}
Working of parser:
Tokenization: Parser begins by breaking raw JavaScript into tokens. A token is the smallest unit of a program like keywords('function', 'const', etc.), operators('+', '/', etc.), literals('add', 'sum', etc.), punctuators('{', '(', etc.).
Abstract Syntax Tree creation: Tokens generated are then used to create an Abstract Syntax Tree. AST is a tree-like structure where each node is a construct occurring in the code. For example: 'If' statement, function declaration, etc.
Syntax Error handling: During parsing if the parser finds something that doesn't follow JavaScript's syntactical rules then it throws error.
Interpreter (also called ignition):
Interpreter specifically called ignition in V8 takes Abstract Syntax Tree generated by parser and converts it into byte code. ByteCode is low level representation of code that can be executed by V8.
Working of interpreter:
ByteCode Generation: Interpreter first takes the Abstract Syntax Tree and converts it into byte code. It is much more efficient as it is easier to process than raw JavaScript.
Execution: Executes bytecode directly as ignition is an interpreter and doesn't wait for the whole code to be compiled.
Profiling: While executing, ignition collects information about the code being executed which is used by the compiler to optimize the code.
What is Profiling?
Profiling is the process of collecting runtime information about the execution of JavaScript code like how many times a function is called, types of variables being used, how variables are being used, etc. This information is used to further optimize the code for better performance. This is done with minimal memory overhead so that it doesn't hinder with the application's performance. This is a large topic and is better suited for a separate blog post on another day.
Compiler (also known as TurboFan):
Compiler in V8 is TurboFan and it is responsible for converting byte code to highly optimized machine code. Machine code is the lowest level representation of code specific to CPU architecture, allowing for faster execution.
Working of Compiler:
Optimization Starts: V8 doesn't compile every piece of code immediately. Instead it waits for the interpreter to complete profiling the code. If a function is executed frequently then ignition marks it as hot and TurboFan steps in to optimize it.
Optimizations:
Inlining: Small functions are inlined (their code is directly inserted at the place they are called), eliminating the memory overhead of function call.
Type Inference: TurboFan makes assumptions about the types of variables(for example, if a variable is of type number throughout the execution of the program then it is marked as number). If the assumption is wrong then V8 deoptimizes it.
Dead Code Elimination: Codes that will never be executed(like code after return statement) are eliminated.
Loop Optimizations: Loops are optimized for faster executions by minimizing unnecessary calculations and instructions in the loop.
Deoptimization: If the assumption made during profiling are invalidated during runtime then TurboFan can revert the code to less optimized state. This ensures correctness of the code at the cost of performance.
For Example:
Sample Code:
function add(a, b) {
return a + b;
}
function sumArray(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += add(arr[i], 1);
}
return sum;
}
const array = [1, 2, 3, 4, 5];
console.log(sumArray(array));
Optimized Loop:
for (let i = 0; i < arr.length; i++) {
sum += arr[i] + 1; // Inlined the add function
}
Garbage Collection
Garbage Collector in V8 is responsible for managing memory. It automatically allocates memory when objects are created and de-allocates when objects are no longer needed preventing memory leaks.
Working of Garbage Collector:
Generational Garbage Collection:
Young Generation: This space is where new objects are allocated. The idea is that new objects die quickly and so garbage collection is done frequently here.
Old Generation: Objects that survive multiple collections in young generation are promoted to the old generation. This space is garbage collected less frequently but with more thorough algorithms as objects are here are expected to live longer.
Mark-and-Sweep:
Marking Phase: During garbage collection, V8 first marks all the objects that still can be accessed by the program.
Sweeping Phase: It then sweeps through the memory and de-allocates all the objects that are no longer needed, thus freeing up memory.
Incremental & Concurrent Garbage Collection:
Incremental Collection: To avoid long pauses, V8 performs garbage collection in small rather than all at once.
Concurrent Collection: V8 can perform garbage collection tasks in parallel with the execution of JavaScript code, minimizing the impact on performance.
For Example:
function createArray() {
let arr = new Array(1000).fill(0);
return arr;
}
createArray();
After "createArray" is called the array "arr" may be garbage collected if it is not referenced, freeing up memory.
Conclusion
Each of these components plays a crucial role in making V8 a fast and efficient engine, capable of executing JavaScript code in real-time with high performance.
Subscribe to my newsletter
Read articles from Sparsh Shandilya directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by