JavaScript Engines Performance

JavaScript Engines Performance

As you may already know Javascript is a Just-In-Time (JIT) compiled language, which means that the code is compiled at runtime, rather than being compiled ahead of time. This makes JavaScript highly flexible. However, this has the side effect of placing the burden of compiling the code onto the client machine, rather than the server. This means that the performance of the code can vary greatly depending on the power and resources of the client machine.

Javascript Compilation Process

There are numerous engines used for executing JavaScript, including V8 for Chromium, Node, and Electron; SpiderMonkey for Firefox; JavaScriptCore for Safari, and many others. However, most engines follow a similar process for compiling code.

First, the code is parsed into an abstract syntax tree (AST), which is then processed by the compiler to create machine code that can be executed. To further improve performance, many engines also have a secondary optimizing compiler that creates highly optimized code. During execution, the engine switches between the optimized and non-optimized versions of the machine code, depending on the specific use case.

Each JavaScript engine has its own implementation and names for different parts of its compilers. For example, in the V8 engine, the baseline compiler is referred to as "Ignition," while the optimizing compiler is called "TurboFan." In the SpiderMonkey engine, the baseline compiler is known as the "Baseline Interpreter" and the optimizing compiler is called "WarpMonkey." And, in the JavaScriptCore engine used by Safari, has two baseline compilers named start-up interpreter LLInt and baseline JIT, and two optimizing compilers a low-latency optimizing JIT (DFG) and a high-throughput optimizing JIT (FTL).

Two key parts in this compilation process are important for us the parsing stage and the optimizing compilation stage. As developers, we want our code to be parsed quickly and compiled into optimized machine code as frequently as possible for better performance.

Parser

The parsing stage can often be the slowest part of the compilation process, particularly on mobile devices where it can be as low as 1MB/s. The parsing stage consists of two subparts: Eager parsing and Lazy parsing. Eager parsing occurs when the parser encounters code that is immediately required for execution. And, Lazy parsing is a lightweight process that only identifies the existence of code without processing it.

const name = 'Mark'; // Eager
(function foo())() // Eager
function bar() {...} // Lazy
class car {} // Lazy

In this example bar() is lazily parsed initially, but later when we invoke this function it will be Eagerly parsed.

There are primarily three ways to optimize the parsing stage:

The first way is to minimize the amount of code that needs to be parsed. This can be achieved through code minimization techniques such as minification, tree shaking, and dead code elimination. (more on this in the Network part of the series)

The second way to optimize parsing is to reduce the number of jumps between Eager and Lazy parsing to streamline the process. What this means is if you made a parser Lazily parse something and then immediately used it will make it go over it a second time and parse it again but now Eagerly.

function foo() {} // Lazy parse
foo() // Go back and parse foo again Eagerly

This can be fixed using IIFE then the function will be parsed on the first parsing run.

(function foo() {})() // Eager

The third way to optimize the parsing process is by reducing the use of nested functions. When code includes nested functions, the parser can encounter issues with context, such as needing to parse a function partially in the Eager mode because it requires access to a variable from its parent context. This can result in slower parsing speeds that fall somewhere between the Eager and Lazy modes. By minimizing the use of nested functions, we can help streamline the parsing process and improve its performance.

function getNameMaker() { // Lazy
    const name = 'John'
    function getName() { // Lazy-ish
        return name;
    }
    return getName;
}
const getName = getNameMaker();
getName() // John

Optimizer

Hidden Classes

Javascript is a dynamically typed language but compilers need type to make code run fast. This is why every engine implements some type of type, v8 they are called Maps, in SpyderMonkey Shapes and JavascriptCode Structures. However because in JavaScript you can change objects at any time and however you want, these types are different from types in other languages. When an object is created it is assigned a type, when an object is changed it is assigned a new type and so on. What this means is that types of objects are defined by their lifecycle.

const obj1 = { a: 1 }; // Type1
obj1.b = 2; // Type2
const obj2 = { a: 1, b: 2 } // Type3
// Hidden Type of obj1 != obj2 because their life pathes are different.

You can compare object types using %HaveSameMap(obj1, obj2) and node --allow-natives-syntax filename.js

Hidden classes are important because they speed up the process of creating and interacting with objects. You can think about them as blueprints, initially engine doesn't know how to create obj3, what properties it has, where to store them and so on, thus it created a hidden class in our blueprint. then when it stumbles on obj4 sees a similar structure so instead of figuring everything out again it just assigned it to the same blueprint and uses it.

I.e. when obj3 was created it was decreased that this class has property a stored at address (address of obj3 + 17) and property b stored at address (address of obj3 + 39). so then when we do obj4.a because obj4 is of the same type as obj4 engine knows that a will be at (address of obj4 + 17).

One more thing to mention is regarding classes and functions, don't put class declarations in functions. because on every call you will be re-calling function declaration so on each call it will have a different hidden class.

Compared to having class declaration outside of a function

Specialization / Speculative Optimization

Type specialization also known as speculative optimization, is a technique that uses types to specialize functions to work specifically with them. Initially, functions are metamorphic they can work with any type of parameters (e.g. string, array, number, etc.) but when we call this function multiple times and always use same types as parameters function becomes monomorphic (take only one type, e.g. numbers) or polymorphic (take two types, e.g. number and string).

Over program run time at some point compiler notices that a certain function is called over and over with the same type of parameters, at this point compiler goes through the process of speculative optimization to make this function run faster.
(function add will get speculatively optimized so I used %NeverOptimizeFunction to prevent it from happening)

Comper to the speculatively optimized version

What we can take from this is by making our function monomorphic or at least polymorphic we increase the chances of them being optimized using speculative optimization. This is easier to achieve if you are using TypeScript or Flow.

There are numerous additional optimization techniques implemented by JavaScript engines, including Inlining, Loop optimizations, Dead code elimination, and Global Value Numbering (SpyderMonkey), among others. However, discussing them in detail is beyond the scope of this conversation, because we can't leverage them in any meaningful way.

Engines
JavaScriptCore – WebKit
Introducing the WebKit FTL JIT | WebKit
Launching Ignition and TurboFan · V8
Docs | SpiderMonkey JavaScript/WebAssembly Engine
SpiderMonkey — Firefox Source Docs documentation (mozilla.org)

Types
SpiderMonkey is on a diet – Nicholas Nethercote (mozilla.org)
Maps (Hidden Classes) in V8 · V8

Parser
Blazingly fast parsing, part 2: lazy parsing · V8

Speculative Optimization
https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8

Maglev (not yet relevant?)
https://docs.google.com/document/d/13CwgSL4yawxuYg3iNlM-4ZPCB8RgJya6b8H_E2F-Aek/edit#