One of my favorites definitions for javascript is: A High-Level, Single Threaded Garbage-Collected Interpreted or Just-in-time Compiled Prototyped-Based Multi-Paradigm Dynamic Language With a Non-Blocking Event Loop Concurrency Module.
Yes I know it’s a mouth full and in this article we will explore some of these terms to understand the inner workings of javascript and why it is often praised for its versatility as a language with a multifaceted internal structure and what separates it from its counterparts.
The JavaScript Engine: Unveiling the Magic
At the core of JavaScript lies the JavaScript engine, a sophisticated piece of software that transforms our human-readable code into machine-executable instructions. We will learn about the components that make up the engine and why it is a powerhouse for web development.
In the realm of programming, the translation of human-readable code to machine language can be approached in two main ways: through interpreters and compilers, each with its own set of advantages and trade-offs.
Interpreters: Reads Code Line by Line
The interpreter reads the code line by line on the fly making it very fast for smaller codebases. In the early days of JavaScript, interpreters were the go-to method, because they are efficient in getting the code up and running fast without the need for an additional conversion step.
However, as the codebase grows, interpreters start showing their limitations. When executing sizable JavaScript files, especially in scenarios where the same code is run repeatedly, such as within a loop, the interpreter’s performance can degrade significantly with a noticeable drop in the speed of execution, making interpreters less suitable for handling larger and more complex applications.
Compilers: Transforming Code into Optimized Magic
Compilers on the other hand take a different approach. They try to understand the entire codebase, compiling it down to a lower-level language before execution. While compilers require more time to initiate because of the code understanding and compilation process, they make up for it by optimizing the code and improving the overall performance.
This optimization step done by compilers contributes to the creation of faster and more efficient programs. By analyzing the entire codebase, compilers can identify areas of the code that can be optimized, resulting in streamlined and highly performant machine code.
The Evolution: Just In Time (JIT) Compilers
JavaScript has evolved to incorporate a hybrid approach by combining the strengths of both interpreters and compilers; it now uses Just-In-Time (JIT) compilers, a combination that aims to leverage the best of both worlds.
When the engine loads a new javascript file, initially the code is sent off to the interpreter (also called the ignition in chrome’s v8 engine) which produces some byte codes to run with – note that javascript engine can understand bytecode.
The profiler or monitor watches the codes as it runs, and makes note of how to optimize it.
As the code is running, the interpreter passes some codes to the compiler, which in turn makes optimization on the codes sent to it so that it runs faster, and then replaces the sections where it has improved on the bytecode with optimized machine code, it constantly runs through this loop.
This result is a language that adapts to the needs of varying code sizes and complexities, delivering efficient execution in order to meet the demands of modern web development.
Components of JavaScript Engine: Call Stack and Memory Heap
Javascript uses two key components in managing memory: the Call Stack and the Memory Heap. These components work together to manage memory allocation and code execution, ensuring a seamless and efficient performance.
Memory Heap: Where Memory Finds a Home
The Memory Heap is an important space where memory allocation takes place. This is the area where the JavaScript engine reserves memory for variables and objects during the runtime of a program.
Consider the following expression:
javascript
const number = 60;
In this simple assignment, we instruct the JavaScript engine to allocate memory for the variable number and assign it the value of 60. This memory allocation happens in the Memory Heap, creating a dedicated space for the variable to store its data.
Call Stack: Tracking the Code Execution
Callstack is a region in memory that allows us to know where we are in an ongoing execution of code and it operates on a first-in-last-out (FILO) basis. It stores functions and variables as the code executes.
Simple variables (primitive data structures) like numbers and strings can be stored on the stack, while complex data structures like objects, arrays and functions are stored in memory heaps.
Note that whatever is on top of the call stack is what javascript is actively executing.
When functions are invoked, they are pushed onto the stack, creating a stack frame for each. When a function completes its execution, it is popped off the stack. This process allows the engine to keep track of the program’s execution flow, indicating exactly where the code is at any given moment.
Execution Pipeline
The journey from a JavaScript file to the execution involves a series of steps: parsing the file, creating an Abstract Syntax Tree (AST), interpretation, and generating bytecode.
The interpreter takes the lead, producing bytecode for execution. Meanwhile, the profiler monitors code execution, passing information to the JIT (Just-In-Time) compiler for optimization, creating a continuous loop of optimization during runtime.
JS File ==> Parser ==> AST (abstract syntax tree) ==> Interpreter ==> Bytecode.
Interpreter ==> Profiler ==> (JIT)Compiler ==> Optimized code.
Shoutout to Andrei Neagoie guide for this engine representation.
Single-Threaded Synchronicity
Recall our initial definition of Javascript as a High level Single threaded language, this indicates the synchronous nature of its code execution. In a single-threaded environment, only one set of instructions is executed at any given moment. Picture it as a solo performer on a stage, delivering a seamless act without any interruptions.
This is so because the Javascript engine has just one call stack that allows the code to run in a linear fashion — one task at a time.
The call stack is a fundamental data structure in the single-threaded JavaScript environment. It records the position in the program, tracking the functions that are currently in progress. As functions are invoked, they are pushed onto the stack, and when they complete, they are popped off the stack.
This synchronous execution ensures that operations are carried out in the order they appear in the code, creating a predictable and orderly sequence of actions. While this simplicity is advantageous in many scenarios, it also introduces challenges, especially in handling time-consuming tasks that could potentially block the execution flow.
Beyond the JavaScript Engine: The Role of the JavaScript Runtime
The Javascript engine is synchronous and single threaded with only one stack and one heap. This means that if any other program needs to execute, it has to wait until the previous program or function is completed.
Luckily for us it’s not just the javascript engine that is running codes, the javascript Runtime comes into play.
Javascript Runtime comprises of:
- Javascript Engine (Callstack & Memory Heap),
- Web API,
- Event loop &
- Callback Queue.
The Web API comes with the browser (chrome, firefox etc), it’s not native to javascript,
and have functions that can do a variety of things e.g. send http requests using the fetch() method, listen to dom events, delay execution e.g. using the setTimeout function etc.
Now the web browser uses the web API to communicate with the javascript engine.
Remember that the Web API is not part of synchronous native javascript, so the browser is working on the background with a different thread from the native javascript.
These Web APIs are what we call Asynchronous, because we can instruct them to do something in the background and return data once its done, while the javascript callstack continuous working and executing functions
The Fun Part: Asynchronous JavaScript
JavaScript’s asynchronous nature introduces additional components to the runtime environment, expanding its capabilities. Let’s take a closer look at these elements.
- Event Loop:
- The event loop plays a pivotal role in JavaScript’s non-blocking behavior. It continually checks the call stack and callback queue, ensuring the efficient handling of asynchronous operations.
- Callback Queue:
- Callbacks, which can be functions invoked by another function or returned from asynchronous operations, find their place in the callback queue, waiting for their turn to enter the call stack.
- Web API:
- While not inherently part of the JavaScript engine, Web APIs such as setTimeout, DOM, and XMLHttpRequest extend the language’s capabilities. Browsers provide these APIs to facilitate interaction with the environment, enhancing the language’s utility.
Putting it all together, the javascript runtime works like this:
We have items on the callstack running synchronously, but as soon as something comes up that is not part of the native javascript eg, setTimeout which is actually a Web API, the callstack pushes it to the Web API to handle it.
The Web API receives the command from the callstack and runs it in the background. Once it’s done, the Callback queue gets notified with the callback function on what to do next.
Finally, the Event loop checks when the callstack is free and adds the callback function back to the callstack for execution.
Consider the codes below:
console.log(‘1’); setTimeout(() => {console.log(‘2’)} , 1000); console.log(‘3’); |
When we run this, we get the result as: 1, 3, 2 as our output. What is happening is this:
the code runs line by line, so first we add the first line console.log onto the stack, it runs and produces 1, and is then removed from the callstack, next we get to the second line, this is also added to the stack, but then the javascript engine notices it is a web api call, removes it from the callstack and passes it to the Web api, and then finally the stack moves to the last line and executes it and pops it out of the callstack.
On the Web API side of things, a timer is started according to the time value, once the timer count finishes, it’ll push the callback function of the setTimeout function to the callback queue.
The Event loop is constantly checking when the callstack is empty so it can push callbacks on it.
Note that the Event loop only runs when callstack is empty and the entire javascript file has been read, at least once. This means that, no matter how fast the web api response, the Event loop can only push the callback to the callstack when the callstack is empty, hence when it has console.log(‘3’) and pop its off the stack
Live Demonstration: Visualizing the Magic
For a better understanding of the call stack, callback queue, event loop and web apis in action, a live demonstration is available here.
Loupe by Philip Roberts
Conclusion
As we understand the inner workings of JavaScript, we gain insights that empower developers to write more efficient and optimized codes. The unique architecture of JavaScript, combining synchronous and asynchronous elements, is what makes it a dynamic and powerful language for web development. Equipped with this knowledge, let’s continue our coding journey, harnessing the magic that JavaScript brings to the world of the web. Happy coding!