Introducing web workers to improve subito.it performance — part 1

Adevinta
Adevinta Tech Blog
Published in
8 min readJul 26, 2022

By: Alberto De Agostini and Alessandro Grosselle

Senior Engineers

Introduction

At Subito.it (part of Adevinta) we recently added web workers to our frontends. This wasn’t without its difficulties, so we thought it was worth sharing our experience.

There’s a lot to talk about, so we’ve split it into two articles. In this first part, we give a brief introduction to web workers, outline our starting point and explain the goal we set for ourselves. In part two, we go into detail about the implementation, the results, the challenges we faced and our verdict on the question: “Was it worth it?”

What is a web worker

Let’s give a quick overview of the technology we’re talking about. If you already know what web workers are and when you should adopt them, you can skip to the next chapter.

A web worker is a browser feature that enables you to run a script (some JavaScript) in a background thread. By background thread we mean a thread that is separated from the main thread. The main thread is the one running the application, doing all the rendering work, running the listener logics, running the framework code, etc. Without the use of web workers, everything runs in the main thread in a browser application.

What led to the development of web workers?

Back in the old days when websites were very simple, web workers didn’t exist. But as time passed, websites became more complex. To deliver a better user experience, the amount of JavaScript running in the websites increased substantially (see this state of JavaScript from the main http archive), and the main thread started to be overwhelmed (at least on some websites). In 2009 (as you can see, this is an ‘old’ feature), the response of the W3C WHATWG was the Web Worker API.
As we said, it’s an API that enables you to run a script (a separate JS file that you provide at worker initialization). We won’t go more into detail here. If you need more, the official MDN docs are amazing guide.

The popup that you’ve probably seen on non-interactive websites.
Web workers can improve this, but if you get this error, you’ve probably gone too far.

When should you use a web worker?

This is a great question to ask yourself whenever you are developing an application from scratch or adding a feature. Theoretically, every piece of JavaScript that doesn’t need to get to or alter the DOM (or other special cases, like browsers’ APIs that are not available in the web worker), can go in a web worker. (Here is a link with all the available APIs.) You should benefit from running that code in a separate thread, giving you better app reaction times and a smoother experience.

This is the theory. Moving everything to web workers is not practical in reality, especially if you use a framework like React, Vue or similar, because the framework does interact with the DOM. This can make it a bit harder to separate the code, and jscan complicate the code a lot.
Adding complexity (and also reducing readability) of the application is a key aspect to keep an eye on when coding. Moving small amounts of code or functions that are performing well into a web worker can have almost zero impact, and in this case the trade-off is not worth it.

So the rule that we suggest is:

Move code into a web worker either if the code is slow, or if it’s easy to encapsulate in something separated from the UI logic and easy to interact with. We definitely suggest reading “When should you be using Web Workers?” from @surma to study the subject further.

Our goal

Yes, it would be cool to move all the code into web workers, but let’s be realistic. In a sizable project/system, that would take a long time. Instead, it’s usually better to identify a small area where you can adopt a web worker, do that “small” piece of work and measure the outcome. Then you can evaluate if something more should be done, or if it’s not worth continuing.

With this approach in mind, we decided to start moving all the code responsible for the http requests and for modelling the responses. Usually this is referred to as the networking layer, and we have a JavaScript library (called Networking), used by all the Next.js applications, that is responsible for exactly this. It exposes some functions that trigger the http requests using Axios, and model the responses using Morphism. We chose this library because it’s well separated from the UI, easy (at least in theory) to migrate and because some of our http responses are pretty big (70–100kb gzipped) and thus the modelling can be expensive.

Of course, our end goal is not to use web workers just because it’s cool. We wanted to improve the experience of our end users by moving code outside the main thread, thereby increasing the interactivity of our website. We expected to reduce the Total Blocking Time of the applications, but more on this later.

We also chose to give developers the option of avoiding web workers (to cater for special cases), so we decided to create a separate library that exposes a function that accepts the Axios configuration, the Morphism schema, and takes care of:

  • Creating the web worker
  • Handling the communication between the web worker and main thread
  • Enabling the web worker to use Axios and Morphism with the configuration and schema provided to make the request and the mapping

This way, when adding a method to our Networking library, a developer can opt to use the worker just by invoking the newly created library. Everything else remains the same, avoiding breaking changes and enabling a smoother/easier migration in case the results are positive.

Initial situation

A picture is worth more than a thousand words.

The diagram shows an example of a network request at the initial state. The client is a Next.js application, it has the sbt-networking library as dependency and can invoke functions (like ‘SaveFavorites’). This directly uses Axios to make the http request, then uses Morphism to map the response, in the end returning the results to the application. This is pretty straightforward, and everything ‘lives’ in the main thread.

What we want to achieve

This is our desired end state.

The networking library now uses the new library, and the part inside the yellow square is running in a web worker, not the main thread.

Here’s a more detailed picture.

Here, the red square is the new library, the left yellow ‘box’ is the main thread and the right yellow ‘box’ is the web worker. We can see that the new library has a piece of code in the main thread which is responsible for the worker creation and handling communication with it.

How we measure the outcome

So far so good, but don’t forget — before implementing any feature, especially those aiming to improve performance, you have to think “how do we measure the impact of this?”

This is an important step, because if you do refactoring that aims to improve performance, but you don’t measure the impact, you’re left in the dark. The result may be even worse than before, which might tell you that that path is not right for your use-case, or that something else is a “performance bottleneck”.

Before actually doing something related to performance, think about how you will measure it

We know that web workers won’t increase all the performance metrics of the application. However, they can improve the interactivity of the page (by relieving some work from the main thread), so we wanted to keep an eye on the TBT (total blocking time) of the page. This can be easily measured using the performance tab of the Chrome DevTools. This performance tab is a pretty useful (and sometimes complex) tool to use. You can read more about it here: https://developer.chrome.com/docs/devtools/evaluate-performance/.

So, this is the method we used to get the measurements we needed:

  • Start measuring performances in DevTools
  • Load the page where we added the web workers
  • Click a button to trigger a search (implemented with web workers)
  • Wait for results
  • End measurement and see values

Repeat this experiment multiple times (at least 30 times) and write down the results. Then repeat the same exact experiment (the same amount of times) but without the web worker. Compare the results and see if there are any differences. We were expecting the TBT to go down (at least a little) and other metrics to remain the same.

Honestly, this process was pretty boring because we had to do all the actions listed above manually. Luckily for us, Chrome DevTools launched a new experimental tab called “recorder” this year. This tab enables us to do the experiment once, record it and then “replay” it just by pressing the play button. I suggest you have a look at the new tab because it also offers different features, like exporting the recorded actions in a puppeteer script. You can find more about it here: https://developer.chrome.com/docs/devtools/recorder/.

With this new tab, we just had to play the flow once, record it, then press a button and write down results. Not super exciting, but better than before and it stopped us making mistakes through the endless repetitions (When you run the same actions manually 50–60 times, you can make some dumb mistakes, like forgetting to press the record button or skipping a step).

Bonus Tip:

When measuring the performance with Chrome DevTool, you can deep dive and analyse what’s happening in great detail. You can even see microtasks that are being executed on the web workers and see exactly how many MS it took (and that’s the burden in ms that you relieved from the main thread, more or less).

Example:
We identified the task that was running on the web worker and saw how many ms it was taking, but we didn’t dive into details. We were more interested in the general TBT metric.

That’s it for this article! In part two, we discuss the results we achieved and the difficulties we faced. Make sure you read it because — spoiler alert — we definitely had some implementation surprises.

And if you have any questions, send me a message on Medium.

--

--

Adevinta
Adevinta Tech Blog

Creating perfect matches on the world’s most trusted marketplaces.