How V8 JavaScript Engine Works? Deep Dive [Step by Step]
Table of Contents
What is JavaScript Engine?
A JavaScript engine is a specialized application or interpreter within a web browser or runtime environment that executes JavaScript code. A JavaScript engine can be implemented as either a regular interpreter or a just-in-time compiler that converts JavaScript to bytecode in some way. These engines are essential to current web development, allowing for dynamic and interactive functionality on websites and web apps. These engines, which have grown greatly since their introduction, provide for dynamic and interactive capabilities that are essential to modern web applications.
Here’s a list of popular projects that use a JavaScript engine:
- V8: Developed by Google.
Used in Google Chrome, Node.js, and Opera.
Features include high-performance JIT compilation, fast garbage collection, and support for Web Assembly. - Spider Monkey: it is developed by Mozilla.
Used in Firefox.
Features include advanced JIT compilation techniques, robust debugging tools, and ongoing support for ECMAScript standards. - Chakra: Developed by Microsoft.
Used in: Internet Explorer and Microsoft Edge (legacy)
Features include JIT compilation, precise trash collection, and compatibility with other languages via Web Assembly. - JavaScript Core (Nitro): it is developed by Apple.
Used in: Safari, different Web Kit-based applications.
Features include JIT compilation, efficient garbage collection, and robust security measures. - Rhino: it is managed by the Mozilla Foundation, open source, and built entirely in Java.
Why was the V8 Engine created?
Google developed the V8 JavaScript engine, which is a high-performance engine designed to efficiently execute JavaScript code. It was built to meet the growing demand for quicker and more reliable execution of JavaScript, which had become the de facto web language. Prior to the introduction of V8, JavaScript engines depended heavily on interpretation, which was fine for simpler online applications at the time but became a bottleneck as web applications expanded in complexity. Google realized this constraint and aimed to revolutionize JavaScript execution, notably for their new web browser, Chrome.
The fundamental aim for developing V8 was to increase the speed and performance of web applications. When Google launched Chrome in 2008, their goal was to create a browser that could handle complicated web apps with the same responsiveness and fluidity as desktop applications. To accomplish this, they required a JavaScript engine capable of running code at extraordinary rates. Just-In-Time (JIT) compilation is the process of compiling JavaScript into machine code prior to execution, and it is the foundation of V8. V8 was able to run JavaScript far faster than conventional interpreted engines because to this method.
V8 Engine JavaScript [Chrome Engine]:
V8 is Google’s open source, high-performance JavaScript and Web Assembly engine written in C++” (V8 description), but what exactly does this mean? V8 is a C++ software that receives, compiles, and executes JavaScript code. Unlike the other engines, V8 also powers the popular Node.js runtime.
V8 lacks understanding of the browser-provided Document Object Model (DOM).
V8 is a single-threaded execution engine. It’s designed to operate only one thread per JavaScript execution context. Two V8 engines, such as web-workers, can operate in the same process but do not exchange variables or context, unlike real threads. This does not imply that V8 runs on a single thread, but rather that it provides a JavaScript flow for a single thread.
V8 lacks Understanding of DOM (Document Object Model):
What does it mean that V8 does not know about the DOM (Document Object Model)? Well from high level view, the render process of browser like chrome you can say, initializes Host environment and V8 engine. A browser contains many renderer processes. Typically, each browser tab contains a renderer process that initializes a V8 instance.
Keep in mind that a browser is just one of several host environments for JavaScript. Another popular one is the Node host environment.
The host environment offers everything that a JavaScript engine depends on, including:
- Call stack Heap Callback queue
- Event loop
- Web API and Web DOM
A sequence of events is triggered when a user interacts with a web page. The browser added them to the callback queue, along with the corresponding callback functions. The event loop functions as an infinite while loop, constantly retrieving a callback from the queue. The JavaScript contained in the callback is then compiled and executed. Some intermediate data is kept on the call stack. Some are stored in the heap, such as an array or an object.
V8 is focused on translating JavaScript code into machine code that can be executed by the computer’s CPU. It handles the basic logic and computations specified in the JavaScript code.
The web browser, such as Chrome, is in charge of rendering web pages, handling user interactions, and providing the interfaces required to interact with the web content.
How V8 Engine of JavaScript Works with Examples:
V8 is designed to run JavaScript code. It converts JavaScript source code into machine code that can be executed by the computer’s hardware. In addition to JavaScript, V8 includes Web Assembly, a binary instruction format that enables developers to run multilingual code on the web at near-native speeds.
V8 is an open-source project, which means that the source code is freely available and can be edited or used by anybody. The following are the stages that V8 takes:
- Compiles and executes JS code, including managing the call stack and performing methods in a specific order.
- Managing memory allocation for objects—the memory heap.
- Garbage collection refers to objects that are no longer in use.
- Describe all data types, operators, objects, and functions.
Internally, the V8 Engine makes use of many threads.
- The main thread fetches your code, compiles it, and then executes it.
- There is also a separate thread for compilation, so that the main thread can continue executing while the latter is optimizing the code.
- A profiler thread will tell the runtime the procedures we spend a lot of time on so that Crankshaft or Turbofan can optimize them.
- Several threads to conduct garbage collector sweeps.
So, in order to understand the threads of V8, you can read the article completely. You will get to know about this by reading further as I m going to explain it step by step:
V8 JavaScript Engine Architecture:
Ignition (JS Interpreter [ Compile JavaScript codes]):
At this point, the V8 engine translates the JavaScript code to an Abstract Syntax Tree (AST) and creates scopes. The V8 engine does not support the JavaScript programming language. Before processing can begin, the script must be organized. The AST format is a tree structure that V8 can easily digest.
Meanwhile, scopes are created at this stage, including the global scope and other scopes at the top that are saved in the host environment call stack. Every line of your JavaScript code will be transformed to AST.
First and foremost, the code is compiled by a baseline compiler, which swiftly generates non-optimized machine code. At runtime, the compiled code is inspected and can be recompiled for maximum performance. The first comes from ignition, and the second from Turbofan and Crankshaft.
The AST describes the source code’s hierarchical structure, which includes expressions, statements, and relationships. Scope analysis determines the visibility and lifetime of variables and functions in a code. This includes handling lexical scope, function scoping, and closures, as well as ensuring that variables are only accessible where they should be.
Ignition uses the AST and scope information to generate bytecode.
Bytecode is a low-level representation of the code that the Ignition interpreter can run. The bytecode is intended to be compact and efficient for interpretation, resulting in faster startup times and less memory use than directly executing high-level JavaScript.
Bytecodes vs. machine codes:
Machine codes demand a significant amount of memory. The V8 engine saved compiled machine codes in memory so that they may be reused when the page loaded.
When compiled to machine code, a 10K JavaScript could generate 20M machine codes. That’s around 2,000 times more memory space.
How about the bytecode size in the same case? It is around 80,000. Bytecodes are still larger than the original JavaScript, but they are far smaller than the corresponding machine codes.
Machine code takes longer to compile, but it executes very quickly. Bytecodes take less time to compile, but they execute slower. To execute bytecodes, an interpreter must first interpret them.
The challenge is to strike a compromise between these two choices while creating a robust interpreter and a clever optimizing compiler for bytecodes. Also, Machine codes increases complexity in development.
JIT (Just-In-Time) Compilation
JIT Compilation is the process of converting code to machine code during runtime rather than before execution.
In the context of the V8 engine, JIT compilers convert the bytecode generated by Ignition into highly efficient machine code.
This approach entails analyzing the running code to discover hot spots (often executed code pathways) that could benefit from optimization.
JIT compilation tries to increase the performance of JavaScript code by ensuring that it runs as quickly as feasible when executed frequently. JIT compilation result machine code might take up a lot of memory even if it is just performed once. This is solved by Ignition, which runs code with less memory overhead.
Crankshaft
Crankshaft is the previous generation optimizing JIT compiler for the V8 engine before Turbofan. Crankshaft intended to improve JavaScript performance by converting it into efficient machine code.
Turbofan has entirely replaced the crankshaft, providing more advanced optimization techniques and improved performance on contemporary hardware
Turbofan
The Turbofan is a JIT compiler which optimizes the V8 engine. It converts the bytecode generated by Ignition into very efficient machine code. Turbofan performs a wide range of advanced optimizations, including function inlining, code elimination, hidden classes, inline cashing and optimization for contemporary CPU architectures. Turbofan’s purpose is to maximize the performance of JavaScript code by generating the fastest machine code.
function inlining:
The initial optimization is to inline as much code as feasible in advance. Inlining is the process of replacing a call site (the line of code from which the function is called) with the body of the called function. This simple step helps the subsequent optimizations to be more effective.
Hidden Classes:
JavaScript is a dynamically typed language, which means that objects can be changed during runtime by adding or removing properties.
To improve property access, V8 employs hidden classes. When an object is formed, V8 provides it a hidden class that describes the arrangement of the object’s properties.
If any properties are added or modified, V8 converts the object to a new hidden class that represents the changes. Turbofan, the optimizing compiler, uses hidden classes to produce highly efficient machine code.
Turbofan can inline property accesses, which means it can produce direct memory access instructions for properties based on an object’s hidden class.
This decreases the overhead of property lookups while greatly increasing execution speed.
Inline Caching
Inline caching is a technique for speeding up repetitive property accesses.
When a property is accessed for the first time, V8 does a thorough lookup and caches the result. Turbofan uses the information acquired from inline caches during the interpretation phase (by Ignition) to make intelligent decisions.
If inline caches reveal that some property accesses are monomorphic or polymorphic, Turbofan can write optimized machine code assuming these patterns will persist.
Turbofan can inline routines and optimize property accesses using these caches, eliminating the need for repeated lookups and enhancing overall speed.
Garbage collection:
Garbage collection is the process of automatically removing memory that is no longer being used by the program, hence preventing memory leaks and optimizing memory use.
The V8 JavaScript engine uses a powerful garbage collector called Orinoco, which combines numerous clever approaches to improve performance and reduce pause periods during trash collection.
Every garbage collector must perform a few basic duties on a regular basis:
- Identify objects that are alive or dead.
- Recycle or reuse the memory used by dead items.
- Compact/defragment memory (optional)
These tasks can be performed sequentially or arbitrarily. A simple solution is to halt JavaScript execution and do each of these actions in order on the main thread. This may result in jank and latency issues on the main thread.
The Orinoco scans the memory heap for unconnected memory allocations. Implementing a generational trash collector involves moving things within the young generation, from the young to the old generation, and within the old generation. These movements leave holes, and Orinoco uses both evacuation and compaction to make room for new things.
Another optimization accomplished by Orinoco is the way it searches across the heap for all pointers that contain the previous location of the objects that have been moved and updates them to the new address. This is accomplished using a data structure known as a remembered set.
Understanding the Generations in Garbage Collection:
Memory management in a JavaScript engine such as V8 is handled via a process known as garbage collection (GC). The heap is a section of memory where objects are allocated. The heap is typically separated into two parts: the younger generation and the older generation.
- Younger Generation:
This section of the heap is where new objects are allocated.
It is best suited for things with short lifespans.
Garbage collection occurs regularly here, generally utilizing a technique known as “scavenge” or “minor GC.” - Older Generation:
This is where long-lasting items are stored.
Objects that survive many garbage collections cycles in the younger generation advance to the older generation.
The older generation manages a bigger amount of memory and has a more complex garbage collection method for long-lived things.
Compaction / Evacuation in V8 engine:
Memory can become fragmented over time, which means that free memory is separated from used memory in discrete blocks.
Compaction involves shifting living items to generate contiguous free space and updating references. The compact phase eliminates fragmentation and creates bigger contiguous blocks of free memory in order to solve this. This is done by moving live items together and updating references. This compaction improves memory efficiency and reduces the likelihood of running out of memory due to fragmentation.
On the other hand, Evacuation is a technique used during garbage collection in the heap’s younger generation, typically as part of a “scavenge” or “minor GC” procedure. The primary goal of evacuation is to move living items from one location in memory to another while compressing them to maximize memory usage. It entails transferring live items between memory regions, usually with the goal of minimizing fragmentation and effectively recovering memory. From the “from” space to the “to” space, live items are copied over.
After copying, the “from” area, which now just contains unreachable items (trash), is completely reclaimed.
Thank you 😊
https://selfhelp247.online/prime-15-motivational-quotes-to-encourage-and-uplift-your-day/
November 12, 2024 @ 8:22 am
Great information. Lucky me I ran across your blog by accident (stumbleupon).
I have saved as a favorite for later!