A Peek Under The Hood of How Modern JavaScript Runs
Tags: javascript, computerscience, webdev
Published: 2020-06-03 21:36:54

You write some JavaScript code. You open the browser. You see what you wrote come to life. Seems like magic, right?

But, of course, we know magic doesn't truly exist - behind every framework, library, array method et cetera are the gears that turn the wheel. Let's take a look into the inner workings of how JavaScript is executed in modern browsers.

First, let's understand the difference between a compiled language and an interpreted one. In both cases, the goal is to take a human-readable language and translate it into machine language. Keep in mind that these are simplified, general distinctions. There are more nuanced differences, even within distinctions themselves. It's worth noting that a compiler can be written for any language.

Before being executed, a compiled language goes through a preliminary step - a "build" step. All of the code is translated at once and then it can be run on the target machine. For this reason, a compiled program can *only* be run on the machine it was compiled for - the translation for MacOS ≠ the one for Windows. Although this seems like a downside, compiled languages often result in a faster program and a lot of trivial bugs can be caught at compile time as opposed to run time. A few examples of compiled languages are Java, C, and C++.

Interpreted languages, on the other hand, are interpreted line-by-line at run time by an interpreter. This allows for a little more flexibility - as long as the machine has an interpreter for the language installed, it'll be able to run. As a downside, programs in interpreted languages can be less performant and more susceptible to bugs. A reason for the former is that in compiled languages, some re-used code (such as a function) only needs to be translated once whereas an interpreter will re-translate. Some examples of interpreted languages are Python, Ruby, and our old pal JavaScript.

JavaScript, our little web workhorse, uses a hybrid method in modern JS engines. I know what you may be thinking — didn't I just list it as an interpreted language? Well, it still is, but we programmers like our efficiency. As mentioned above, a compiler can be written for any language. In comes the JIT. 

Please note that these explanations are **very simplified**. I highly recommend further reading if you want to learn more about what happens under the hood (sources listed below).

JIT stands for [just-in-time compilation](https://en.wikipedia.org/wiki/Just-in-time_compilation). In a similar vein as being interpreted line-by-line on the fly, the code is compiled as the program is running, rather than going through a full compilation step before being executed. To go further, as opposed to the all source code being translated & optimized into the target's machine code, JIT compilation goes through several steps to optimize the code as needed, resulting in programs that are fast to start and retain optimization as they run. Code is first simply interpreted and, during execution, "warm" segments that are run several times are sent to a base compiler and "hot" segments that are run a significant number of times are sent to an optimized compiler.

While that is the basic idea, the nitty gritty of JavaScript engines are more advance and utilize different concepts in different places. I'm going to use [Chrome's V8 engine](https://v8.dev/) (post 5.9 with [Ignition](https://v8.dev/docs/ignition) & [Turbofan](https://v8.dev/docs/turbofan)) as a baseline, but keep in mind that other browsers' engines have slight differences in implementation. For a brief introduction, V8 is an open-source JavaScript engine written in C++ that compiles JavaScript to optimized machine code.

First, the source JS code goes through a parsing step that creates an Abstract-Syntax-Tree ([AST] (https://en.wikipedia.org/wiki/Abstract_syntax_tree)) representation of the code. The AST is then interpreted into unoptimized bytecode (Ignition's responsibility) and executed. This allows programs to get up and running quickly. The bytecode is then selectively optimized by Turbofan based on feedback from Ignition. The specifics of how the optimizations are made can be difficult to explain succinctly. However, as with all optimization, at the heart it aims to reduce unnecessary repeated actions and generally improve efficiency of tasks.

Gaining a deeper knowledge of JavaScript engines can lend to better program design and practices to optimize efficiency. For further reading, take a look at these resources:

- [Interpreted vs. Compiled Languages](https://kb.iu.edu/d/agsz)
- [Crash Course in JIT Compilers](https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/)
- [How JavaScript Works / optimizing for the V8 Engine](https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency/)
- [An Introduction to Speculative Optimization in V8](https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8)

Again, a lot of this explanation simplified for sake of brevity. Let me know if there are any concepts that could benefit from being more in-depth!