Frontend Myths Exposed: JavaScript’s Compilation Unveiled

Rafał Opacki
Jit Team
Published in
10 min readJan 16, 2024

--

Welcome to the inaugural installment of our series on Frontend Myths! Throughout this series, I’ll be tackling prevalent misconceptions that often circulate within Frontend development environment. Join me as I unravel these myths, providing comprehensive explanations to uncover the truth behind each one.

Introduction

In this article, I would like to start with a brief introduction to the fundamental concepts of interpretation and compilation, providing clear definitions of these processes and highlighting the key differences between them. I would also like to offer a short overview of JavaScript engines, the software responsible for executing JavaScript code. Later, we will dive into the core subject of the article, which is a thorough exploration of whether JavaScript should be classified as an interpreted or compiled language.

The programming community often encounters misconceptions regarding JavaScript’s processing characteristics, leading to incorrect or incomplete information being disseminated. In this article, my goal is to challenge and debunk these myths, offering a precise and well-founded perspective on that matter. By the end of this discussion, you will gain a complete understanding of how JavaScript truly operates and you will also be better equipped to discern fact from fiction, in this ongoing debate.

Compilation vs Interpretation

When operating within the context of programming language processing, compilation describes the process of converting the code from a human-readable high-level language, like Java, into a lower-level format output, also known as the “machine language” or “machine code”. This lower-level form is directly executable by the computer’s central processing unit (CPU) — the “processor”. The tool used for this transformation is called a “compiler”. While the compilation process involves multiple steps, our primary focus is on the essential function of translating high-level code into a format that a computer can execute in an efficient manner. [1]

Interpretation is a process carried out by the interpreter, which uses high-level, human-readable code as input and executes it, statement by statement. Unlike compilation, interpretation does not involve the generation of a separate low-level output (“machine code”). Instead, it directly analyzes and executes the source code. In some cases, an intermediate-level form of the processed code, also known as “bytecode”, is generated and executed. The interpretation shares some common steps with the compilation process, making it a complex process in its own right. [1]

What are the main differences between those two processes? First of all, the result of the compilation process is the output that is executable by the CPU, on the other hand, interpretation means running the code by interpreter without the output machine code. Compiled code is also faster — it is ready-to-go and doesn’t require any further actions or processing after compilation. Interpreted code may be slower, due to the fact that each sentence has to be processed. Even if there are many of the very same sentences — without any optimization techniques applied, each and every one of them will require the same processing steps to be executed, again and again. Interpretation offers the advantage of immediate code execution, bypassing the necessity for an initial processing phase. It promptly translates and executes the code, providing real-time results. In contrast, the compilation process entails a time-consuming phase before code execution begins.

JavaScript Engines

JavaScript code is executed by a component known as the JavaScript engine. These engines play a fundamental role in various JavaScript runtime environments. For the sake of our case, we would focus on browsers. In popular browsers like Chrome and Edge, the V8 engine handles JavaScript execution, while Firefox relies on SpiderMonkey, and Safari uses JavaScriptCore. The interpreter is the heart of every engine, but it’s not the only component. In this article, we’ll primarily focus on V8 and SpiderMonkey, as they are among the most widely used engines in the browser context.

What do you think?

Let’s embark on a journey into the intricate world of JavaScript’s processing. To begin, I’d like to test your thoughts on whether JavaScript should be classified as an interpreted, or a compiled language, based on the definitions provided earlier. Join me in this exploration, and by the end of this discussion, we’ll reveal the answer. Feel free to share your initial bet, and let me know if the final revelation surprises you!

JavaScript Origins

Let’s dive into the early days of JavaScript code processing. It’s common knowledge, spread even on popular course platforms and blogs, that JavaScript is an interpreted language, and that’s true, but it’s a partial truth. As mentioned earlier, the engine is the tool responsible for executing JavaScript code. In the first web browser that could handle JavaScript, Netscape Navigator 2.0 (1995), the engine didn’t have a specific name, but it was commonly referred to as “Mocha” (same as the first name of JavaScript). Notably, Brendan Eich, the creator of JavaScript itself, was the creator of the first JavaScript engine. Back then, roughly 30 years ago, that engine was an interpretation-only engine, so the definition of JavaScript as an interpreted language was entirely accurate. Interestingly, this engine generates the “intermediate form” (“bytecode”) mentioned earlier, which is subsequently executed by the interpreter. [2]

The second JavaScript engine, SpiderMonkey (which shares its name with the current Firefox engine), made its debut in 1996. In its early days, it too was also an interpretation-only tool. [2] However, SpiderMonkey has played a crucial role in the processing of JavaScript code. Now, let’s fast-forward to the year 2008.

JavaScript Evolution — part one

We had to wait for more than a decade, until 2008, to witness the introduction of the first compilation tools in the aforesaid JavaScript engines. During that year, significant developments occurred in the world of JavaScript engines. SpiderMonkey introduced TraceMonkey, a Just-In-Time (JIT) compiler, which was a revolution in JavaScript execution. [3] Simultaneously, the V8 Engine was launched, and right from the very start, it also implemented a JIT compiler. [4] It’s hard to determine which one was the absolute first, but considering their release dates, TraceMonkey became available in August, and V8 with its unnamed JIT compiler, followed on, in September 2008.

These developments were a turning point in JavaScript’s performance and opened a new set of possibilities for web development. In 2008, JavaScript evolved into a language that could be both interpreted and compiled, which reshaped its capabilities and performance.

Thanks to JIT Compilation, the execution of JavaScript code in the SpiderMonkey engine became more than 20 times faster. [3] This dramatic improvement in execution speed was a direct result of the Just-In-Time compilation technology.

JIT Compilation

Before we transition to the modern era, let’s delve further into the concept of JIT (Just-In-Time) compilation. JIT compilation is a versatile mechanism employed across programming languages, transcending its use beyond JavaScript engines. The timing of JIT compilation can vary, depending on the implementation, occurring either before or after the code is executed. JIT compilation assumes a critical role in the optimization of code, addressing a fundamental shortcoming of interpretation: the redundant processing of identical code segments whenever they appear. By dynamically optimizing and compiling code on-the-fly, JIT compilation yields a substantial boost in code execution speed. The result of JIT compilation is machine code, identical to what is produced through traditional compilation processes. [5]

JavaScript Evolution — part two

In the following years, significant advancements occurred in the JIT compilers, of both SpiderMonkey and V8, leading to dramatic improvements in the code execution speed.

In 2010, Firefox’s TraceMonkey was succeeded by JägerMonkey, representing the second generation of the Firefox JIT compiler. Later, in 2012, IonMonkey was introduced as a replacement. These upgrades resulted in notable speed enhancements, with code execution improvements ranging from 7% to 26%. [6]

V8 also underwent a major transformation. The earlier, unnamed V8 Compiler was replaced by Crankshaft in 2010, a sophisticated code-processing infrastructure that combines various JIT compilers. This change effectively doubled the code execution speed. [7, 8]

JavaScript Nowadays

JavaScript has matured over nearly three decades, and the integration of Just-In-Time (JIT) compilation has become a fundamental element in its code processing within JavaScript engines. Notably, engines like SpiderMonkey and V8 have undergone significant evolution.

In SpiderMonkey, a pivotal moment occurred in 2020 with the introduction of the fourth-generation JIT compiler, named “WarpMonkey”. This marked a milestone in enhancing JavaScript’s performance. WarpMonkey, while noteworthy, is just one component of SpiderMonkey’s sophisticated toolset. The engine employs various interpreters and JIT compilers, each with specific roles in managing different compilation processes during code execution. This multifaceted approach significantly contributes to the engine’s overall efficiency. [9]

Meanwhile, V8 has undergone significant transformations. The latest iteration of V8 boasts the integration of the “TurboFan” compiler, supplanting the former “Crankshaft” compiler back in 2017. [10] Moreover, this evolved V8 implementation now includes not just one but two additional compilers: “Sparkplug” introduced in 2021 [11], and the recently launched “Maglev” JIT Compiler, unveiled in December 2023. [12] Quite impressive for a language often deemed “interpreted”, wouldn’t you say?

The technical implementations of these engines and these changes are not our focus at the moment. Instead, I would like to highlight the remarkable progress we’ve witnessed. We’ve seen a transformation from interpreted-only engines to JIT-compiled engines, which has significantly enhanced the complexity and readiness of JavaScript execution for handling even the most complex code.

Experiencing JavaScript Optimization

Having explored the brief history of JavaScript code transformation, you might be curious about experiencing these optimization mechanisms firsthand. Surprisingly, it’s quite simple to achieve. All you need is an IDE like VS Code, Node.js (which employs the V8 engine, akin to Chrome’s), and a function that involves substantial mathematical operations. However, let’s begin with a straightforward function to establish a benchmark.

// test.js file

function doSomething() {
let result = 0;

for (let i = 0; i < 1000; i++) {
result += Math.sqrt(i);
}

return result;
}

console.time('doSomething()');
doSomething();
console.timeEnd('doSomething()');

// doSomething(): 0.113ms

Surprisingly straightforward, isn’t it? Running this code with Node.js (via node test) will provide a fairly accurate execution time for the function. Now, let’s up the ante by adding another function call and running it again:

// test.js file

function doSomething() {
let result = 0;

for (let i = 0; i < 1000; i++) {
result += Math.sqrt(i);
}

return result;
}

doSomething(); // new function call

console.time('doSomething()');
doSomething();
console.timeEnd('doSomething()');

// doSomething(): 0.095ms

Something intriguing unfolded: despite having two calls of the same function, the traced execution time of the function with two calls was unexpectedly shorter than a single function call. Is it magic or perhaps an optimization due to the function being called twice?

Let’s delve deeper. This time, we’ll explore a more complex example. We’ll stick with two function calls and use a flag to investigate any optimizations occurring in our code. Here’s the code snippet for this example:

// test.js file

function doSomething() {
let result = 0;

for (let i = 0; i < 10000; i++) {
result += Math.sqrt(i);
}

return result;
}

doSomething();

console.time('doSomething()');
doSomething();
console.timeEnd('doSomething()');

// doSomething(): 0.816ms

Now, let’s introduce our flag ( --trace-opt). Running the code using node --trace-opt test will reveal something you might recognize:

[marking 0x3956f65fe051 <JSFunction doSomething (sfi = 0x3eb6c8b3d409)> for optimized recompilation, reason: small function]
[compiling method 0x3956f65fe051 <JSFunction doSomething (sfi = 0x3eb6c8b3d409)> (target TURBOFAN) using TurboFan]
[optimizing 0x3956f65fe051 <JSFunction doSomething (sfi = 0x3eb6c8b3d409)> (target TURBOFAN) - took 0.250, 0.750, 0.000 ms]
[completed optimizing 0x3956f65fe051 <JSFunction doSomething (sfi = 0x3eb6c8b3d409)> (target TURBOFAN)]
doSomething(): 0.709ms

Amidst the flurry of information, let’s zero in on key terms like “compiling”, “optimizing” and “TurboFan” previously mentioned in this article. You’re familiar with TurboFan, the V8 engine’s JIT compiler responsible for optimizing code, and now, you’ve witnessed its work, solidifying the argument that JavaScript is indeed a compiled language.

I encourage you to conduct a similar exercise on your own code. Witness firsthand what unfolds beneath the hood of V8’s optimization mechanisms.

Summary

In summary, it’s accurate to describe JavaScript as an interpreted language, given its inherent interpretive nature, which was its primary mode of processing in its early days. However, the modern JavaScript ecosystem has seen a significant transformation. The set of powerful tools, such as JIT (Just-In-Time) compilers, has become an integral part of this ecosystem. Over the years, these JIT compilers have continuously evolved, employing various optimization techniques to dramatically enhance JavaScript’s execution speed. They have proven to be inseparable from the JavaScript landscape, enabling the development of highly efficient and complex applications while preserving the language’s adaptability and versatility.

References:

[1] Introduction to Compilers and Language Design
https://www3.nd.edu/~dthain/compilerbook/compilerbook.pdf
[2] JavaScript: The First 20 Years
https://dl.acm.org/doi/pdf/10.1145/3386327
[3] TraceMonkey: JavaScript Lightspeed
https://brendaneich.com/2008/08/tracemonkey-javascript-lightspeed/
[4] Welcome to Chromium
https://blog.chromium.org/2008/09/welcome-to-chromium_02.html
[5] A Brief History of Just-In-Time
https://dl.acm.org/doi/10.1145/857076.857077
[6] IonMonkey if Firefox 18
https://blog.mozilla.org/javascript/2012/09/12/ionmonkey-in-firefox-18/
[7] Celebrating 10 years of V8
https://v8.dev/blog/10-years
[8] A New Crankshaft for V8
https://blog.chromium.org/2010/12/new-crankshaft-for-v8.html
[9] Warp: Improved JS performance in Firefox 83
https://hacks.mozilla.org/2020/11/warp-improved-js-performance-in-firefox-83/
[10] Launching Ignition and TurboFan
https://v8.dev/blog/launching-ignition-and-turbofan
[11] Sparkplug — a non-optimizing JavaScript compiler
https://v8.dev/blog/sparkplug
[12] Maglev — V8’s Fastest Optimizing JIT
https://v8.dev/blog/maglev

--

--