Ever Wondered what happens when you write a Javascript code and run it? Either in your browser or in a runtime like Node? I mean when you run it with HTML and CSS in the browser and try to console log something then you see your console logs in the browser, but when you write console logs in a runtime like Node then you see them in the terminal?
Why there are some APIs that you can use while writing frontend like localStorage or Canvas but not while writing Backend using Nodejs, I mean both of them are using Javascript, right?
Spoiler - It's because of the Javascript engine.
So, today in this blog we are going to understand
- what is a Javascript engine?
- What is its purpose?
- How does it work?
- How Javascript runs on both client-side and server-side
What is a Javascript Engine?
So a Javascript engine is basically a piece of code that is used to convert Javascript code into machine-understandable code and execute it.
There are many Javascript engines available in different browsers for example -
Chrome - V8 Engine
Mozilla - SpiderMonkey
Safari - JavaScriptCore
... and many more
Let's understand the workings of the Javascript engine by taking the V8 engine as the base.
So we know that computers only understand binary i.e. 0's and 1's right? So we have to follow some steps before our computer can understand our code written in JS just like any other language.
So there are multiple steps before the source code written by a Developer is ready to be machine understandable. Let's go to each step and understand what's happening there.
Tokenization
When you write JavaScript code, the first step in the journey from source code to executable instructions is tokenization.
Tokenization, also known as lexical analysis, involves breaking down the source code into smaller units called tokens.
Tokens are the building blocks of a programming language and represent individual elements such as keywords, identifiers, operators, and literals.
Here is an example -
let x = 10 + 5;
Its tokenized form will look like - "let", "x", "=", "10", "+", "5", ";"
Once the given code is tokenized then the parser converts that code into an AST (Abstract Syntax Tree)
The AST (Abstract Syntax Tree) is like a map of your code's structure.
Imagine your code as a story. The AST breaks down this story into smaller pieces, like sentences and paragraphs, called nodes. Each node represents different parts of your code, like statements, expressions, or functions.
Semantic Analysis is like checking the grammar and meaning of your story.
Once we have the map (AST), we want to make sure the story makes sense. Semantic analysis is like checking the grammar and meaning of the words and sentences. It helps us catch mistakes and makes sure the story follows the rules of the language it's written in.
So, in simpler terms, the AST helps us understand the structure of the code, and semantic analysis makes sure the code is well-written and follows the rules. It's like having a guide to understand the layout of your code and a proofreader to check if your code "speaks" correctly.
So once the AST is ready the next step that comes into play is generation of bytecode.
Bytecode
So what is this byte code? Bytecode is an abstraction of machine code. Bytecode is what makes Javascript run on all platforms like Windows, Android, MacOS, etc. Bytecode is created keeping the system architecture in which it is being executed in check. What does this mean?
So whenever we download an application on our PC, there are mostly 2 versions available for that software one is for 64-bit architecture and the other for 32-bit architecture right? But why this is done? This is done because languages like C++ are compiled languages, which means that the whole code is compiled first and then an execution file is created which runs your code. So the executable file that is created after the compilation is only that machine's architecture specific. Meaning it is only optimized for that architecture. That's why most software has two versions of the same software.
But we never had this problem in opening a website on any system right? We just type in the site of the name and voilà it opens up.
This is because Javascript is platform independent, and what makes it platform independent? Bytecode
Bytecode is generated in the JIT (Just in Time) compilation.
So if I have to give a short analogy
Bytecode is like a translator who knows all the languages (meaning different architectures) and it helps the JIT (tourist) understand the place where it is (Different platforms), but the translator is always the tourist's same friend, just that it knows a lot of languages.
(If you don't understand JIT yet don't worry I'll explain it as well)
Bytecode as a Translator:
Bytecode acts as an intermediate representation or a translator that is knowledgeable about different architectures or platforms. It is designed to be platform-independent, like a translator who understands various languages.
JIT Compiler as a Tourist:
The JIT compiler is a tourist who wants to explore a new place (execute code on a specific device or architecture). The tourist, or JIT compiler, relies on the translator (bytecode) to guide and communicate with the locals (native machine code) effectively.
Portability of Bytecode:
The translator (bytecode) is versatile and can assist the tourist (JIT compiler) in any location, making the code portable across different architectures. The tourist doesn't need to learn every local language; instead, they communicate through the translator.
JIT compilation
Compilation? Compiler? So Javascript is a compiled language? No it's not, So is it interpreted language? Haha No.
Javascript is a "Just-In-Time (JIT) compiled" language, what does that even mean?
So we know how compiled languages and interpreted languages work right?
Compiled Languages
A compiled language will take the whole source and convert it to machine code at once and will return an executable file. During this phase user can do nothing but wait.
Slower Start-Up Time - So the startup time is slow because it is reading the whole source code at once, but the later executions are fast as the machine code is already created in the exe file and we can just run it.
Platform Dependency: The compiled code is often platform-specific, requiring different binaries for different architectures.
Interpreted Language
An Interpreted language will read the code line by line and execute it line by line.
Slower Execution: Interpreted code tends to run slower than compiled code since the interpreter must translate each line of code on the fly.
No Static Optimization: Interpreters cannot perform extensive static optimizations before execution.
JIT takes the best of both worlds. How?
JIT is comprised of both a compiler and an interpreter, so what happens is when JS code first comes in contact with the JS engine, it starts with the interpreter, Why? So that the execution can get started and the user does not need to wait. It starts to run the code.
While it is running the code it also keeps track of what functions or parts of code are being called again and again and marks them as hot paths. Once enough data is collected to call a function a hot path, the compiler kicks in and takes the byte code and creates a more optimized version of that bytecode, and caches it.
So the next time when the same function is called again, the interpreter does not reads it again, but the compiler sends the pre-compiled code and is executed directly.
There is also a system of fallback in the compiler, so if there are some changes detected in the cached function or code, then that cache is not used and the interpreter re-executes the code.
What are these changes that are detected?
So you know how sometimes we have a function that takes in a value, now lets say when the hot path was created till then an integer value was being passed in that function, but now suddenly a string is passed into that function, this causes in the change of bytecode as the data type is changed and hence the same optimized bytecode cannot be used. This is also why it is advised to keep type safety in your code and not change it again and again because JIT won't be able to create optimized byte code otherwise.
In the V8 engine, the interpreter is known as ignition, the Compiler is known as TurboFan and the system that tracks the hot paths is called profiler.
Here is a diagram which explains the above process
Now that we understand what is the purpose of the JS engine and how it works, there is just one thing left, how does JS run on servers? Like Node.js?
So Node.js acts as a wrapper around V8, providing additional features and modules for server-side development.
Node.js provides an additional layer of functionality and modules on top of the V8 engine to enable server-side development.
Node.js includes its own event loop, which is responsible for managing asynchronous operations. This event loop allows Node.js to efficiently handle multiple concurrent connections without blocking the execution of code.
The wrapper provides abstractions for working with non-blocking I/O operations, making it easier for developers to write asynchronous code.
Node.js includes a set of built-in modules and APIs for common server-side tasks, such as file system operations, networking, HTTP handling, and more.
These built-in modules, like fs for file system operations and HTTP for creating HTTP servers, are part of the wrapper that Node.js provides.
Here is a summary of what we understood today
JavaScript Engine Fundamentals:
A JavaScript engine, such as V8, serves as the powerhouse behind code execution, translating JavaScript code into machine-readable instructions.
Browser Environment:
In the browser, JavaScript interacts with APIs provided by the browser environment, including the DOM API, Web APIs, and more. This enables dynamic and interactive web pages.
Node.js on the Server:
Node.js acts as a wrapper around the V8 engine on the server side, extending JavaScript capabilities for server-side development. It introduces features like event-driven architecture, a CommonJS module system, and built-in APIs for server tasks.
Compilation Process:
The journey from source code to execution involves tokenization, AST creation, and semantic analysis. The Abstract Syntax Tree acts as a map, guiding the understanding of code structure.
Bytecode and JIT Compilation:
Bytecode serves as an intermediate representation, making JavaScript platform-independent. JIT compilation optimizes hot paths dynamically, combining the benefits of both compiled and interpreted languages.
Thanks for reading this far😁.