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

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

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

Cool Web App That Pays The Bills #PerfMatters @TRENTMWILLIS

#PerfMatters @TRENTMWILLIS

#PerfMatters @TRENTMWILLIS

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

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

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

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

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

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

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

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

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

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

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

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

// worker thread self.addEventListener( ‘message’, event => { console.log(event.data); self.postMessage(message); }; ); #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

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

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

PROBLEMS #PerfMatters @TRENTMWILLIS

Problem: It is hard to know when a worker’s task is complete When am I done? worker.postMessage(‘doTask’); #PerfMatters @TRENTMWILLIS

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

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

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

Problem: Workers can not be dynamically defined If only this was possible… const worker = new Worker(data => { // Expensive data operations… return processedData; }); #PerfMatters @TRENTMWILLIS

SOLUTIONS #PerfMatters @TRENTMWILLIS

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 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

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

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

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

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

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

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

Solution: Expose Worker methods as main thread functions const data = await Promise.all([ backendOneWorker.fetch(‘first’), backendTwoWorker.fetch(‘second’) ]); #PerfMatters @TRENTMWILLIS

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

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

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

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

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

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

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

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

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 bit.ly/netflix-lumen #PerfMatters @TRENTMWILLIS

Lumen #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

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

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

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

MessageChannel consists of 2 MessagePorts #PerfMatters @TRENTMWILLIS

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

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

// 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

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

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

// 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

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

Non-Blocking Canvas Graphics #PerfMatters @TRENTMWILLIS

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

Non-Blocking DOM Operations #PerfMatters @TRENTMWILLIS

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

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

So janky! #PerfMatters @TRENTMWILLIS

Much better! #PerfMatters @TRENTMWILLIS

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

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

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

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

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

Solution #1: Run the testing framework and worker in the same thread That is NOT how Workers are used. #PerfMatters @TRENTMWILLIS

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

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

Solution #2: Treat your Worker as a Function Sub-Problem: How do we mock/stub calls from a Worker? #PerfMatters @TRENTMWILLIS

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

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

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

• • • • • • • • • • • 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