preface

In the past few years, JavaScript has rapidly grown to become one of the hottest high-level languages on the Internet, with its performance improvements and the emergence of cutting-edge Web technologies making it the backbone of HTML5.

JavaScript engine

When we write JavaScript code and hand it directly to the browser or Node, the underlying CPU is unaware of it and cannot execute it. The CPU only knows its own instruction set, which corresponds to assembly code. It is a pain to write assembly code, for example, if we want to compute N factorial, we only need 7 lines of recursive function. However, to write N factorial in assembly language would require 300+ lines of code (code omitted)

function factorial(N) {
    if (N === 1) {
        return 1;
    } else {
        return N * factorial(N - 1); }}Copy the code

A JavaScript engine is a standard interpreter or just-in-time compiler that somehow compiles JavaScript scripts to CPU-specific bytecode. It’s usually shipped with a web browser. Of course, a JavaScript engine does more than compile code; it also executes code, allocates memory, and collects garbage.

Common JavaScript engines are as follows:

Mozilla -----> The parsing engine is Spidermonkey(implemented in C) Chrome ------> the parsing engine is V8(implemented in C ++) Safari ------> the parsing engine is JavaScriptCore(c/c++) IE and Edge ------> the parsing engine is Chakra(c++) node.js ------> the parsing engine is V8Copy the code

The V8 engine was born

V8 engine is a JavaScript engine originally designed by a group of linguists and later acquired by Google. It takes some strength to dare to name the V8 engine, which was released in 2008 and was inspired by the V8 engine of the Super Performance car. Thanks to the improvements V8 engine has made in JavaScript performance optimization, it has become a popular open source high performance JavaScript engine, currently used in Google Chrome, Android Browser, Node.js and other large projects, and has become an integral part.

Three, V8 engine introduction

[3.1] Examples of calling V8 programming interface and corresponding memory management methods:

The first statement creates a domain containing a set of Handles for managing and releasing them.

The second statement: Obtain a Context object based on the ISOLATE object and use Handle to manage it. The Handle object itself is stored on the stack, while the actual Context object is stored in the heap.

Create an object between functions based on the Isolate and Context objects. Use Persistent classes to manage the object.

Create a stack-based domain for the Context object, and perform the following steps within the corresponding Context of that domain.

Statement 5: Read a piece of JavaScript code;

The sixth statement: compile the code string into V8’s internal representation and save it as a Script object;

The seventh statement: execute the compiled internal representation to obtain the generated result;

[3.2] V8 compilation:




First, the source code is compiled into an abstract syntax tree by a compiler. Different from the JavaScriptCore engine, V8 engine does not convert the abstract syntax tree into bytecode, but directly generates native code from the abstract syntax tree through the full code generator of the JIT compiler.

The main class diagram in the process is as follows:



  • Script: represents JavaScript code, which contains both source code and native code generated after compilation, so it is both a compile entry and a run entry.
  • Compiter: a compiler class that assists the Script class in compiling generated code. It serves primarily as a coordinator, calling the Parse to generate the abstract syntax tree and the full code generator to generate native code for the abstract syntax tree.
  • Parse: Parses source code and builds abstract syntax trees, using the AstNodeFactory class to create them and the Zone class to allocate memory;
  • AstNode: Abstract syntax tree node class, which is the base class for all other nodes; Contains a very large number of subclasses, and will later generate different native code for different subclasses;
  • AstVisitor: Visitor class for iterating through the abstract syntax tree;
  • FullCodeGenerator: a subclass of the AstVisitor class that generates native executable code for JavaScript by iterating through an abstract syntax tree;

The process of compiling JavaScript code is roughly as follows: the Script class calls the Compile function of the Compiler class to generate native code for it. The Compile function uses the Parser class to generate the AST and the FullCodeGenerator class to generate native code. Native code is closely related to the specific hardware platform, and FullCodeGenerator uses multiple backends to generate platform-appropriate native assembly code. Since FullCodeGenerator traverses the AST to generate assembly code for each node, the global view is missing and optimization between nodes is not possible.

Before compiling, V8 builds a runtime environment by building a number of global objects and loading some built-in libraries, such as the Math library. And in JavaScript source code, not all functions are compiled to generate native code, but rather are deferred and compiled when called. Because V8 is missing the process of generating intermediate code, it lacks necessary optimizations. To improve performance, V8 uses profilers to collect information after native code is generated and then optimizes the native code to produce more efficient native code, a step-by-step process. At the same time, when the optimized code is found to be performing less well than the unoptimized code, V8 reverts to the original code, known as optimization rollback.

[3.3] V8 runs

The main class diagram for the V8 runtime is as follows:



  • Script: an entry point for running code that contains native code generated after compilation;
  • Execution: a helper class for running code, including the important “call” functions that assist in the entry and Execution of native code in Script;
  • JSFunction: represents a class of JavaScript functions that need to be executed;
  • Runtime: a helper class that runs native code and provides Runtime helper functions.
  • Heap: the Heap of memory needed to run native code;
  • MarkCompactCollector: The main implementation class of the garbage collection mechanism, used to mark, clean, and collate the basic garbage collection process;
  • SweeperThread: the thread responsible for garbage collection;

Compile and generate the native code as needed, using the classes and operations at compile time. In V8, a function is the basic unit, and when a JavaScript function is called, V8 looks up whether the function has generated native code and, if so, calls the function directly. Otherwise, the V8 engine generates native code belonging to the function. This saves time and reduces the amount of time spent dealing with code that is not being used. Second, you execute the compiled code to build THE JS objects for JavaScript, which requires the Runtime class to help group the objects and allocate memory from the Heap class. Third, we use the auxiliary group functions in the Runtime class to perform some functions, such as attribute access. Finally, unused space is marked for cleanup and garbage collection.

The code execution process in V8 is shown below:

Iv. Optimization of V8 engine

[4.1] Optimization rollback: Since V8 directly generates native code based on AST without intermediate presentation layer optimization, the native code is not well optimized. So, in 2010, V8 introduced a new compiler -Crankshaft. It is optimized for hotspot functions and is based on JS source code analysis rather than native code. For performance reasons, the Crankshaft compiler made some optimistic and bold predictions that the code would be stable and the variable types would not change, thus generating efficient native code; However, given the weakly typed language of JavaScript, variable types can also change during execution, and in this case, V8 rolls back the optimizations that the compiler takes for granted, called optimization rollback. The following is an example:

let counter = 0;
function test(x, y) {
    counter++;
    if (counter < 1000000) {
        // do something
        return 'hehe';
    }
    var let = new Date();
    console.log(unknown);
}Copy the code

After this function is called multiple times, the V8 engine might trigger the Crankshaft compiler to optimize it, and the optimization code might assume that the type information for the sample code has been determined. However, V8 had to roll back that part of the code because it hadn’t actually gotten to new Date() and didn’t get the type of unknown. Optimization rollback is a time-consuming operation. Do not trigger optimization during code writing.

In the recent V8 5.9 release, a new Ignition bytecode interpreter, TurboFan and Ignition, are combined to compile JavaScript together. This version eliminates Cranshaft, the old compiler, and lets the new Turbofan optimize code directly from bytecode, and de-optimize directly to bytecode when needed, regardless of the JS source code.

[4.2] Hidden class: The hidden class divides objects into different groups. In the case that objects in the group have the same attribute name and value, the attribute names and corresponding offset positions of these groups are saved in a hidden class, and all objects in the group share this information. At the same time, objects with different attributes can also be identified. The following is an example:



When objects A and B in an instance contain the same attribute name, V8 classifies them into the same group, the hidden class; These attributes have the same offset value in the hidden class, so that objects A and B can share this type information, and when accessing these object attributes, their location can be known and accessed based on the offset value of the hidden class. Since JavaScript is a dynamically typed language, variable types can be changed during execution. If a.z=1 is executed after the above code is executed, then A and B will no longer be considered a group, and A will be a new hidden class.

[4.3] Memory cache

The normal process of accessing an object property is to first get the address of the hidden class, then look up the offset value based on the property name, and then compute the address of the property. It’s a lot less work than it used to be to find the entire execution environment, but it’s still time consuming. Can you cache the results of previous queries for re-access? Of course it does, it’s called inline caching.

The idea behind inline caching is to save the first lookup of the hidden class and its offset value. The next lookup compares whether the current object is the previous hidden class, and if so, directly uses the previous cache result to reduce the time to look up the table again. Of course, if an object has more than one attribute, then the probability of cache errors increases, because when the type of an attribute changes, the hidden class of the object also changes, which is inconsistent with the previous cache, and the hash table needs to be found again in the previous way.

[4.4] Memory management:

Zone: manages small memory blocks. The Zone allocates a small memory and manages and allocates some small memory. After a small memory is allocated, it cannot be reclaimed by the Zone. Only the small memory allocated by the Zone can be reclaimed at a time. If a process requires a lot of memory, the Zone needs to allocate a large amount of memory, which cannot be reclaimed in a timely manner, resulting in insufficient memory.

Heap: V8 uses a heap to manage data used by JavaScript, as well as generated code, hash tables, etc. To facilitate garbage collection, V8, like many virtual machines, divides the heap into three parts: the first is the young generation, the second is the old generation, and the third is the space reserved for large objects. The diagram below:



  • Young generation: Allocates memory space for newly created objects, often requiring garbage collection. To facilitate the collection of content in the young generation, you can divide the young generation into two halves, with one half for allocation and the other half for copying over objects that need to be retained before collection.
  • Aging generation: Storing old objects, Pointers, code, and other data as needed, with less garbage collection.
  • Large objects: Allocate memory for objects that require a large amount of memory. Of course, it may also contain memory allocated for data, code, etc. Only one object is allocated per page.

Garbage collection:

V8 uses generational and big data memory allocation, using a compact collation algorithm to mark unreferenced objects when reclaiming memory, then eliminate unmarked objects, and finally collate and compress objects that have not yet been saved to complete garbage collection.

In V8, the younger and older generations are used a lot. The object garbage collection in the young generation is based on the Scavenge algorithm. The Cheney algorithm, a garbage collection algorithm implemented by replication, is used in Scavenge. It divides the heap memory into two Semispaces, one in use (From space) and the other idle (To space). When an object is allocated, it is first allocated in the From space. When garbage collection begins, live objects in the From space are checked, they are copied To the To space, and space occupied by non-live objects is freed. After the replication is complete, the roles of the From space and To space are swapped. During garbage collection, live objects are copied between two Semispace Spaces. The younger generation can be promoted To the older generation on the basis of the Scavenge avenge and the insane memory usage.

For objects in the aged generation, there are two problems when using the above method because the proportion of living objects is large. One is that there are many living objects, and the efficiency of copying living objects will be low. The other problem, again, is the waste of half the space. For this reason, V8 uses a combination of Mark-sweep and Mark-compact for garbage collection in the old generation.

[4.5] Snapshot

When the V8 engine starts, it loads a lot of built-in global objects and sets up built-in functions like Array, String, Math, etc. To make the engine cleaner, tasks such as loading objects and creating functions are done using JS files. The V8 engine takes care of loading the incoming JavaScript code before compiling and executing it.

The snapshot mechanism is to save and serialize some built-in objects and functions in memory after loading. The serialized results can be easily serialized. The snapshot mechanism shortens the startup time. The snapshot mechanism can also serialize the JS files that the developer deems necessary, reducing the time for later processing.

[4.6] Binding and extension

V8 provides two mechanisms to extend the engine’s capabilities. The first is the Extension mechanism, which extends JavaScript capabilities through V8’s base class Extension. The second is binding, which uses IDL files or interface files to generate the binding files and then compiles them with the V8 engine code.

Practice — JavaScript optimization suggestions

[5.1] Create objects

Do not break hidden classes, try to initialize all object members in the constructor, and do not change the type later to keep the structure of the object unchanged, allowing V8 to optimize the object.

    function Point(x, y) {
      this.x = x;
      this.y = y;
    }

    let p1 = new Point(11, 22);
    letp2 = new Point(33, 44); // p1 and p2 share a hidden class p2.z = 55; // P1 and P2 have different hidden classes. The hidden class is brokenCopy the code

Initialize object members in the same order

Const obj = {a: 1} // The hidden classId was created Obj = {a: 1} const obj = {a: 1} const obj = {a: 1} const obj2 = {a: 1} Obj2.b = 3Copy the code

[5.2] Data representation

In V8, the representation of data is divided into two parts. The first part is the actual contents of the data, which are variably long, and the second part is the handle to the data, which has a fixed size and contains Pointers to the data. Why is it designed this way? The main reason is that V8 has to do garbage collection and move the data around, which can be problematic or expensive if you use Pointers directly. You don’t have these problems if you use handles, and you just need to change the Pointers in the handles.

Specific definitions are as follows:



The size of a Handler is 4 bytes (32-bit machines), the integer gets its value directly from value_ without being allocated from the heap, and then allocates a pointer to it, which reduces memory usage and increases data access speed.

So: for numeric values, try not to use floating-point numbers if you can use integers.

[5.3] Array initialization

    leta = new Array(); a[0] = 11; a[1] = 22; A [2] = 0.2; a[3] =true;Copy the code

It would be better:

letA = [11, 22, 0.2,true]Copy the code

Suggestion: Initialize small fixed-size arrays using array constants

Do not store non-numeric values in numeric arrays (objects)

Do not delete elements from arrays, especially numeric arrays

Do not load uninitialized or deleted elements

[5.4] Memory

Set variables that reference objects that are no longer used to null (a = null) and introduce the delete keyword to delete unwanted objects.

[5.5] Optimized rollback

Do not write code that triggers an optimization rollback, as this will significantly degrade the performance of your code; Do not change the object type after executing it more than once;


The article is updated every week. You can search “Front-end highlights” on wechat to read it in the first time, and reply to [Books] to get 200G video materials and 30 PDF books