Make your website faster: web workers
Slow websites are the bane of the internet.
A lot of the time it’sthe connection speed or downloading the 18Mb of JavaScript and adverts you need to browse a basic blog, but sometimes it’sraw processing power getting in the way of awesome user experience.
In this article, we are going to learn about web workers, what they are and how to use them to make your site faster.
The problem
As powerful as JavaScript is, at its core, it’s a single threaded language. This means it can only execute one command at a time. It’spretty bonkers to think that all these powerful and engaging web apps are literally just doing one thing at a time, really really fast.
Sometimes you run into some code that takes a while to run, long enough for a user to really notice it. Because JavaScript can only do one thing at a time, the UI will completely lock up until the code has finished executing.
We came across this problem whilst importing, processing and validating a 160,000 row excel file (don’t ask...).
The file was taking around 2 minutes to process, giving the impression to the user that the site had completely hung. I wanted to update the state of the react UI to show how much progress we had made and what was currently happening. Long loading times are so much more manageable when you feel in control and aware of what is happening. However, setState is an asynchronous method, and it was refusing to execute until the data processing has been completed.
I had tried giving the processor some time to relax and update the UI using setTimeout/setInterval techniques, but to no avail! What is a poor developer to do?
Enter the web worker
Web workers are essentially scripts that run in the background of your app, without affecting the main thread. They communicate with the UI via a messaging system, allowing you to offload intensive tasks and keep the UI fresh and up to date.
Getting started
Web workers must have their own file. With the magic of modern bundling techniques, you may need some kind of web worker loader to make sure your worker doesn’t get bundled into your main app file.
- https://www.gatsbyjs.org/packages/gatsby-plugin-workerize-loader/
- https://www.npmjs.com/package/@zeit/next-workers
Of course if you are doing this manually you may not run into this problem.
Communication is key
The trick to using web workers is the messaging system. This is how our main app and the worker file will communicate with each other. There’s a small amount of setup at each end.
The main app needs to import and instantiate the web worker. We then need to add event listening for messages from the worker and fire the worker a message to start it running.
// In the main appconst worker = new Worker('/path/to/my/worker.js'); // Import and instantiate// Listen for messages from the workerworker.addEventListener('message', function(e) {console.log('Message from worker received: ', e.data);}, false);worker.postMessage('Do some work then'); // Start the worker
On the worker side, we just need to listen and react to messages from the main app.
// In the web worker file// Listen for messages from the main appself.addEventListener('message', function(e) {// Do something with the message data...// Post a message back to the main appself.postMessage(e.data);}, false);
When you have finished with the worker, it will need to be shut down again so it doesn’t hang around eating memory.
// Shut down the workerworker.terminate();
Make yourself useful
Messages don’t have to be just text, you can send anything between the worker and the main app. We ended up sending the spreadsheet file reference to the worker, which would periodically fire back messages on its progress to the main app, which could then update the state.
// Main app// Next.js withWorkers automagically splits this into it’sown fileimport UploadWorker from 'lib/uploadWorker/upload.worker';...componentDidMount() {// Start the web workerthis.worker = new (UploadWorker)();this.worker.addEventListener('message', e => {this.setState(e.data);});}componentWillUnmount() {// Close the worker threadthis.worker.terminate();}...handleUpload(file) {this.worker.postMessage(file);return null;}...
Having this in place changed our app from a hanging mess to a snappy loading state that tells the user exactly what’sgoing on, making them feel safe and in control.
It’snever simple
Of course, as with any technology, there are a bunch of limitations and considerations to keep in mind. Rather than muddy this article with all the different details to remember, we're just going to list a few areas to watch out for.
Transferring data
Data is copied, rather than literally transferred, to the worker. Of course you are a good developer and treat data as immutable I'm sure, but copying a 50Mb file can be quite time consuming, so keep an eye out for lag.
If things are getting laggy, you can use a slightly different format to actually transfer the object, rather than copying it. This uses what is known as Transferrable Objects.
Limited functionality
Web workers have a slightly stripped back amount of power to work with. Whilst most functionality is still available, there are some things you can’t use in the worker context. This might require some creative thinking to get around, using the messaging system.
- The DOM
- The
window
object - The
document
object - The
parent
object
Further reading
If you've run into something strange and it’snot covered here, it’sprobably in one of these links below.
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
- https://www.html5rocks.com/en/tutorials/workers/basics/
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
- https://angular.io/guide/web-worker
- https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast
- https://w3c.github.io/workers/