Update: this blog post was based on the latest browsers as of early 2016. Things have changed, and in particular the benchmark shows that recent versions of Chrome do not exhibit the performance cliff for non-stringified postMessage()
messages as described in this post.
In recent posts and talks, I’ve explored how Web Workers can vastly improve the responsiveness of a web application, by moving work off the UI thread and thereby reducing DOM-blocking. In this post, I’ll delve a bit more deeply into the performance characteristics of postMessage()
, which is the primary interface for communicating with Web Workers.
Since Web Workers run in a separate thread (although not necessarily a separate process), and since JavaScript environments don’t share memory across threads, messages have to be explicitly sent between the main thread and the worker. As it turns out, the format you choose for this message can have a big impact on performance.
TLDR: always use JSON.stringify()
and JSON.parse()
to communicate with a Web Worker. Be sure to fully stringify the message.
I first came across this tip from IndexedDB spec author and Chrome developer Joshua Bell, who mentioned offhand:
We know that serialization/deserialization is slow. It’s actually faster to
JSON.stringify()
thenpostMessage()
a string than topostMessage()
an object.
This insight was further confirmed by Parashuram N., who demonstrated experimentally that stringify
was a key factor in making a worker-based React implementation that improved upon vanilla React. He says:
By “stringifying” all messages between the worker and the main thread, React implemented on a Web Worker [is] faster than the normal React version. The perf benefit of the Web Worker approach starts to increase as the number of nodes increases.
Malte Ubl, tech lead of the AMP project, has also been experimenting with postMessage()
in Web Workers. He had this to say:
On phones, [stringifying] is quickly relevant, but not with just 3 or so fields. Just measured the other day. It is bad.
This made me curious as to where, exactly, the tradeoffs lie with stringfying messages. So I decided to create a simple benchmark and run it on a variety of browsers. My tests confirmed that stringifying is indeed faster than sending raw objects, and that the message size has a dramatic impact on the speed of worker communication.
Furthermore, the only real benefit comes if you stringify the entire message. Even a small object that wraps the stringified message (e.g. {msg: JSON.stringify(message)}
) performs worse than the fully-stringified case. (These results differ between Chrome, Firefox, and Safari, but keep reading for the full analysis.)
Test results
In this test, I ran 50,000 iterations of postMessage()
(both to and from the worker) and used console.time()
to measure the total time spent posting messages back and forth. I also varied the number of keys in the object between 0 and 30 (keys and values were both just Math.random()
).
Clarification: the test does include the overhead of JSON.parse()
and JSON.stringify()
. The worker even re-stringifies the message when echoing it back.
First, here are the results in Chrome 48 (running on a 2013 MacBook Air with Yosemite):
And in Chrome 48 for Android (running on a Nexus 5 with Android 5.1):
What’s clear from these results is that full stringification beats both partial stringification and no-stringification across all message sizes. The difference is fairly stark on desktop Chrome for small messages sizes, but this difference start to narrow as message size increases. On the Nexus 5, there’s no such dramatic swing.
In Firefox 46 (also on the MacBook Air), stringification is still the winner, although by a smaller margin:
In Safari 9, it gets more interesting. For Safari, at least, stringification is actually slower than posting raw messages:
Based on these results, you might be tempted to think it’s a good idea to UA-sniff for Safari, and avoid stringification in that browser. However, it’s worth considering that Safari is consistently faster than Chrome (with or without stringification), and that it’s also faster than Firefox, at least for small message sizes. Here are the stringified results for all three browsers:
So the fact that Safari is already fast for small messages would reduce the attractiveness of any UA-sniffing hack. Also notice that Firefox, to its credit, maintains a fairly consistent response time regardless of message size, and starts to actually beat both Safari and Chrome at the higher levels.
Now, assuming we were to use the UA-sniffing approach, we could swap in the raw results for Safari (i.e. showing the fastest times for each browser), which gives us this:
So it appears that avoiding stringification in Safari allows it to handily beat the other browsers, although it does start to converge with Firefox for larger message sizes.
On a whim, I also tested Transferables, i.e. using ArrayBuffer
s as the data format to transfer the stringified JSON. In theory, Transferables can offer some performance gains when sending large data, because the ArrayBuffer
is instantly zapped from one thread to the other, without any cloning or copying. (After transfer, the ArrayBuffer
is unavailable to the sender thread.)
As it turned out, though, this didn’t perform well in either Chrome or Firefox. So I didn’t explore it any further.
Transferables might be useful for sending binary data that’s already in that format (e.g. Blob
s, File
s, etc.), but for JSON data it seems like a poor fit. On the bright side, they do have wide browser support, including Chrome, Firefox, Safari, IE, and Edge.
Speaking of Edge, I would have run these tests in that browser, but unfortunately my virtual machine kept crashing due to the intensity of the tests, and I didn’t have an actual Windows device handy. Contributions welcome!
Correction: this post originally stated that Safari doesn’t support Transferables. It does.
Update: Boulos Dib has gracious run the numbers for Edge 13, and they look very similar to Safari (in that raw objects are faster than stringification):
Conclusion
Based on these tests, my recommendation would be to use stringification across the board, or to UA-sniff for Safari and avoid stringification in that browser (but only if you really need maximum performance!).
Another takeaway is that, in general, message sizes should be kept small. Firefox seems to be able to maintain a relatively speedy delivery regardless of the message size, but Safari and Chrome tend to slow down considerably as the message size increases. For very large messages, it may even make sense to save the data to IndexedDB from the worker, and then simply fetch the saved data from the main thread, but I haven’t verified this idea with a benchmark.
The full results for my tests are available in this spreadsheet. I encourage anybody who wants to reproduce these results to check out the test suite and offer a pull request or the results from their own browser.
And if you’d like a simple Web Worker library that makes use of stringification, check out promise-worker.
Update: Chris Thoburn has offered another Web Worker performance test that adds some additional ways of sending messages, like MessageChannels. Here are his own browser results.