Web Workers to improve UI performance

Why you should always consider Javascript Workers in your project

Alberto De Agostini
THRON tech blog
9 min readNov 12, 2020

--

Javascript workers have been introduced around 2010 and are interesting because they address one of the biggest shortcomings of javascript: being single-threaded.
With time, different types of workers are being introduced to the language with different objectives, here is a quick summary of them.

Web workers are the first added workers and are the most general-purpose type. Unlike service workers and worklets as we will see below, they do not have a specific use case, other than the feature of being run separately from the main thread.
As a result, web workers can be used to offload pretty much any heavy processing from the main thread, gaining some performance in the main thread, the one where the UI is running.
You should consider whether to use them if you have heavy tasks that don’t need interaction with the UI.
John Resig has an amazing post of real-world use cases of web workers.

Web Worker browser support (from caniuse.com)

Shared Workers are a special worker that can be accessed by any script that comes from the same domain. Web workers can be accessed only from the script that created them.
This means that if you have multiple applications hosted under the same domain, you could send and receive a message between these applications using the shared workers. This means that you could build a worker that caches information, any app that invokes an API could save the result in the shared worker and could ask the worker if the information is already there to avoid doing the same request between different applications. These workers are actually rarely used (at least I’ve not seen a lot of them) and are not widely supported (there was also an issue to remove them)

Shared Worker browser support (from caniuse.com)

Service workers are a type of worker that serve the explicit purpose of being a proxy between the browser and the network. This means that they can cache resources, modify the outgoing request, or modify the response received from the server.
They are probably the most powerful API that has been added to the web platform recently.
They gained a lot of attention with the PWA’s and they let you achieve functionality that you couldn’t before like:
- Receival of push notification
- Apps working in offline mode
- and more…
You should consider them if you want your web app to work similarly to a mobile app or you want to handle push notifications, work offline, etc… You can find everything they enable you to do here

Service Worker browser support (from caniuse.com)

Worklets are a very lightweight, highly specific, worker. They enable us, as developers, to hook into various parts of the browser’s rendering process.
So basically they are hooks into the browser’s rendering pipeline, enabling us to have low-level access to the browser’s rendering processes such as styling and layout.
You can think of them as lightweight, low-level Web Worker, they are still in an experimental-ish state so I would not use them in production. They might be interesting in the future

Worklet browser support (from caniuse.com)

Which worker we use and how

Our product must support Internet Explorer 11 (at least for now). This requirement puts strong constraints and cuts off pretty much every choice except for the “simple” one, the Web Worker.
We began adopting workers in early 2018. Around that time we started to split our monolithic web application (article) and we thought that one of the libraries (we call it ThronModels) that we were developing would have been a nice use case for workers because it is responsible to make the HTTP request to our backend APIs, model the result and return it. This means that it has no connection to the DOM and it just does logical operations, it is not linked with anything that is related to the UI (because web workers live in a separate thread they have no access to the DOM).

Even if we were sure that lifting some work from the main thread was a nice idea we weren’t 100% confident that this would have had no drawbacks, so we added a fallback to make the library work even without Workers. The cost for doing so was small and we thought that was a nice idea to implement the fallback and then removing it if it proved to be not useful later on.
This turned out to be a great idea because it came in pretty handy in the long run. We will talk about the troubles that the workers caused in a moment. Let’s find out the goodies first.

Recently we developed a web application that is an interactive version of the documentation of some APIs (something similar to this).
The app must fetch an OpenApi JSON file, parse it, and then build the interface accordingly. This specification can grow a lot over time so we thought that fetching and parsing this possibly big file was another perfect use case for the workers, so we did it.
This time we changed approach and we opted for webpack worker loader, this webpack plugin lets you write a “normal” javascript file (it must have an `onmessage` function because if it doesn’t, you won’t be able to communicate with the worker) that is compiled and handled as a worker automatically… Like most of the webpack plugins, it’s Magic

webpack is magic

We added a layer of abstraction to avoid having to deal with `onmessage` and `postMessage` of the worker and everything worked nicely and easily.
We also used babel worker to let us use all the latest js functionality inside the worker file

The result was better than expected. With low-end devices, you can see the difference in the reactivity of the page while it is loading.
If you have some task that is pretty heavy, try to use a Worker, the benefit can be huge. Have a look at these codepens to see with your eyes the difference that they can make:

Tearable cloth (thanks to https://github.com/dissimulate/Tearable-Cloth) to see the difference that a worker can do in the UI responsiveness
Another codepen to see the difference in responsiveness

Do the workers have downsides?

The worker adoption brought some issues that we did not anticipate:

Mocking requests
- We found an issue with workers and testcafè: HTTP requests that are started from the worker won’t be intercepted by the framework and this leads to the impossibility of testing the worker.
To bypass this issue we found 2 possible solutions:

1. When opening the page to test, simply “remove” the worker from the window to prevent the library to use it:
“window.Worker = undefined;”
By doing this the ThronModels library will detect that the Worker API is not there and thus it will fallback to run the same exact code but in the UI thread. This way testcafé can intercept and mock all the requests we want.

2. Add some sort of flag on the page while testing and avoid using the Worker if that flag is true
“window.__disableWorker__ = true;”
Then check that flag in ThronModels library to “choose” if we need to use a worker or not.

We used the first solution but the second one is also easy to implement. None of the solutions are elegant, but they work just fine for our cases.

Workers need a javascript file
The Worker needs a file to work: when you create a new worker it requires the URL of a script as an input parameter. This can be annoying, especially if you are developing a library like the one I used as an example above.
That javascript library is hosted in NPM and thus, we needed no CDN at all, the web applications that need to use the library would just install it from NPM.
But our library creates a worker and it expects to receive an URL of a javascript file to import, luckily we already have hosting and CDN setup so this was not an issue for us (we also needed hosting for the documentation we write with Vuepress) but keep this in mind.
I’m sure there might be some tool to mitigate this issue but it’s something you should pay attention to (check out comlink by Google and the amazing Surma, or greenlet by Jason Miller — as amazing as Surma :D)
Because of this, we have to remember on every deploy, not only to publish the new library version in npm, but also to deploy this static asset for the worker, but this is easily mitigated if you have a CI/CD where you can add a task to automate it.
We still don’t have a CD setup yet (it’s in progress) so we are doing it manually (triggering a Jenkins job) at every deployment. The fallback we implemented proved to be helpful in those cases where operators did forget about deploying the worker file.

Requests are the same but different

Requests are not 100% the same

I said that we use the Worker to make HTTP requests in a library.
When we started working on a new project, some time ago, we had some issues while integrating on a specific API. This API had a validation on the referer header and we realized (after some debug headache) that the requests sent from the Worker did not include this header. We tried and searched on the web if this limit was avoidable but actually found pretty much zero information about that. So in that specific project, we turned off the Worker (the fallback again was useful).

A limitation to emphasize is that you cannot manipulate the DOM inside the worker (because they run in an isolated and separated thread compared to the UI) so you can use them to make massive blocking operations but not to update the DOM or stuff like that. This is because when you send a message to the worker using the postMessage the browser can serialize the message into a JSON string, it can construct a clone of the message (to allow more complex data to be sent, more info here) or it can transfer the ownership of the message to the worker.
All of this is done to avoid sharing memory between the threads and because javascript was originally designed to be single-threaded.

Conclusion

Web workers can bring massive improvements in UI responsiveness, especially if you have synchronous blocking operations.
However, you have to be prepared to have some unexpected issues if you don’t study before using them: we didn’t found any evidence of the fact that the worker does not send the referer header so we probably would never have caught that problem before facing it. If you know something about it please leave us a comment.
If you can rely on better browser support, other workers could help you improve your app even more, so pay double attention to your minimum browser version to be supported when starting a new project (especially the Service Worker)

We plan to continue using javascript workers and evaluate if it is appropriate to move pieces of code in the workers to lighten the UI even further as we add features or start new projects.

Do you also use webworkers? Have you encountered any other unexpected behaviors? If so, how did you overcome them?

Bonus

There is a framework that is experimenting with the full use of Workers, check out neomjs, it’s very promising
If you find this topic interesting, I advise to read Surma’s article about workers, it further explains details and it includes more benchmarks and examples

--

--