This content originally appeared on Telerik Blogs and was authored by Ashnita Bali
Instead of doing everything in a single process on a single thread, modern browsers have a multi-process architecture. What does this mean for developing in JavaScript, which is single-threaded?
We often hear “main thread” mentioned when learning about optimizing JavaScript code for performance.
JavaScript is single-threaded. Only one thing can happen at a time, on a single main thread and everything else is blocked until an operation completes. — MDN
It took me a while to realize that the main thread we’re talking about belongs to a process in the browser that is specifically responsible for rendering web page and running anything that affects rendering (JavaScript and user input events). Modern browsers have a multi-process architecture with separate processes that take care of different parts of the browser.
Being aware of processes and threads also helped me see that Web APIs introduce asynchronous tasks in our applications. When we call Web APIs such as fetch(url).then(cb)
or setTimeout(cb, delay)
, they do not run on the main thread of the renderer process. For example, fetch()
runs on a network thread in the browser process.
Using the Web Workers API we can run CPU-intensive tasks on a background thread of the renderer process. Additionally, we can use the requestIdleCallback()
API to queue time-consuming, low-priority tasks to run on the main thread of the renderer process when the browser would otherwise be idle.
When programming with JavaScript, we mostly don’t have to think about threads. However, a basic understanding of threads and processes helps clear some of the mysteries of asynchronous programming in JavaScript. Therefore, in this article we will talk about processes, threads, the responsibilities of the main thread of the renderer process, and its interaction with other the other browser processes and threads.
Photo credit: John Anvik on Unsplash.
Before we can talk about processes and threads, we need to look at the difference between compiled and interpreted languages.
Compiled vs. Interpreted Programming Languages
Programming languages are high-level human-readable languages that need to be converted to low-level binary code and machine code that computers can execute. Programming languages can be categorized into compiled or interpreted languages.
What is the difference between the two?
Compiled Languages
Applications written with compiled languages are compiled to produce machine code that is executed directly by the operating system. The application is compiled using a compiler. Compiling an application is often referred to as the “build” step. The build step produces an executable file containing the machine code.
The executable file is packaged and made available to the users so they can install it on their devices.
For example, Google Chrome is an application written with a compiled language (mainly C++).
When we run the Chrome application, for example by clicking on the icon, the operating system on our device creates a process to execute the application.
Interpreted Languages
An interpreted language uses an interpreter to parse the application code, translate it into instructions the interpreter can understand and then execute the instructions. The interpreters themselves are programs (written in assembly language or high-level language).
JavaScript is an interpreted language used to build web applications. Browsers such as Google Chrome have a JavaScript Engine that has an interpreter to translate the JavaScript code and execute it.
Now we know that compiled applications are compiled to produce machine code that is executed directly on the user’s computer, whereas interpreted applications are parsed, translated and executed by an interpreter. Let us see how processes and threads fit into the picture next.
Processes and Threads
Process
When we run an application that was written with a compiled language (for example, by double-clicking on its executable file), the operating system starts a process.
Starting a process means that the operating system does the following things:
- Loads the application’s binary code into memory
- Allocates a block of memory for the application to keep its state (a heap)
- Starts a thread of execution
Thus, a process is an instance of the application in execution. It includes the application’s bytecode in memory, a heap and a thread. The heap stores the application’s state, while the thread is the actual flow of execution through the binary code.
An application can create additional threads to execute parts of the instructions.
A process can also ask the operating system to create child processes to control separate parts of the application. The operating system allocates separate memory space to each process. Processes do not share resources—instead, they communicate with each other using a mechanism called Inter-Process Communication (IPC).
Thread
As we mentioned earlier, a process can create additional threads. We refer to the main thread of execution as the main thread, and to the threads created to execute parts of the program as background threads.
Threads represent independent execution contexts within a process. In a multi-threaded process, each thread has its own stack, stack pointer, program counter and thread-specific registers to keep track of its execution.
Now that we have a general overview of processes and threads, let us talk about the multi-process architecture used by browsers with the aim to see where web applications fit in.
Modern Browsers Have a Multi-Process Architecture
Browsers are built using compiled languages. Instead of doing everything in a single process on a single thread, modern browsers have a multi-process architecture.
Browsers create multiple processes, each responsible for a different part of the browser’s functionality. The processes in turn create multiple threads to run programs concurrently.
A multi-process architecture provides the browsers with better:
- Security—each process has its own memory and resources accessible only by the threads within the process
- Stability—if a process is running slowly or becomes unresponsive , it can be restarted without affecting other processes ♀️
Let us look at Google Chrome for an example. When we open a Chrome browser, we run the Chrome application. The operating system creates a process — this is Chrome’s main process which Chrome aptly calls the browser process.
The browser process creates further child processes to control various parts of the browser. Following are some of the processes in Chrome:
- Browser process
- Renderer process
- GPU process
- Plugin process
- Extensions process
- Utility process
The process names are reflective of their functions. Please refer to “Inside look at modern web browser” by Mariko Kosaka for a beautifully illustrated and detailed explanation of the processes in Chrome.
As web developers, we are especially interested in the renderer process and its interaction with the main browser process.
The browser process controls the “browser” part of the application including the address bar, bookmarks, back and forward buttons. It also handles the invisible, privileged parts of a web browser such as network requests and file access.
While the renderer process controls the actual rendering of the web page. — Mariko Kosaka
Great! Now we know that the renderer process is responsible for rendering web pages. Let us take a closer look at what rendering actually means and how the renderer process does it.
The Renderer Process
Rendering happens in a sandboxed process so if an evil website exploits a security vulnerability in the rendering code, the sandbox keeps the damage contained. The browser itself is safe and the other tabs are safe.
In order to talk about the role of the renderer process, let us first talk about what rendering is.
What is Rendering?
Rendering is the process of turning HTML content into pixels. — Steve Kobes
An HTML document contains a web application’s code (HTML elements, text content, embedded content such as images, CSS and JavaScript). The rendering process turns the HTML document into a web page that users can see on their screen and can interact with. The HTML document in a Angular application may look something like this:
// index.html
<!DOCTYPE html>
<html>
<head>
`<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">`
<style>
/* critical css style rules */
</style>
`<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">`
</head>
<body>
<app-root></app-root>
<script src="runtime.js" defer>
<script src="polyfills.js" defer>
<script src="vendor.js" defer>
<script src="main.js" defer>
</body>
</html>
When we visit a website, the browser process gets the site’s HTML document from the cache or service worker, or makes a network request to the server hosting the website.
The browser process then sends the HTML document to the renderer process to render the web page.
Rendering a page involves:
- Performing the critical rendering path
- Loading JavaScript, interpreting JavaScript to binary code, and executing the binary code
- Painting the actual pixels on the screen
The renderer process uses a rendering engine to carry out the steps in the rendering path. Let us take a closer look at the rendering engine and the critical rendering path next.
Rendering Engine
Browsers use rendering engines to render web pages.
A rendering engine is a software that:
- Implements the specs of the web platform
- Carries out the critical rendering path
- Embeds the JavaScript engine
Examples of rendering engines include Blink (Chrome), Gecko (Mozilla) and WebKit (Apple).
Critical Rendering Path
The rendering engine goes through a sequence of steps called the critical rendering path to transform an HTML document (HTML, CSS and JavaScript) into the pixels drawn on the user’s screen.
The rendering engine does the following steps during the critical rendering path:
- Parses the HTML and starts building the Document Object Model (DOM)
- Requests external resources (stylesheets, scripts, images, etc.)
- Parses the styles and builds the CSS Object Model (CSSOM)
- Computes styles for the visible nodes in the DOM tree and creates a render tree that contains the computed styles
- Determines the visual geometry (width, height and position) of the elements based on the viewport size (and orientation for mobile devices)
- Paints the pixels on the screen
We can categorize rendering into two parts:
- Rendering the application’s home page when the application first loads
- Updating the rendering as the application runs, in response to user interaction, navigation, scrolling, etc.
The initial render starts from scratch. From parsing the HTML document, creating all the data structures (DOM, CSSOM, render tree, layout tree, etc.), painting the whole page, and downloading, processing and executing JavaScript, then finally registering the event listeners to make the page interactive.
While the application is running, the JavaScript code can update the document content, structure and styles using the DOM API. The rendering engine updates the rendering to reflect the changes made by JavaScript.
I really recommend watching Life of a Pixel by Steve Kobes (2019, 2018) for an in-depth look at the rendering pipeline in Blink (Chrome’s rendering engine). This talk is truly amazing, and you will be delighted at the amount of learning you will take away from it.
JavaScript Engine
Since JavaScript is an interpreted language, we need an interpreter to convert JavaScript code into machine code and then execute it.
Browsers have a JavaScript engine that encompasses a parser, an interpreter and an optimizer. Most major browsers have their own implementation of the JavaScript engine. Chromium’s JavaScript engine is called V8.
As we mentioned earlier, the browser’s rendering engine embeds its JavaScript Engine. For example, Chrome’s rendering engine (Blink) creates an instance of V8 (the JavaScript engine) — an instance of V8 is called an Isolate.
Anything that interacts with the DOM needs to run on the main thread to avoid synchronization issues. Since JavaScript can modify the content, structure and styles of elements on the web page using the DOM API, it makes sense that JavaScript runs on the main thread of the renderer process.
As we saw earlier, the application’s scripts are loaded during the critical rendering path. Once the scripts are loaded, the JavaScript engine uses its various components to parse, interpret, execute and optimize the JavaScript.
Using Chrome as an example, the JavaScript engine does the following tasks:
- The parser parses the JavaScript to create an AST.
- The interpreter (Ignition) has a bytecode generator that walks the AST and generates a stream of bytecode.
- The interpreter executes the bytecode, one bytecode at a time.
- The optimizing compiler (TurboFan) generates optimized code.
Please refer to Life of a Script to learn details about how JavaScript is loaded, parsed, compiled and optimized in Chrome.
Now we see that when we say JavaScript is single-threaded because it runs on a single main thread, we’re talking about the main thread of the renderer process. We know that the browser’s rendering engine runs on the main thread of the renderer process, the rendering engine creates an instance of the JavaScript engine, and the JavaScript engine creates a JavaScript callstack to keep track of the execution of the application’s JavaScript.
I want to point out here that the JavaScript callstack is not the same as the stack created by the operating system for the main thread. I naively thought so at the start and was quite confused.
Renderer Process Main Thread
I’m sure we are quite aware of the importance of the main thread of the renderer process by now. We know that the rendering engine and the JavaScript engine both run on the main thread of the renderer process. Thus, the main thread does most of the work in the renderer process.
The main thread:
- Carries out the critical rendering path
- Stores the DOM, CSSOM, render tree, layout tree and other data structures created during the critical rendering path
- Exposes the DOM API to the application’s JavaScript
- Updates rendering
- Responds to user inputs (accepts events from input devices and dispatches those events to the elements that should receive them)
- Interprets and executes the application’s JavaScript (except workers)
The main thread has an event loop that orchestrates running JavaScript, updating rendering and responding to user inputs. A thread can only run one task at a time. Therefore, while the main thread is running JavaScript, it cannot update the rendering or respond to user input. It is important that our application’s JavaScript does not block the main thread—a function that takes too long to run blocks the main thread until it finishes executing.
As we see the renderer process does not actually paint the actual pixels on the screen. So who does?
Painting the Pixels on Screen
Talking about painting pixels makes me think of this song from Disney’s Alice in Wonderland :
We’re painting the roses red.
We dare not stop,
Or waste a drop,
So let the paint be spread.
As Steve Kobes explains in his talk , Life of a Pixel, browsers use the graphics library provided by the underlying operating system to paint the actual pixels on the user’s screen. Most platforms use a standardized API called OpenGL. There are also newer APIs such as Vulkan.
However, renderer processes are sandboxed for security to keep the user’s device safe from web applications and keep other processes safe from exploitations of any security vulnerabilities in the renderer process. Therefore, the programs running on the renderer process cannot make system calls to request services from the operating system.
The renderer process communicates with the GPU process to paint the actual pixels on the user’s device using the graphics library. The browser trusts the code running on the GPU process since it is its own code, therefore the GPU process can make system calls.
Web APIs
Web APIs allow web applications to access the user’s files, microphone, camera, geolocation, etc. with the user’s permission.
Web APIs are built into the web browsers. Web APIs expose data from the browser and surrounding computer environment. — MDN
Examples of Web APIs include:
- DOM API
setTimeOut()
- Fetch API
- Client-side storage APIs
- Device APIs
- Media APIs
While the DOM API methods run synchronously, the other Web API methods run asynchronously.
For example, if we call document.createElement()
the JavaScript engine sequentially adds the method’s execution context on the JavaScript callstack even if the callstack is not empty.
Whereas, if we call the setTimeout()
which is a Web API, the renderer process asks another process (perhaps the browser process) to start the timer, and when the specified time has passed, the browser process queues the callback we sent setTimeout() so that it can run on the main thread of the renderer process.
The browser uses callback queues (also called job queues, task queues or message queues) and a microtask queue, to queue the callbacks that are ready to run on the main thread. An event loop executes the callbacks waiting in the queue when the JavaScript callstack becomes empty.
Worker Threads
Finally, we have arrived at worker threads. What are worker threads?
Browsers provide us with the Web Workers API so that we can offload CPU-intensive operations in our web applications from the main thread to background threads of the renderer process. These background threads are also called worker threads or workers.
We use the Worker
interface, available on the global window
object, to create a Web Worker. (The browser exposes a global window
variable representing the window in which the script is running to JavaScript code. The window
object includes items that are globally available.)
The JavaScript engine creates a new worker thread and loads the named script to run in parallel to the main thread. The DOM API, CSSOM and other data structures created during the critical rendering path exist on the main thread. Therefore, scripts running in the worker threads cannot access the DOM API.
// main.js
if (window.Worker) {
const myWorker = new Worker('worker.js');
myWorker.onmessage = function(e) {
console.log(e.data);
}
}
The main thread and worker thread communicate by posting messages to each other using the postMessage()
method. And they respond to messages via the onmessage
event handler. The message event has a data attribute that contains the message.
// worker.js
const result = doCpuIntensiveWork();
postMessage(result);
function doCpuIntensiveWork() {}
Scripts running in the worker thread are already within the worker space so they can access postMessage()
directly.
Please refer to MDN to learn more about Web Workers and the Angular docs to learn how to create workers in Angular.
Summary
In this article, we saw that browsers are built using compiled languages. Instead of doing everything in a single process on a single thread, modern browsers have a multi-process architecture. The multi-process architecture allows browsers to provide web applications the necessary security and stability.
We learned that browsers use a rendering engine to render pages. The rendering engine implements the specs of the web platform, carries out the critical rendering path, and embeds a JavaScript engine. JavaScript is an interpreted language—therefore, the JavaScript engine includes an interpreter that translates the JavaScript code into binary code. The JavaScript engine creates a JavaScript callstack to keep track of the execution of the JavaScript code.
The main thread of the renderer process is responsible for rendering web pages and runs anything else that affects rendering to avoid synchronization issues. JavaScript and user input events can affect rendering by manipulating the DOM or styles. Therefore, in addition to carrying out the critical rendering path, the main thread runs JavaScript (except workers) and accepts events from input devices, and dispatches those events to the elements that should receive them. The event loop orchestrates running these tasks on the main thread.
Web APIs introduce asynchronous tasks to our application. Asynchronous tasks run on other threads depending on the Web API being called (background thread of the renderer process or a thread in another process). We pass callbacks to the Web API call or to a promise returned by the call. When the asynchronous task finishes running, it adds the callback together with the result to a queue in the main thread. The event loop executes the queued callbacks on the main thread of the renderer process when the JavaScript callstack is empty, thus ensuring that synchronous code runs before asynchronous code.
I hope you found that learning about processes and threads, and taking a closer look at the renderer process and its interaction with the other processes in the browser helps you understand the synchronous and asynchronous nature of the code in our frontend applications.
Resources
- Life of a Pixel (2018, 2019, 2020)
- Life of a Process
- Philip Roberts: Help, I’m stuck in an event-loop.
- SmashingConf London—Jake Archibald on “The Event Loop”
- Chromium’s Multi-process Architecture
- Chrome University
- How Blink works
This content originally appeared on Telerik Blogs and was authored by Ashnita Bali
Ashnita Bali | Sciencx (2021-12-07T14:17:51+00:00) Angular Basics: Introduction to Processes and Threads for Web UI Developers. Retrieved from https://www.scien.cx/2021/12/07/angular-basics-introduction-to-processes-and-threads-for-web-ui-developers/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.