DEV Community

Cover image for Understanding JavaScript Execution with some Pizza
Anshuman Mahato
Anshuman Mahato

Posted on • Edited on • Originally published at anshumanmahato.me

Understanding JavaScript Execution with some Pizza

Welcome back, fellow developers! I'm excited to have you here for the next part of our JavaScript deep-dive series. The previous article explored the fundamental concepts and core features that make JavaScript a powerful language. Don't worry if you missed it - you can catch up here.

I was all set to explore JavaScript code execution. Now that I am here, all of a sudden, I am craving some homemade Pizza. You know what? How about we do both? After all, both coding and cooking are about following a recipe, executing steps in the right order, and creating something amazing from basic ingredients.

Today, we will unravel one of JavaScript's most intriguing aspectshow JS code is processed and executed. We'll peek behind the curtain to understand what happens when our JS code executes while baking a cheesy corn pizza.

So preheat your brain (and maybe your oven), and let's dive into this tasty technical adventure!

a teenage mutant ninja turtle is holding three pizzas and saying it 's pizza time ..

The JavaScript Engine

The execution of JavaScript code is handled by a program known as the JavaScript Engine or JS Engine. Just like Java requires JVM, JavaScript requires a JS Engine. All browsers and any other environment that executes JavaScript have a JS Engine, with Google's V8 Engine leading the pack as the powerhouse behind Chrome and Node.js. Firefox uses SpiderMonkey, Safari runs on JavaScriptCore, and several others.

The JS Engine has two key components - the Call Stack and the Heap. The Call Stack is where the code executes. On the other hand, the Heap is an unstructured memory space used for storing objects required by the code during execution. Think of JavaScript Execution as baking a pizza. The Cooking Area is your Engine, and the Cooking Counter is your Call Stack. The space over where you keep your ingredients is the Heap.

Diagram illustrating a JavaScript engine with two main sections: the Heap and the Call Stack. The Heap contains various colored blocks representing objects in memory, while the Call Stack includes stacked rectangles labeled as execution contexts.

Before you begin with the pizza, you have to prepare the ingredients. You cannot put them directly into the oven. You chop the vegetables, prepare the dough, grate the cheese, etc. Similarly, before the engine can execute the code, it has to be processed and converted to a machine-understandable form. It must speak the computer's language - machine code.

Earlier, we saw how JavaScript uses a clever hybrid approach called JIT Compilation, combining the best of compilation and interpretation. While I prepare the toppings for my pizza, let's peek under the hood and see how JIT Compilation inside the JS Engine.

Just-in-Time Compilation

When code first arrives at the engine, something fascinating happens. The engine starts breaking down your code into meaningful pieces. It parses the code to segregate tokens holding some meaning to JavaScript, e.g., 'const', 'var', and 'for'. But it doesn't stop there. The engine then transforms these pieces into an Abstract Syntax Tree (AST) - a structured way for the engine to understand your code's intent. Let's take a simple example: for the line const a = 10;, here's what the AST looks like.

{
  "type": "Program",
  "start": 0,
  "end": 13,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 13,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 12,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 12,
            "value": 10,
            "raw": "10"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

Enter fullscreen mode Exit fullscreen mode

Don't worry too much about the details of AST for now - but if you're curious to dive deeper, check out this article.

The parsing phase does more than create the AST it's your code's first quality check, handling transpilation, linting, and ensuring your syntax is spot-on. Once the engine parses the code to AST, it compiles this AST to machine code, which is then immediately put into the Call Stack for execution. Here's where it gets interesting. Initially, the engine generates a rough, unoptimised version of the machine code to get things up and running as fast as possible. Then, while your code is executing, it continuously works in the background to optimise this code for better performance.

Just in Time Compilation

While different JavaScript engines might handle these steps in their own ways, this is basically how Just-in-Time Compilation works in JavaScript. It's a clever balance between speed and optimisation.

Now that my toppings are ready, its time to bake the pizza. Lets do that alongside learning how the engine executes the compiled code.

Pizza toppings

Execution Context

Once the code is parsed and compiled, it is ready for execution. As stated previously, the Call Stack is responsible for executing the code. It does so using something known as an Execution Context.

An Execution Context is an environment where a piece of code executes. Each context is like a self-contained environment that includes not just the code in execution but everything it needs to run your variables, functions, objects, and more. Back to our cooking analogy, pots, pans, pizza trays, and all cooking vessels are the execution contexts.

We have two types of Execution Contexts: Function Execution Context and Global Execution Context. A Function Execution Context forms the environment for executing a function's code whenever we call it. Every function call creates a new separate execution context. The Global Execution Context is for the Top-Level code, i.e., the code which is not a part of any function. It is the first execution context to go into the call stack when execution begins. There is only one Global Execution Context, unlike Function Contexts, which can be many.

The code execution begins as the engine pushes the Global Execution context into the Call Stack. The code executes line by line until it hits a function call. Upon hitting a function call, the execution pauses for the current context, and its state is saved. The engine creates a new execution context for the called function and pushes it into the Call Stack on top of the current context. Control then moves to this new context, and execution starts from the first line of the function's body. This process is known as Context Switching. It happens every time the Call Stack encounters a function call.

Once the Execution Context successfully executes the last line of the function associated with the context, the Call Stack pops it off, and the control passes to the previous Execution Context. The execution resumes from the position where it stopped before context switching. The process continues until the Call Stack pops off the Global Execution Context.

Well, that was a lot of jibber jabber. Let's make some pizza and see this in action.

a teenage mutant ninja turtle is holding three pizzas and saying it 's pizza time ..

Code Execution in Action

Here's our pizza recipe. Let's see how our JS engine will process it.

function prepareDough() {
    console.log("Step 1: Preparing Dough");
    console.log(" - 2 cups all-purpose flour");
    console.log(" - 1 tsp yeast");
    console.log(" - 1/2 tsp salt");
    console.log(" - 1 tsp sugar");
    console.log(" - 3/4 cup warm water");
    console.log(" - 1 tbsp olive oil");
    console.log("Step 2: Mixing ingredients and kneading the dough.");
    console.log("Step 3: Letting the dough rest for 1 hour.");
}

function prepareSauce() {
    console.log("Step 4: Preparing Tomato Sauce");
    console.log(" - 1/2 cup tomato puree");
    console.log(" - 1/2 tsp salt");
    console.log(" - 1/2 tsp oregano");
    console.log(" - 1/4 tsp black pepper");
    console.log(" - 1/2 tsp garlic powder");
    console.log("Step 5: Cooking sauce for 10 minutes.");
}

function prepareToppings() {
    console.log("Step 6: Preparing Toppings");
    console.log(" - 1/2 cup grated mozzarella cheese");
    console.log(" - 1/2 cup sweet corn");
    console.log(" - 1/4 cup chopped bell peppers (optional)");
}

function assemblePizza() {
    prepareDough();
    prepareSauce();
    prepareToppings();
    console.log("Step 7: Rolling out the dough into a pizza base.");
    console.log("Step 8: Spreading the sauce over the dough.");
    console.log("Step 9: Adding cheese, corn, and other toppings.");
}

function bakePizza() {
    assemblePizza();
    console.log("Step 10: Baking Pizza");
    console.log(" - Preheating oven to 220C (430F).");
    console.log(" - Baking for 12-15 minutes until golden brown.");
}

bakePizza();

Enter fullscreen mode Exit fullscreen mode

Once the engine compiles your code, it kicks things off by placing the Global Execution Context (GEC) in the Call Stack. First up, the engine scans through your code and sets aside memory for all your pizza-making functions: prepareDough(), prepareSauce(), prepareToppings(), assemblePizza(), and bakePizza(). When it hits the bakePizza() call, the engine pauses the Global Context, creates a fresh Execution Context for bakePizza(), and adds it to the Call Stack.

An animation demonstrating the JavaScript execution context, where the global execution context is created, followed by function calls like 'bakePizza()'. The call stack dynamically updates as functions are pushed and popped.

As bakePizza() springs into action, it needs assemblePizza() to do its job. The engine creates another Execution Context that jumps onto the Call Stack. Now, assemblePizza() starts with prepareDough() - yes, you guessed it, another Execution Context joins the stack! After logging the dough preparation steps and finishing its job, prepareDough() context checks out and leaves the stack, handing control back to assemblePizza(). The same sequence plays out for prepareSauce() and prepareToppings() - each gets its own Execution Context, does its thing with the toppings, and exits the stack when done.

An animation illustrating the JavaScript engine with heap memory and call stack. Functions related to assembling a pizza, such as adding dough, sauce, and toppings, are pushed onto the call stack and then removed as execution completes.

With all preparations complete, assemblePizza() handles the final assembly - rolling dough, spreading sauce, and adding toppings. Once done, it exits the Call Stack, passing control back to bakePizza(). Now, bakePizza() can do its part - handling the baking instructions and logging the final messages. After it completes its tasks, it leaves the Call Stack as well.

An animation showing the JavaScript call stack in action, executing functions step by step. Functions are added to the stack and removed as they complete, visualizing how JavaScript processes synchronous code execution.

When the Call Stack finally empties, we know our pizza-making program has completed its journey - all functions have done their jobs and returned home. And with this, my pizza is ready.

Speaking of which, writing this article has made me incredibly hungry. I think it's time for me to savour this tasty homemade pizza.

Home baked Corn pizza

This ain't much but it's honest work

Wrapping Up!

So, we've reached the end of our delicious journey through JavaScript's execution process! We've learned how the engine processes our code, manages execution contexts, and handles the call stack. What seems like simple scripting is a highly optimised and synchronised process under the hood.

Thanks for reading! I hope this article was insightful for you. I'll leave you here to digest all this information. If you found this helpful, pass it along to your fellow developers (maybe include a pizza when you do)!

Want to connect? Follow me on:

Happy Learning!! πŸ˜ŠπŸ™

Top comments (4)

Collapse
 
thomas180399 profile image
Thomas Frank

How might the JavaScript Engine’s handling of asynchronous operations, such as setTimeout or fetch requests, differ across various engines like Chill Guy Clicker V8, SpiderMonkey, and JavaScriptCore, and what implications could this have for cross-browser compatibility?

Collapse
 
anshumanmahato profile image
Anshuman Mahato

Asynchronous tasks like setTimeout and fetch requests are not managed by the Engine itself. Upon encountering an async task, the Engine sends it to the Runtime Environment to be handled. Once the task is done, the Runtime Environment queues the callback function associated with the task for execution. The Engine picks it up from the queue, creates an execution context for it and executes the code as explained in the article. Now, although the process is similar across all engines, there are slight differences in how each of these handles the queued tasks. So there is a possibility of inconsistencies across browsers. I'll write about the Runtime Environment and asynchronous code execution in detail in the next article of this series so stay tuned.

Collapse
 
madhurima_rawat profile image
Madhurima Rawat

Great article πŸ‘ Never thought pizza πŸ• and javascript will come together πŸ™ƒ

Collapse
 
anshumanmahato profile image
Anshuman Mahato

Before writing this article, neither did I πŸ˜….
Anyway, it's good to know that you liked the article. Thanks for appreciating. 😊