HOLIDAY SALE! Save 50% on Membership with code HOLIDAY50. Save 15% on Mentorship with code HOLIDAY15.

4) Dashboard Lesson

Basics of the JavaScript Runtime Environment in the Browser

20 min to complete · By Ian Currie

In this in-depth lesson, you'll learn about the V8 JavaScript engine, how it works, and how it interacts with the browser to handle asynchronous processes.

The V8 JavaScript Engine

The V8 engine is what powers Chromium and Chrome. It's also used in Node.js. It's a JavaScript engine written in C++. It's open source and mainly maintained by Google. There are other JavaScript engines, like SpiderMonkey which powers Firefox, and JavaScriptCore that powers Safari. In this lesson, you'll focus on V8, though the concepts are similar for other engines.

The V8 engine is responsible for parsing, compiling, and executing JavaScript code. As part of this, it needs to manage memory, control the flow of execution, optimize the code, and interact with the environment (the browser or Node.js).

While executing, there are a couple of core components that many programming language engines, V8 included, use to control the flow of execution:

  • The heap is a pool of memory where variables, objects, and references are stored.
  • The stack is a data structure that keeps track of what the program is doing at any given time.

You can think of the heap as a data store where all the variables, objects, and references are stored in no particular order. The V8 engine also has a garbage collector that keeps track of what is in the heap and tries to clear out memory that's no longer needed.

When you have a memory leak, it is usually due to your code mistakenly "holding on" to references to objects that are no longer needed, which is essentially a bug with your code. When your code keeps references like this, sometimes the garbage collector can't determine whether the object is still needed or not, and so it can't clear it out. This can happen often with closures.

Colorful illustration of a light bulb

Review closures in the JavaScript 101 course, in the Scope Basics lesson.

The stack is more structured than the heap, essentially allowing the JavaScript engine to keep track of what it's doing when functions, call functions that call functions, and so on.

Getting a high level overview of how the stack is important to understanding how the V8 engine and the Browser handle asynchronous processes, so in the next section you'll look at it in more detail.

Visualize the Call Stack With Examples

The stack is a LIFO (last in, first out) data structure, meaning that the last thing to be pushed onto the stack is the first thing to be popped off of it. Like a stack of plates, the last plate you put on the stack is the first one you take off. When talking about the stack, the terms push and pop are used to describe adding and removing items from the stack, respectively.

To visualize this, you'll look at the following code:

function hello() {
	console.log('hello');
	console.log('how are you?');
}

console.log('welcome');
hello();

To run this script, the V8 engine will parse, compile and then start executing the code. Then the stack comes into play:

  1. When the script starts, the global execution context is pushed onto the stack:
|             |
| global      |
|_____________|
  1. console.log('welcome') is called and pushed onto the stack:
| console.log |
| global      |
|_____________|
  1. console.log('welcome') finishes executing and is popped off the stack:
|			  |
| global      |
|_____________|
  1. hello() is called and pushed onto the stack:
| hello       |
| global      |
|_____________|
  1. Inside hello(), console.log('hello') is called and pushed onto the stack:
| console.log |
| hello       |
| global      |
|_____________|
  1. console.log('hello') completes, and then console.log('how are you?') is called, pushing another console.log onto the stack:
| console.log |
| hello       |
| global      |
|_____________|
  1. After the console.log('how are you?') call completes, hello() also finishes and the call stack looks like:
| global      |
|_____________|
  1. Finally, when the script finishes executing, the global execution context is also popped off the stack, leaving it empty:
|             |
|_____________|

The stack keeps track of where each function was invoked from so that it knows where to return after executing a function. The stack stores the calls and execution contexts so that all the functions have everything in their scope that they need to execute.

Recursion and the Call Stack

Recursion occurs when a function "calls itself". That is, as part of the definition of the function, it contains a call to the function that's being defined:

function recursive() {
	recursive();
}

recursive();

Ignoring the global execution context for now, the execution of this script involves the following steps:

  1. The first call to recursive() is made, pushing it onto the stack:
| recursive   |
|_____________|
  1. Within recursive(), it calls itself, adding another instance to the stack:
| recursive   |
| recursive   |
|_____________|
  1. This process repeats, adding more instances of recursive() on the stack:
| recursive   |
| recursive   |
| recursive   |
|_____________|
  1. Each subsequent call adds another recursive() to the stack:
| recursive   |
| recursive   |
| recursive   |
| recursive   |
|_____________|
  1. Eventually, if there's no base case or exit condition, this will result in a stack overflow error, as the call stack limit is exceeded:
Uncaught RangeError: Maximum call stack size exceeded

Call Stack Traces

When you run into errors, you will have noticed a call stack trace showing you the state of the call stack when the error was thrown. So, if you had three functions; a() which calls b() which calls c(), which in turn throws an error, you can track down where the error occurred:

function a() {
	b();
}

function b() {
	c();
}

function c() {
	throw 'Error!';
}

a();

/* STACK TRACE:
Uncaught Error!				script.js:10

c	@	script.js:10
b	@	script.js:6
a	@	script.js:2
(anonymous)	@	script.js:13

*/

The stack trace shows that at the top of the stack is c(), which is where the error was thrown. Then b() because c() was called as part of b() and so on.

The (anonymous) part means that there is no function name, which here refers to the fact that this is the global context of the script (where a() is called) and so, is not in a named function. (anonymous) can also refer to execution inside function expressions and arrow functions, which also have no name.

Being able to interpret the stack trace is fantastic for debugging!

Now, you'll turn your attention to how the browser interacts with the stack to handle asynchronous processes.

Key Components of Asynchronous JavaScript Processes

Conceptually, there are three main components to asynchronous processes in JavaScript:

  • The JavaScript engine, which is single-threaded and handles the stack.
  • The browser, which is multi-threaded and handles Browser APIs and Web APIs like the DOM, Fetch, and XMLHttpRequest.
  • A queue system where the browser puts jobs that are executed by the JavaScript engine when the stack is empty.

As you have experienced, the V8 engine is single-threaded. When expensive processes are run, the UI freezes and no events or code can be processed.

However, the browser can take advantage of multiple threads. Each tab in your browser is typically given its own instance of the JavaScript engine, so the browser must be able to handle concurrent or multi-threaded workloads, able to take advantage of multiple cores of a CPU, if available.

Colorful illustration of a light bulb

The Web Workers API allows you to run JavaScript in a background thread. Giving you some limited ability to run JavaScript operations in a truly multi-threaded way.

The Browser Facilitate Asynchronous Operations in JavaScript

A single JavaScript thread takes advantage of the concurrent environment it is in by using the browser, via browser APIs and Web APIs. You can think of these as "services" that the JavaScript thread can use to get things done. Some of the APIs the browser makes available are:

  • The DOM
  • The Fetch API
  • XMLHttpRequest
  • setTimeout()
  • addEventListener()

Yes, most of the stuff you have been interacting with is part of the browser and not JavaScript the language itself.

Since the tasks that the browser performs can be taken care of by different processes in the browser, it can appear that it's JavaScript that's working concurrently (on two or more things at the same time). In fact, JavaScript is working asynchronously; it's the browser that is working concurrently.

For example, when JavaScript call fetch(), it's asking the browser to take care of making the request. When the browser is done, it adds a job to the queue system. The JavaScript thread picks up jobs from the queue when the stack is empty.

Communication Between the Browser and the JavaScript Engine

Say the main JavaScript thread is working away and the next thing on its call stack is code that contains a call to a Web API, setTimeout(). The code is saying to the browser, " run console.log("hello") after 5 seconds.":

setTimeout(() => console.log('hello'), 5000);

This is one way that JavaScript can send a "job" to a Web API. It usually sends some arguments specifying the details of what the browser should do, and it will often send a callback along too. This is like JavaScript writing a reminder of what it needs to do once the browser is done.

The browser stores this information and takes over the task from the JavaScript engine. Since the JavaScript engine has delegated this task, that part can popped off the call stack. Now, the main JavaScript thread can continue working on other bits of code while the browser works on the setTimeout() job.

Once the 5 seconds have timed out, the browser will put () => console.log("hello") into a queue system.

The call stack, however, may be occupied, and so the callback waits in the queue. Once the call stack is empty, the queue will put the task that is first in line on the call stack where the JavaScript engine takes over again, and finally log "hello" to the console.

You can think of the queue system as being made up of three parts:

  • The callback queue or task queue where all the jobs that come back from the WebAPIs get lined up
  • The render steps, where the screen updates get lined up
  • The event loop, which pushes jobs from the queues onto the call stack when it's empty

As you can see, render jobs are also taken care of by the main JavaScript thread. This is generally updated at around 60 frames per second. However, if you have a massive for loop, the render jobs can end up waiting until the call stack is free. This is why the UI freezes!

Colorful illustration of a light bulb

This is a high level overview of how the browser and JavaScript engine work together to handle asynchronous processes. There are many more details to how this works, for example, some tasks are prioritized over others and there may be more than just one queue. Namely the microtask queue is generally for promises and is prioritized over the callback queue.

For detailed technical information, check out the Event Loops section of the HTML Standard.

Summary

You've just expanded your understanding of how JavaScript runs behind the scenes, particularly with the V8 engine. You've:

  • Found out that V8 is a powerful JavaScript engine built by Google. It's the driving force behind Chrome and Node.js, which parses and executes JavaScript code.
  • Grasped the concepts of the heap for storing objects and the stack for tracking function calls.
  • Understood how the stack works, which keeps track where functions are called from and where to return to after execution.
  • Learned about recursion and how it can lead to a stack overflow if there's no base case.
  • Learned about stack traces, a valuable tool in debugging that shows the state of the stack when errors occur.
  • Dived into asynchronous operations, distinguishing between single-threaded JavaScript and multi-threaded browser capabilities that handle Web APIs.
  • Discover how the browser serves as a facilitator, handling tasks like DOM manipulation and HTTP requests, allowing JavaScript to run asynchronously.
  • Became familiar with the queue system, including the callback queue and the event loop, which govern how the browser and JavaScript engine interact to process “jobs” after async operations are completed.