Web Workers
I like the way you work it
@nolanlawson
Kirby is © Nintendo and HAL Laboratory
Hi everybody.
@nolanlawson
I'm Nolan Lawson. Sometimes I make "here's how big of a sub sandwich I had for lunch" hands, or as Mariko taught me they're called in Japanese, "pottery hands" (ろくろ回す手).
I work at Squarespace, and I help maintain PouchDB.
The web has a problem :(
I have to start us off on kind of a dour note, by pointing out that
the web has a problem. It's not a new problem – it's actually
been around since the beginning of the web, but it's an interesting problem,
and it's going to motivate the rest of my talk.
This problem is pretty easy to reproduce. All you need to do is
make a web page with an animated gif - just a simple web page!
Then fire up your dev console and enter some long-running JavaScript
operation, like this loop. What you'll notice is that the GIF is
blocked for the entirety of the loop.
You can reproduce this in all the browser, by the way: Chrome, Firefox,
Safari, and IE.
But it doesn't just affect animated GIFs - it affects almost everything
on your page that moves. HTML5 video, JavaScript animations, CSS animations
(except for hardware-accelerated ones) will all be stopped.
In fact, it's worse than that - any form elements on the page will be
non-interactive as well. Note that I can't click on any checkboxes or
buttons while this is running.
Every line of frontend JS you've ever written has (momentarily) stopped your page from working.
Of course, the reason for this is that JavaScript is single-threaded,
and the browser environment (i.e. the DOM) is single-threaded. So while
JavaScript is running, the browser can't do anything else.
That's kind of a mind-blowing thought, right? It means that every line of JavaScript you've ever written has (momentarily) stopped your page from working. (Frontend JavaScript, anyway.)
We should feel humbled by this fact. We have this great responsibility with the JavaScript we use.
Now, you might not have ever written *such* a long-running JavaScript operation
that you blocked the page entirely. But have you ever made or seen a page that looked like this?
It's functional, but it feels kinda... off. I can interact with the page, but occasionally
the scrolling locks up, and the animations might briefly jerk for a few split-seconds. It works, but
it doesn't feel *good*.
Now, this isn't due to one big long-running operation - this is due to lots of little operations
that add up over time. It's death by a thousand cuts.
Why does this happen? Well, we can explain with MATH!
60 frames per second (FPS)
Most monitors will refresh at 60 frames per second. What that means is that,
every second, they flash an image 60 times, which gives the illusion of movement. It's just
like this flipbook.
Source: Eadweard Muybridge (public domain)
1000ms / 60 frames = 16.7ms
If you do the math, and divide 1000 milliseconds by 60 frames, you get 16.7 milliseconds.
What that means is that, any JavaScript you do has to complete in under 16.7 milliseconds if
you want to stay at 60 FPS.
Factor in browser overhead...
Source: https://vimeo.com/125121010
If you factor in browser overhead, according to Paul Lewis on the Chrome team, this is actually
even smaller. 10ms for all your JavaScript to execute! That's tiny! that's punishingly small!
And if you think it's bad on desktop, try mobile, where we have slower CPUs.
It led Jeff Atwood
to write this post about how Android phones just aren't fast enough to run Discourse, which is
an Ember app. Newer iPhones are barely fast enough, but Android phones just aren't there.
This is the kind of stuff that leads people to say, "Well, I guess the web's not ready for rich,
immersive 60fps mobile apps. We'll have to write an iOS or Android app instead!" But I take exception
with that, because I _am_ an Android developer, and I know the tricks that native developers use to
milk performance out of a weak CPU.
Source: https://meta.discourse.org/t/the-state-of-javascript-on-android-in-2015-is-poor/33889
So what are we supposed to do? Well, it turns out the web has a solution to this, and it's called web workers!
// index.js
var worker = new Worker('worker.js');
worker.postMessage('ping');
worker.onmessage = function (e) {
console.log(e.data); // 'pong'
};
// worker.js
self.onmessage = function (e) {
console.log(e.data); // 'ping'
self.postMessage('pong');
};
I'm not going to go too deep into this code, but a web worker is basically a background thread that you can spawn
whenever you want to do a bit of JavaScript work. Anything that runs inside the worker runs on another thread (another
process in fact), so it cannot possibly block the DOM.
UI thread (main thread)
See, the way that UIs work, on both the web, Android, and iOS, is that you have one thread, called the main
thread, whose responsibility is to respond to UI events like clicks and scrolling, and to paint.
This UI thread is pretty busy. You'd be busy too, if somebody asked you to paint something every 16 milliseconds!
Plus, we're sending down our giant 500KB JavaScript app, which we also expect him to run within those 16 milliseconds.
So what'd be nice, is if the UI thread could spawn some friends to help him out. Then those friends
could work independently, and he wouldn't be trying to do so much himself.
Four cores
This is great, because we can actually spawn as many workers as we want, and even low-end Android phones sold
today have 4 cores. So potentially we can do many things at once, in addition to the UI.
When we want to do something that's unrelated to UI - business logic, reading from local storage, talking to the
network, we shove that off in a background thread.
What can Web Workers do?
- Standard JavaScript
- JSON
-
btoa(), atob()
- FileReader, Blob, ArrayBuffer
So what can you do inside of a web worker? Well, you have ajax and IndexedDB, and like I said,
those actually *can* block the DOM, so it's worthwhile to put them in a web worker. You can also
do any normal JS operations, except for the DOM. But hey, you can do Virtual DOM, right?
What can Web Workers do? (cont.)
IndexedDB (async!) blocks the DOM
Source: http://nolanlawson.github.io/database-comparison/
What can Web Workers do? (cont.)
Ajax (async!) blocks the DOM
What can Web Workers do?
- JavaScript
- JSON
- IndexedDB
- AJAX
- ...but not DOM 😢
- Virtual DOM!
So I experimented with this, and I demonstrated that it's possible. I wrote an app
with an architecture where every UI event basically gets fired off as an action to the web worker.
Inside the web worker, it generates the Virtual DOM tree, diffs it with the previous tree, and then
sends the patch (i.e. the minimal set of operations to be applied to the DOM) back to the main thread.
This is great that the patch tends to be small, because the *only* cost you pay with a web worker on the main
thread is the cost of serializing and de-serializing the messages sent back and forth with the worker.
Source: http://www.pocketjavascript.com/blog/2015/11/23/introducing-pokedex-org
Pokedex.org on a 2011 Galaxy Nexus
This works surprisingly well. Here's my Pokedex.org app running on a 2011-era Galaxy Nexus phone.
It runs at 60fps, and is nearly indistinguishable from a native app. On newer Android phones, it
*is* indistinguishable from a native app.
Pokedex.org bundle sizes
main.js
24.1kB
worker.js
90.9kB
serviceWorker.js
15.8kB
React with Web Workers
And if you still don't believe me, there was a blog post recently where Parashuram demonstrates that you can run
React's virtual DOM inside of a web worker, and he actually demonstrated that this resulted in a better framerate than
when you don't use a worker.
Source: http://blog.nparashuram.com/2015/12/react-web-worker-renderer.html
Angular 2 - without Web Workers
Source: https://www.youtube.com/watch?v=Kz_zKXiNGSE
Angular 2 - with Web Workers
Source: https://www.youtube.com/watch?v=Kz_zKXiNGSE
This is coming to frameworks
Ember
Source: https://github.com/runspired/skyrocket
Web worker support is very good.
Four cores
Now, most phones today have four cores, so if we're only using one core, it looks like this.
Four cores
Let's make it look like this!
Thank you!
@nolanlawson
This think this stuff is going to start popping up in frameworks. It's already going to appear in Angular 2, which is
where I got the idea. But you can use web workers yourself today. The APIs are very easy to work with, and there are
polyfills.
So if you have any JavaScript on your frontend that can run in a worker, please consider throwing it in a worker! Your
framerate will improve, and your users will thank you. Thanks!