In our previous article, we highlighted what issues JavaScript can cause when loading a page. We already touched the subject of ‘web workers’, but how can web workers improve the page load experience and performance?
Let’s start off with a quick recap on what happens when a browser loads the page: the browser uses the device’s processing power and in general everything is done through a single thread, called the ‘main thread’. The main thread is the only part of the browser that can access the source code (Document Object Model, DOM) and is responsible for digesting the source code, styling of the page, responding to user events such as links being clicked and renders the page.
Because in general the JavaScript is also evaluated and executed on the main thread, it can cause delays in the page being ready to be shown to users; which in turn provides a bad user experience.
Introducing web workers
To help the main thread out, we can deploy ‘web workers’ for the heavy lifting. Web workers operate on a parallel thread to the main thread, meaning that all the calculations and work they perform does not impact the loading of the page itself – they operate in the background if you will.
Web workers cannot access the DOM, or alter the user interface (the page itself), but are able to retrieve information, send information to other services as well as perform calculations. This allows us to move every function that does not alter the DOM directly to a different thread and clear up the main thread so it can perform its core duties.
Because web workers are on a different thread, information cannot be directly shared between threads (for example the main thread cannot share information directly with a web worker and vice versa). However, you can send messages between threads and a worker (for example posting a message from the main thread to the worker).
This is where the magic happens. For example, the main thread sends a worker a message with data, the worker does its task with the information (for example a calculation) and sends a message back to the main thread, which shows the outcome of the calculation within the DOM.
Testing & results
To test the impact of the web workers, we have created a set of simple pages that shows some content (HTML), two images (large images provided by NASA) and the outcome of the 39th Fibonacci number. One page has the JavaScript working on just the main thread, while the other page uses a web worker.
For this test, the most important metric is to see how long the JavaScript files take up the main thread in total. The two scripts on the main thread in question are:
- single-thread.js
- multi-thread.js
Below are two screenshots that show the impact on the main thread. The timelines are different, due to the scales.
The script that is executed on the main thread takes up 2.24 seconds on the main thread, causing the browser to render the images later after they have been downloaded. The user won’t see the images loading as of yet, but only after the script has finished its runtime.
Looking at the script that uses the web worker and a parallel thread, we can clearly see that the images are being loaded sooner. Showing the users the loading of the page, while the Fibonacci outcome is being calculated in the background.
The script only occupies the main thread for 1.40 milliseconds; which allows the browser to start rendering the images – providing users with the feeling that the page is loading.
- Time the single thread script occupies the main thread: 2,244.8 milliseconds
- Total the script with the web worker occupies the main thread: 1.62 milliseconds
- Initial 1.4 milliseconds to install the worker and to send the message to it
- Final 0.42 milliseconds to receive the message from the worker and to manipulate the DOM
While it takes both pages to fully load roughly the same time, the page speed perception for the user is vastly different. As page speed performance is strongly influenced by a user’s perception, it is fair to say that a page that shows the images being loaded and rendered feels more performant than when the images are not loaded due to the JavaScript execution.
Example of a real web worker
The test provided great results! We have taken the heavy calculations to find the 39th Fibonacci number away from the main thread and allowed a web worker to calculate it in the background. But how did we free up the main thread exactly?
The page we have constructed features three sections:
- The first section with a large image from NASA, ‘lorem ipsum’ and a green background
- The second section which is not shown, but will be dynamically as soon the JavaScript has found the 39th Fibonacci number. This section has a yellow background.
- The third and last section with a large image from NASA again, ‘lorem ipsum’ and a green background
The idea is to review how the page loads when the 39th Fibonacci number is calculated on the main thread and how it loads when a web worker is being used to perform the calculation in parallel.
The JavaScript used is very simple; we have a JavaScript file with a function that directly calculates the Fibonacci number of choice, while the other JavaScript file creates a web worker to calculate the outcome of the Fibonacci number. Both scripts have the same outcome: the Fibonacci number is inserted within the yellow section on the page.
JavaScript for both files
Fibonacci function
To calculate the outcome of the given Fibonacci number, we use a simple yet effective JavaScript function.
let fib = (x) => {
if (x <= 0) return 0;
if (x == 1) return 1;
return fib(x-1) + fib(x-2);
}
Constants
Both scripts also use the same mechanisms to define the constants, to ensure that the outcome is the same and that as little as possible code changes influence the data returned. There are two constants, these being the Fibonacci number of choice (we have used 39) and the element for the yellow section.
const num = 39;
const el = document.getElementById('section-2');
Single thread
Combining these scripts together will provide the script for the page that only uses the main thread.
Parallel thread with a web worker
The script where we use a web worker is more interesting, where we have to use two files. One file sits on the main thread and communicates with the web worker, while the web worker deals with the calculations.
Creation of web worker and sending/receiving messages
To support backwards compatibility (for older browsers) we can first check whether the browser accepts web workers. In our example, we have not provided a solution for backwards compatibility, but the code for the single-threaded implementation could be used when there is no support.
if(window.Worker){
var worker = new Worker('multi-thread-worker.js');
}else{
// single thread code
}
This snippet of code installs a web worker that uses the file ‘multi-thread-worker.js’. Every message posted to the object ‘worker’ will be received by this file.
To send the worker a message, we can use the Worker API.
worker.postMessage(num);
Now we have installed the web worker and sent our first message to it!
The same script will also listen to what the web worker comes back with. The web worker will pass on a message object with ‘data’ as a property. In our case, the data is just a single number.
As soon we have received this number, we will add it to the HTML.
worker.onmessage = (message) => {el.innerHTML = '<h2>Section 2</h2>
<p>Fibonacci number of '+num+' is:
'+message.data+'</p>';};
As you can see, the main thread will only have to create a web worker and send/receive messages. There are no calculations to be done, and the main thread will be able to continue with processing the page.
The worker file
Our worker file only features the function to calculate the Fibonacci number and receive/post messages. It is very similar to the code used on the main thread.
onmessage = (message) => {postMessage(fib(message.data));}
The worker awaits a message, and as soon as it receives the message it will deal with it. In our case, it will use the ‘fib’ (Fibonacci) function to calculate the number and post it back to the main thread.
In summary
While using a web worker won’t make a page load faster in itself, it is a brilliant way to improve the user’s experience on a page and to avoid the rendering of a page being blocked by JavaScript.
So review what JavaScript your site is using and how you can free up the main thread by moving functions that do not manipulate the DOM to a worker thread.
To find out more about how to improve page speed or site performance, get in touch.