Weaving Webs of Workers

A presentation at JS Kongress in March 2019 in Munich, Germany by Trent Willis

Slide 1

Slide 1

WEAVING WEBS F WORKERS @TRENTMWILLIS #JSKongress

Slide 2

Slide 2

Hallo JS Kongress! @TRENTMWILLIS #JSKongress

Slide 3

Slide 3

Hallo JS Kongress! Raise your ✋ if you work on a web application. @TRENTMWILLIS #JSKongress

Slide 4

Slide 4

Cool Web App That Pays The Bills @TRENTMWILLIS #JSKongress

Slide 5

Slide 5

Cool Web App That Pays The Bills @TRENTMWILLIS #JSKongress

Slide 6

Slide 6

Cool Web App That Pays The Bills @TRENTMWILLIS #JSKongress

Slide 7

Slide 7

Cool Web App That Pays The Bills @TRENTMWILLIS #JSKongress

Slide 8

Slide 8

@TRENTMWILLIS #JSKongress

Slide 9

Slide 9

@TRENTMWILLIS #JSKongress

Slide 10

Slide 10

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

Slide 11

Slide 11

Web Workers. They help, but also complicate. @TRENTMWILLIS #JSKongress

Slide 12

Slide 12

WEAVING WEBS F WORKERS @TRENTMWILLIS #JSKongress

Slide 13

Slide 13

WEAVING WEBS F WORKERS @TRENTMWILLIS #JSKongress

Slide 14

Slide 14

@TRENTMWILLIS Senior UI Engineer at Netflix y e d i p S @TRENTMWILLIS #JSKongress

Slide 15

Slide 15

The Web Workers API “allows Web application authors to spawn background workers running scripts in parallel to their main page.” @TRENTMWILLIS #JSKongress

Slide 16

Slide 16

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

Slide 17

Slide 17

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

Slide 18

Slide 18

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

Slide 19

Slide 19

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

Slide 20

Slide 20

// main thread worker;; @TRENTMWILLIS #JSKongress

Slide 21

Slide 21

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

Slide 22

Slide 22

// worker thread self // WorkerGlobalScope @TRENTMWILLIS #JSKongress

Slide 23

Slide 23

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

Slide 24

Slide 24

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

Slide 25

Slide 25

Messaging is the bulk of the Web Workers API you need! // main thread worker.addEventListener( ‘message’, event => console.log(event.data) ); @TRENTMWILLIS #JSKongress

Slide 26

Slide 26

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

Slide 27

Slide 27

PROBLEMS @TRENTMWILLIS #JSKongress

Slide 28

Slide 28

PROBLEM Knowing when a task is completed @TRENTMWILLIS #JSKongress

Slide 29

Slide 29

PROBLEM Management and coordination of multiple workers @TRENTMWILLIS #JSKongress

Slide 30

Slide 30

PROBLEM Difficult to test @TRENTMWILLIS #JSKongress

Slide 31

Slide 31

PROBLEM No dynamic definition of workers @TRENTMWILLIS #JSKongress

Slide 32

Slide 32

SOLUTIONS @TRENTMWILLIS #JSKongress

Slide 33

Slide 33

PROBLEM Knowing when a task is completed @TRENTMWILLIS #JSKongress

Slide 34

Slide 34

PROBLEM Knowing when a task is completed Turn messages into Promises @TRENTMWILLIS Replace one platform feature with another! #JSKongress

Slide 35

Slide 35

PROBLEM Knowing when a task is completed 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); }); @TRENTMWILLIS #JSKongress

Slide 36

Slide 36

PROBLEM Knowing when a task is completed SOLUTION Turn messages into Promises postMessage(worker, data) data).then(response response => console.log(response) console.log(response)); @TRENTMWILLIS #JSKongress

Slide 37

Slide 37

PROBLEM Knowing when a task is completed SOLUTION Turn messages into Promises const response = await postMessage(worker, data); console.log(response); @TRENTMWILLIS #JSKongress

Slide 38

Slide 38

PROBLEM Knowing when a task is completed SOLUTION Turn messages into Promises promise-worker github.com/nolanlawson/promise-worker @TRENTMWILLIS #JSKongress

Slide 39

Slide 39

PROBLEM Management and coordination of multiple workers @TRENTMWILLIS #JSKongress

Slide 40

Slide 40

PROBLEM Management and coordination of multiple workers Use Promises (again) @TRENTMWILLIS #JSKongress

Slide 41

Slide 41

PROBLEM Management and coordination of multiple workers Expose Worker methods as main thread functions @TRENTMWILLIS #JSKongress

Slide 42

Slide 42

PROBLEM Management and coordination of multiple workers SOLUTION Expose Worker methods as main thread functions backendOneWorker backendTwoWorker @TRENTMWILLIS #JSKongress

Slide 43

Slide 43

PROBLEM Management and coordination of multiple workers SOLUTION Expose Worker methods as main thread functions const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]); @TRENTMWILLIS #JSKongress

Slide 44

Slide 44

PROBLEM Management and coordination of multiple workers 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); @TRENTMWILLIS #JSKongress

Slide 45

Slide 45

PROBLEM Management and coordination of multiple workers 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! @TRENTMWILLIS #JSKongress

Slide 46

Slide 46

PROBLEM Management and coordination of multiple workers SOLUTION Expose Worker methods as main thread functions Workerize github.com/developit/workerize @TRENTMWILLIS #JSKongress

Slide 47

Slide 47

PROBLEM No dynamic definition of workers @TRENTMWILLIS #JSKongress

Slide 48

Slide 48

PROBLEM No dynamic definition of workers Create Workers from Blob URLs of functions @TRENTMWILLIS #JSKongress

Slide 49

Slide 49

PROBLEM No dynamic definition of workers 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); }; @TRENTMWILLIS #JSKongress

Slide 50

Slide 50

PROBLEM No dynamic definition of workers SOLUTION Create Workers from Blob URLs of functions greenlet github.com/developit/greenlet @TRENTMWILLIS #JSKongress

Slide 51

Slide 51

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

Slide 52

Slide 52

Lumen @TRENTMWILLIS #JSKongress

Slide 53

Slide 53

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.” @TRENTMWILLIS #JSKongress

Slide 54

Slide 54

This is how we “weave” a web of Web Workers! Worker-To-Worker Communication @TRENTMWILLIS #JSKongress

Slide 55

Slide 55

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

Slide 56

Slide 56

MessageChannel @TRENTMWILLIS #JSKongress

Slide 57

Slide 57

MessageChannel consists of 2 MessagePorts @TRENTMWILLIS #JSKongress

Slide 58

Slide 58

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

Slide 59

Slide 59

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

Slide 60

Slide 60

// 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]); @TRENTMWILLIS #JSKongress

Slide 61

Slide 61

Transferable “[A Transferable] represents an object that can be transferred between different execution contexts, like the main thread and Web Workers.” @TRENTMWILLIS #JSKongress

Slide 62

Slide 62

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

Slide 63

Slide 63

// 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’); }; }); @TRENTMWILLIS #JSKongress

Slide 64

Slide 64

const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]); const result = await processingWorker.process(data); console.log(result); @TRENTMWILLIS You can do this entirely off the main thread! #JSKongress

Slide 65

Slide 65

Non-Blocking Canvas Graphics @TRENTMWILLIS #JSKongress

Slide 66

Slide 66

OffscreenCanvas @TRENTMWILLIS #JSKongress

Slide 67

Slide 67

Non-Blocking DOM Manipulation @TRENTMWILLIS #JSKongress

Slide 68

Slide 68

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

Slide 69

Slide 69

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

Slide 70

Slide 70

@TRENTMWILLIS So janky! #JSKongress

Slide 71

Slide 71

@TRENTMWILLIS Much better! #JSKongress

Slide 72

Slide 72

You Can Do A LOT With Web Workers… @TRENTMWILLIS #JSKongress

Slide 73

Slide 73

PROBLEM Difficult to test How do we test them? @TRENTMWILLIS #JSKongress

Slide 74

Slide 74

PROBLEM Difficult to test A Tale of Two Strategies @TRENTMWILLIS #JSKongress

Slide 75

Slide 75

PROBLEM Difficult to test Run your testing framework and worker in the same thread @TRENTMWILLIS #JSKongress

Slide 76

Slide 76

PROBLEM Difficult to test // Main <script <script <script @TRENTMWILLIS thread src=”test-framework.js”></script> src=”worker.js”></script> src=”tests.js”></script> #JSKongress

Slide 77

Slide 77

PROBLEM Difficult to test // 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… @TRENTMWILLIS #JSKongress

Slide 78

Slide 78

PROBLEM Difficult to test That is NOT how Workers are used. @TRENTMWILLIS #JSKongress

Slide 79

Slide 79

PROBLEM Difficult to test Treat your Worker as a Function @TRENTMWILLIS #JSKongress

Slide 80

Slide 80

PROBLEM Difficult to test SOLUTION 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!); }); @TRENTMWILLIS #JSKongress

Slide 81

Slide 81

PROBLEM Difficult to test SOLUTION Treat your Worker as a Function Sub-Problem: How do we mock/stub calls a Worker? @TRENTMWILLIS #JSKongress

Slide 82

Slide 82

PROBLEM Difficult to test SOLUTION Treat your Worker as a Function worker-box github.com/trentmwillis/worker-box @TRENTMWILLIS #JSKongress

Slide 83

Slide 83

canvas-of-life.glitch.me/tests @TRENTMWILLIS #JSKongress

Slide 84

Slide 84

Web Workers are powerful @TRENTMWILLIS #JSKongress

Slide 85

Slide 85

Web Workers are powerful, but avoid using them directly @TRENTMWILLIS #JSKongress

Slide 86

Slide 86

Web Workers are powerful, but avoid using them directly, instead stand on the shoulders of giants. @TRENTMWILLIS #JSKongress

Slide 87

Slide 87

Thank you! @TRENTMWILLIS Web Workers are powerful, but avoid using them directly, instead stand on the shoulders of giants. There is no better time to start than right now. #JSKongress

Slide 88

Slide 88

• • • • • • • • • Resources Spider icon made by Freepik from www.flaticon.com Web Workers spec: www.w3.org/TR/workers/ Promise Worker: github.com/nolanlawson/promise-worker Workerize: github.com/developit/workerize Greenlet: github.com/developit/greenlet Lumen: bit.ly/netflix-lumen Worker DOM: github.com/ampproject/worker-dom Game of Life Demo: canvas-of-life.glitch.me Worker Box: github.com/trentmwillis/worker-box @TRENTMWILLIS #JSKongress