The Problem
You have a computational intense piece of javascript that is blocking browser interactions and
animations and making you question why you even try building desktop class web applications. In my case
I was trying to bring spreadsheet-like functionality into an application complete with formulas that
referenced other formulas that referenced other formulas to the brink of infinity. I ended up needing
to calculate tens of thousands of “cells” to get the data I needed.
While these calculations can usually complete in under 500 milliseconds (on a fast
browser/computer), a 1/2 second where the browser can’t do anything makes for a less than ideal
experience. (Think back in the day when you tried to use async: false
on your ajax requests
because you couldn’t figure this callback stuff out. Was I the only one to try that? 🤔)
Solution #1 - setTimeout
In a server or native application we’d fire up a new thread to punt all of these computations
to. The browser, for better or for worse, is all like #lolThreads. I was not familiar enough with web
workers at the time to immediately reach for it so I improvised with the setTimeout
trick. It went something like:
function calculate(years, callback) {
processYear(years, {}, callback);
}
function processYear(years, results, callback) {
if (years.length) {
doAllTheThingsForYear(years.shift(), results);
setTimeout(processYear, 0, years, results, callback);
} else {
callback(results);
}
}
This actually worked quite well. It uses setTimeout
to give up control after each year and lets the
browser do all the things it needs to do. Although it works well it still felt a little hackish and
assumes you have a good seam like processing over a number of years to give up control at. Also, for
our tests we don’t want to just give away milliseconds like that because they add up over time.
Solution #2 - web worker
So I let the setTimeout hack ride for a week or two before I listened to this Adventures in Angular episode where they talked about web workers in Angular. They spent a good amount of time trying to come up with real world examples of where web workers would help. As I listened it occurred to me that I was sitting on one of the quintessential examples…a spreadsheet…so it made me give them another look.
When I realized that a web worker loaded an entirely separate script resource I almost gave up, but we’re using webpack so it’s just another entry, right? Kind of. My first attempt was exactly that which was adding another entry with all my web workin code. The problem with that is this other script runs in a completely different thread outside of your page so when it tries to load it has no idea about the webpack module loading. In other words, it didn’t work.
I thought to myself…surely someone has done this before with webpack so off to duckduckgo it was. It turns out that there is an example of how to do this in the webpack repo itself. Unfortunately, there’s not a lot of actual words in this example so you have to have both a good understanding of web workers and webpack to interpret it. The result is going to be a little anti-climatic since my solution looks a lot like the example, but here goes:
My first step was to make the calculator code where I had previously had the setTimeout
s synchronous:
function calculate(years) {
const results = {};
years.forEach((year) => doAllTheThingsForYear(year, results));
return results;
}
Simple enough, I like deleting code. Step 2 was to change the way I was calling the calculator since it moved from callback based to synchronous:
Before:
function serviceUsedByComponent($q) {
return {
calculate: function() {
return $q(function(resolve) {
calculatorService.calculate([2016, 2017, 2018], (results) => resolve(results));
});
}
};
}
After:
function serviceUsedByComponent($q) {
return {
calculate: function() {
return $q(function(resolve) {
resolve(calculatorService.calculate([2016, 2017, 2018]));
});
}
};
}
$q
is Angular’s promise library. I had previously wrapped my interactions with the calculator
in a promise in order to support the setTimeout solution.
At this point I’m back at square one with my component that needs the data using a synchronous call into the calculator.
[webpack enters stage right]
making the worker work
function serviceUsedByComponent($q) {
const Worker = require('worker!../workers/calculator.worker');
const worker = new Worker();
return {
calculate: function() {
return $q(function(resolve) {
worker.onmessage = (event) => resolve(event.data);
worker.postMessage([2016, 2017, 2018]);
});
}
};
}
Here we change it up a little to instantiate a new worker from the worker code loaded by webpack’s
worker-loader so don’t forget to npm i
worker-loader
. Instead of calling the calculator directly we now post a message to the worker
with our calculator input and resolve the promise with the data we get posted back to us from the
worker. That’s it for calling the worker, now on to the worker itself which is a new file.
working on the worker
require('babel-polyfill');
const calculatorService = require('services/calculator.service');
onmessage = function(event) {
postMessage(calculatorService.calculate(event.data));
};
The first line imports babels polyfills in case you use anything like Object.assign
. Remember
that this worker is loaded in it’s own thread/context so you don’t get anything from your regular
webpack bundle. The rest of the file is receiving the message we posted from the main thread and posting back
the results of the calculations.
webpack.config.js
The only thing I changed in my webpack config was taken from the example itself which sets the output filename for the worker:
worker: {
output: {
filename: 'calculator.worker.js',
chunkFilename: '[id].calculator.worker.js'
}
},
ship it
There you have it, webpack saves the day again.
errors
I left out error handling in my examples for
brevity. I’m starting off with a nodejs style err
property on my postback messages, but it appears
you can get any errors back with an onerror
callback - details here.
browsers
Browser support is all I needed it to be, ymmv - caniuse
performance
Performance wasn’t any better in my case and it may be slightly worse. I don’t think a user is
going to be able to tell the difference between 500ms and 700ms. The more important part here was
getting these computations off of the ui thread. I’m sending a fairly complex object which may be
helped with Transferable
Objects, but I’m calling
it good for now. ¯\_(ツ)_/¯
lazy loading
An unforeseen benefit in our situation was that this provides lazy loading qualities. Our calculator is relatively big so it’s nice to not have that in the initial load.