Getting Out Of Our Users’ Way: Less Jank With Web Workers

A presentation at #PerfMatters in April 2020 in by Trent Willis

Slide 1

Slide 1

GETTING OUT OF OUR USERS’ WAY LESS JANK WITH WEB WORKERS #PerfMatters @TRENTMWILLIS

Slide 2

Slide 2

Hello PerfMatters! Think about your web app for a moment… #PerfMatters @TRENTMWILLIS

Slide 3

Slide 3

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

Slide 4

Slide 4

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

Slide 5

Slide 5

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

Slide 6

Slide 6

#PerfMatters @TRENTMWILLIS

Slide 7

Slide 7

#PerfMatters @TRENTMWILLIS

Slide 8

Slide 8

How do we prevent numerous, large, and/or slow data operations from impacting our users? #PerfMatters @TRENTMWILLIS

Slide 9

Slide 9

Web Workers They help, but they complicate #PerfMatters @TRENTMWILLIS

Slide 10

Slide 10

GETTING OUT OF OUR USERS’ WAY LESS JANK WITH WEB WORKERS #PerfMatters @TRENTMWILLIS

Slide 11

Slide 11

@TRENTMWILLIS SENIOR UI ENGINEER AT NETFLIX y e d i p S #PerfMatters @TRENTMWILLIS

Slide 12

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

Slide 13

new Worker(‘worker.js’); #PerfMatters @TRENTMWILLIS

Slide 14

Slide 14

new Worker(‘worker.js’); new SharedWorker(‘worker.js’); #PerfMatters @TRENTMWILLIS

Slide 15

Slide 15

new Worker(‘worker.js’); It’s like a <script> but loads in a different thread! #PerfMatters @TRENTMWILLIS

Slide 16

Slide 16

Web Workers allow “for thread-like operation with message-passing as the coordination mechanism” #PerfMatters @TRENTMWILLIS

Slide 17

Slide 17

// main thread worker.postMessage(message);; #PerfMatters @TRENTMWILLIS

Slide 18

Slide 18

// worker thread self // “window” for a Worker #PerfMatters @TRENTMWILLIS

Slide 19

Slide 19

// worker thread self.addEventListener( ‘message’, event => { console.log(event.data); }; ); #PerfMatters @TRENTMWILLIS

Slide 20

Slide 20

// worker thread self.addEventListener( ‘message’, event => { console.log(event.data); self.postMessage(message); }; ); #PerfMatters @TRENTMWILLIS

Slide 21

Slide 21

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

Slide 22

// main thread worker.terminate(); #PerfMatters @TRENTMWILLIS

Slide 23

Slide 23

Web Worker life-cycle worker.postMessage() new Worker() worker.terminate() worker.addEventListener() main worker self.addEventListener() self.postMessage() #PerfMatters @TRENTMWILLIS

Slide 24

Slide 24

PROBLEMS #PerfMatters @TRENTMWILLIS

Slide 25

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

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

Slide 27

Problem: Workers are difficult to test How do I unit test this? worker.postMessage(‘doNetworkTask’); #PerfMatters @TRENTMWILLIS

Slide 28

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

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

Slide 30

SOLUTIONS #PerfMatters @TRENTMWILLIS

Slide 31

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

Slide 32

Slide 32

Solution: Turn messages into Promises const postMessage = (worker, message) => new Promise(resolve => { const resolution = (event) => { worker.removeEventListener(‘message’, resolution); resolve(event.data); }; worker.addEventListener(‘message’, resolution); worker.postMessage(message); }); #PerfMatters @TRENTMWILLIS

Slide 33

Slide 33

Solution: Turn messages into Promises postMessage(worker, data) data).then(response response => console.log(response) console.log(response)); #PerfMatters @TRENTMWILLIS

Slide 34

Slide 34

Solution: Turn messages into Promises const response = await postMessage(worker, data); console.log(response); #PerfMatters @TRENTMWILLIS

Slide 35

Slide 35

Solution: Turn messages into Promises promise-worker github.com/nolanlawson/promise-worker #PerfMatters @TRENTMWILLIS

Slide 36

Slide 36

Problem: Workers are difficult to manage and coordinate Solution: Use Promises (again) #PerfMatters @TRENTMWILLIS

Slide 37

Slide 37

Problem: Workers are difficult to manage and coordinate Solution: Expose Worker methods as main thread functions #PerfMatters @TRENTMWILLIS

Slide 38

Slide 38

Solution: Expose Worker methods as main thread functions backendOneWorker backendTwoWorker #PerfMatters @TRENTMWILLIS

Slide 39

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

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

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

Slide 42

Solution: Expose Worker methods as main thread functions Comlink github.com/GoogleChromeLabs/comlink #PerfMatters @TRENTMWILLIS

Slide 43

Slide 43

Solution: Expose Worker methods as main thread functions Workerize github.com/developit/workerize #PerfMatters @TRENTMWILLIS

Slide 44

Slide 44

Solution: Expose Worker methods as main thread functions importFromWorker github.com/GoogleChromeLabs/import-from-worker #PerfMatters @TRENTMWILLIS

Slide 45

Slide 45

Problem: Workers can not be dynamically defined Solution: Create Workers from Blob URLs of functions #PerfMatters @TRENTMWILLIS

Slide 46

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

Slide 47

Solution: Create Workers from Blob URLs of functions greenlet github.com/developit/greenlet #PerfMatters @TRENTMWILLIS

Slide 48

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

Slide 49

Slide 49

Lumen bit.ly/netflix-lumen #PerfMatters @TRENTMWILLIS

Slide 50

Slide 50

Lumen #PerfMatters @TRENTMWILLIS

Slide 51

Slide 51

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

Slide 52

VaporBoy (WASMBoy) Runs a WASM-based GameBoy emulator with Web Workers for smooth UI vaporboy.net #PerfMatters @TRENTMWILLIS

Slide 53

Slide 53

We can “weave” a web of Web Workers! Worker-To-Worker Communication #PerfMatters @TRENTMWILLIS

Slide 54

Slide 54

// worker thread const workerInWorker = new Worker(‘worker.js’); #PerfMatters @TRENTMWILLIS

Slide 55

Slide 55

MessageChannel consists of 2 MessagePorts #PerfMatters @TRENTMWILLIS

Slide 56

Slide 56

// main thread const worker1 = new Worker(‘worker-1.js’); const worker2 = new Worker(‘worker-2.js’); #PerfMatters @TRENTMWILLIS

Slide 57

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

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

Slide 59

A Transferable object can be transferred between execution contexts. Normal Object Transferable Object Main Thread Web Worker #PerfMatters @TRENTMWILLIS

Slide 60

Slide 60

// worker thread self.addEventListener(‘message’, (event) => { if (event.ports.length) { }; }); #PerfMatters @TRENTMWILLIS

Slide 61

Slide 61

// worker thread self.addEventListener(‘message’, (event) => { if (event.ports.length) { event.ports[0].onmessage = event => console.log(event.data); event.ports[0].postMessage(‘hello from worker 2’); }; }); #PerfMatters @TRENTMWILLIS

Slide 62

Slide 62

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

Slide 63

Slide 63

Non-Blocking Canvas Graphics #PerfMatters @TRENTMWILLIS

Slide 64

Slide 64

OffscreenCanvas API Allows a <canvas> to be used in a Web Worker #PerfMatters @TRENTMWILLIS

Slide 65

Slide 65

Non-Blocking DOM Operations #PerfMatters @TRENTMWILLIS

Slide 66

Slide 66

worker-dom github.com/ampproject/worker-dom #PerfMatters @TRENTMWILLIS

Slide 67

Slide 67

Conway’s Game of Life canvas-of-life.glitch.me #PerfMatters @TRENTMWILLIS

Slide 68

Slide 68

So janky! #PerfMatters @TRENTMWILLIS

Slide 69

Slide 69

Much better! #PerfMatters @TRENTMWILLIS

Slide 70

Slide 70

You can do a LOT with Web Workers, but… #PerfMatters @TRENTMWILLIS

Slide 71

Slide 71

Problem: Workers are difficult to test How do we test them? #PerfMatters @TRENTMWILLIS

Slide 72

Slide 72

Problem: Workers are difficult to test A Tale of Two Strategies #PerfMatters @TRENTMWILLIS

Slide 73

Slide 73

Problem: Workers are difficult to test Solution #1: Run the testing framework and worker in the same thread #PerfMatters @TRENTMWILLIS

Slide 74

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

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

Slide 76

Problem: Workers are difficult to test Solution #2: Treat your Worker as a Function #PerfMatters @TRENTMWILLIS

Slide 77

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

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

Slide 79

Sub-Problem: How do we mock/stub calls from a Worker? worker-box github.com/trentmwillis/worker-box #PerfMatters @TRENTMWILLIS

Slide 80

Slide 80

Sub-Problem: How do we mock/stub calls from a Worker? canvas-of-life.glitch.me/tests #PerfMatters @TRENTMWILLIS

Slide 81

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

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