GETTING OUT OF OUR USERS’ WAY LESS JANK WITH WEB WORKERS
#PerfMatters @TRENTMWILLIS
Slide 2
Hello PerfMatters! Think about your web app for a moment…
#PerfMatters @TRENTMWILLIS
Slide 3
Cool Web App That Pays The Bills
#PerfMatters @TRENTMWILLIS
Slide 4
Cool Web App That Pays The Bills
#PerfMatters @TRENTMWILLIS
Slide 5
Cool Web App That Pays The Bills
#PerfMatters @TRENTMWILLIS
Slide 6
#PerfMatters @TRENTMWILLIS
Slide 7
#PerfMatters @TRENTMWILLIS
Slide 8
How do we prevent numerous, large, and/or slow data operations from impacting our users? #PerfMatters @TRENTMWILLIS
Slide 9
Web Workers They help, but they complicate
#PerfMatters @TRENTMWILLIS
Slide 10
GETTING OUT OF OUR USERS’ WAY LESS JANK WITH WEB WORKERS
#PerfMatters @TRENTMWILLIS
Slide 11
@TRENTMWILLIS SENIOR UI ENGINEER AT NETFLIX
y e d i p S #PerfMatters @TRENTMWILLIS
Slide 12
The Web Workers API “allows Web application authors to spawn background workers running scripts in parallel to their main page” #PerfMatters @TRENTMWILLIS
Slide 13
new Worker(‘worker.js’);
#PerfMatters @TRENTMWILLIS
Slide 14
new Worker(‘worker.js’); new SharedWorker(‘worker.js’);
#PerfMatters @TRENTMWILLIS
Slide 15
new Worker(‘worker.js’);
It’s like a <script> but loads in a different thread!
#PerfMatters @TRENTMWILLIS
Slide 16
Web Workers allow “for thread-like operation with message-passing as the coordination mechanism” #PerfMatters @TRENTMWILLIS
Slide 17
// main thread worker.postMessage(message);;
#PerfMatters @TRENTMWILLIS
Slide 18
// worker thread self // “window” for a Worker
#PerfMatters @TRENTMWILLIS
Messaging is the bulk of the Web Workers API you need! // main thread worker.addEventListener( ‘message’, event => console.log(event.data) );
#PerfMatters @TRENTMWILLIS
Slide 22
// main thread worker.terminate();
#PerfMatters @TRENTMWILLIS
Slide 23
Web Worker life-cycle
worker.postMessage() new Worker()
worker.terminate() worker.addEventListener()
main
worker self.addEventListener() self.postMessage()
#PerfMatters @TRENTMWILLIS
Slide 24
PROBLEMS
#PerfMatters @TRENTMWILLIS
Slide 25
Problem: It is hard to know when a worker’s task is complete
When am I done? worker.postMessage(‘doTask’);
#PerfMatters @TRENTMWILLIS
Slide 26
Problem: Workers are difficult to manage and coordinate worker.postMessage(‘doTask’); otherWorker.postMessage(‘doOtherTask’); How do I manage both results? processResults( taskResult, otherTaskResult );
#PerfMatters @TRENTMWILLIS
Slide 27
Problem: Workers are difficult to test
How do I unit test this? worker.postMessage(‘doNetworkTask’);
#PerfMatters @TRENTMWILLIS
Slide 28
Problem: Workers are difficult to test
How How dodo I stub I unit the test network? this? worker.postMessage(‘doNetworkTask’);
#PerfMatters @TRENTMWILLIS
Slide 29
Problem: Workers can not be dynamically defined If only this was possible… const worker = new Worker(data => { // Expensive data operations… return processedData; });
#PerfMatters @TRENTMWILLIS
Slide 30
SOLUTIONS
#PerfMatters @TRENTMWILLIS
Slide 31
Problem: It is hard to know when a worker’s task is complete
Solution: Turn messages into Promises Replace one platform feature with another!
#PerfMatters @TRENTMWILLIS
Solution: Turn messages into Promises
promise-worker github.com/nolanlawson/promise-worker
#PerfMatters @TRENTMWILLIS
Slide 36
Problem: Workers are difficult to manage and coordinate
Solution: Use Promises (again)
#PerfMatters @TRENTMWILLIS
Slide 37
Problem: Workers are difficult to manage and coordinate
Solution: Expose Worker methods as main thread functions
#PerfMatters @TRENTMWILLIS
Slide 38
Solution: Expose Worker methods as main thread functions backendOneWorker backendTwoWorker
#PerfMatters @TRENTMWILLIS
Slide 39
Solution: Expose Worker methods as main thread functions const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]);
#PerfMatters @TRENTMWILLIS
Slide 40
Solution: Expose Worker methods as main thread functions const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]); const result = await processingWorker.process(data); console.log(result);
#PerfMatters @TRENTMWILLIS
Slide 41
Solution: Expose Worker methods as main thread functions const data = await Promise.all([ backendOne.fetch(‘first’), backendTwo.fetch(‘second’) ]); const result = await processing.process(data); console.log(result);
A good Worker abstraction looks like any other object!
#PerfMatters @TRENTMWILLIS
Slide 42
Solution: Expose Worker methods as main thread functions
Comlink github.com/GoogleChromeLabs/comlink
#PerfMatters @TRENTMWILLIS
Slide 43
Solution: Expose Worker methods as main thread functions
Workerize github.com/developit/workerize
#PerfMatters @TRENTMWILLIS
Slide 44
Solution: Expose Worker methods as main thread functions
importFromWorker github.com/GoogleChromeLabs/import-from-worker
#PerfMatters @TRENTMWILLIS
Slide 45
Problem: Workers can not be dynamically defined
Solution: Create Workers from Blob URLs of functions
#PerfMatters @TRENTMWILLIS
Slide 46
Solution: Create Workers from Blob URLs of functions const workerFromFunction = (fn) => { const src = (${fn})();; const blob = new Blob([src], {type: ‘application/javascript’}); const url = URL.createObjectURL(blob); return new Worker(url); };
#PerfMatters @TRENTMWILLIS
Slide 47
Solution: Create Workers from Blob URLs of functions
greenlet github.com/developit/greenlet
#PerfMatters @TRENTMWILLIS
Slide 48
Web Worker libraries to use promise-worker -> turn Worker messages into Promises greenlet -> turn a Function into a Worker workerize -> turn a Module into Worker comlink -> give a Worker a nice main thread interface importFromWorker -> turn a Module import into a Worker
#PerfMatters @TRENTMWILLIS
Lumen “The majority of data operations in Lumen are done in Web Workers. This allows Lumen to keep the main thread free for user interactions, such as scrolling and interacting with individual charts, as the dashboard loads all of its data.”
#PerfMatters @TRENTMWILLIS
Slide 52
VaporBoy (WASMBoy) Runs a WASM-based GameBoy emulator with Web Workers for smooth UI vaporboy.net
#PerfMatters @TRENTMWILLIS
Slide 53
We can “weave” a web of Web Workers!
Worker-To-Worker Communication
#PerfMatters @TRENTMWILLIS
Slide 54
// worker thread const workerInWorker = new Worker(‘worker.js’);
#PerfMatters @TRENTMWILLIS
Slide 55
MessageChannel consists of 2 MessagePorts
#PerfMatters @TRENTMWILLIS
Slide 56
// main thread const worker1 = new Worker(‘worker-1.js’); const worker2 = new Worker(‘worker-2.js’);
#PerfMatters @TRENTMWILLIS
Slide 57
// main thread const worker1 = new Worker(‘worker-1.js’); const worker2 = new Worker(‘worker-2.js’); const channel = new MessageChannel();
#PerfMatters @TRENTMWILLIS
Slide 58
// main thread const worker1 = new Worker(‘worker-1.js’); const worker2 = new Worker(‘worker-2.js’); const channel = new MessageChannel(); worker1.postMessage(‘MessagePort’, [channel.port1]); worker2.postMessage(‘MessagePort’, [channel.port2]);
#PerfMatters @TRENTMWILLIS
Slide 59
A Transferable object can be transferred between execution contexts. Normal Object Transferable Object Main Thread
Web Worker #PerfMatters @TRENTMWILLIS
const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]); const result = await processingWorker.process(data); console.log(result);
You can do this entirely off the main thread!
#PerfMatters @TRENTMWILLIS
Conway’s Game of Life canvas-of-life.glitch.me
#PerfMatters @TRENTMWILLIS
Slide 68
So janky!
#PerfMatters @TRENTMWILLIS
Slide 69
Much better!
#PerfMatters @TRENTMWILLIS
Slide 70
You can do a LOT with Web Workers, but…
#PerfMatters @TRENTMWILLIS
Slide 71
Problem: Workers are difficult to test
How do we test them?
#PerfMatters @TRENTMWILLIS
Slide 72
Problem: Workers are difficult to test
A Tale of Two Strategies
#PerfMatters @TRENTMWILLIS
Slide 73
Problem: Workers are difficult to test
Solution #1: Run the testing framework and worker in the same thread
#PerfMatters @TRENTMWILLIS
Slide 74
Solution #1: Run the testing framework and worker in the same thread // Main <script <script <script
thread src=”test-framework.js”></script> src=”worker.js”></script> src=”tests.js”></script>
// Or, worker thread importScripts(‘test-framework.js’, ‘worker.js’); // Your tests here…
#PerfMatters @TRENTMWILLIS
Slide 75
Solution #1: Run the testing framework and worker in the same thread
That is NOT how Workers are used.
#PerfMatters @TRENTMWILLIS
Slide 76
Problem: Workers are difficult to test
Solution #2: Treat your Worker as a Function
#PerfMatters @TRENTMWILLIS
Slide 77
Solution #2: Treat your Worker as a Function test(‘transforms data’, async (assert) => { const worker = new Worker (‘transform.js’); const data = [1, 2, 3]; const result = postMessage(worker, data); assert.equal(result, I'm transformed!); });
#PerfMatters @TRENTMWILLIS
Slide 78
Solution #2: Treat your Worker as a Function
Sub-Problem: How do we mock/stub calls from a Worker?
#PerfMatters @TRENTMWILLIS
Slide 79
Sub-Problem: How do we mock/stub calls from a Worker?
worker-box github.com/trentmwillis/worker-box
#PerfMatters @TRENTMWILLIS
Slide 80
Sub-Problem: How do we mock/stub calls from a Worker?
canvas-of-life.glitch.me/tests
#PerfMatters @TRENTMWILLIS
Slide 81
Thank you!
Web Workers are powerful, but avoid using them directly, instead stand on the shoulders of giants. Let’s get out of our users’ way and give them better experiences! #PerfMatters @TRENTMWILLIS
Slide 82
• • • • • • • • • • •
Resources
Spider icon made by Freepik from www.fl aticon.com Web Workers spec: www.w3.org/TR/workers/ Promise Worker: github.com/nolanlawson/promise-worker Comlink: github.com/GoogleChromeLabs/comlink Workerize: github.com/developit/workerize ImportFromWorker: github.com/GoogleChromeLabs/import-from-worker Greenlet: github.com/developit/greenlet Lumen: bit.ly/netfl ix-lumen Worker DOM: github.com/ampproject/worker-dom Game of Life Demo: canvas-of-life.glitch.me Worker Box: github.com/trentmwillis/worker-box #PerfMatters @TRENTMWILLIS