with React and Web Workers
Mehdi Vasigh
Senior Engineer @ Mailchimp
♥️ Creative coding
👩🍳 UI chef
I love meeting people!
Twitter: @mehdi_vasigh
@mehdi_vasigh
@mehdi_vasigh
@mehdi_vasigh
We want to target 60 frames each second*
Browser takes ~4-6 ms per frame to do work
Our JS for each frame should take
~10-12 ms or less
* This is a generalization! The browser optimizes its rendering to avoid unnecessary work.
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
https://developers.google.com/web/fundamentals/performance/rendering
16 ms
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
Browser can skip steps if nothing changes
16 ms
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
16 ms
UI/Local Logic
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
Front-end Framework
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
16 ms
CSS-in-JS Runtime
UI/Local Logic
Front-end Framework
State Management Logic
We've exceeded our frame budget and introduced jank
@mehdi_vasigh
JavaScript
Style
Layout
Paint
Composite
16 ms
The more blocking JavaScript that we have, the more frames that we will drop
@mehdi_vasigh
@mehdi_vasigh
@mehdi_vasigh
CPU Core
Task 1
Task 2
Concurrency
CPU Core
Task 1
Parallelism
CPU Core
Task 2
@mehdi_vasigh
@mehdi_vasigh
while (true) {
console.log('To infinity and beyond!');
}
// Will never run!
extremelyImportantFunction();
@mehdi_vasigh
Web Workers allow us to do work in parallel using separate threads
Web Workers are not the same as Service Workers!
@mehdi_vasigh
@mehdi_vasigh
Main Thread
Worker
self.postMessage()
MessageEvent
worker.postMessage()
MessageEvent
@mehdi_vasigh
Source: caniuse.com
// worker.js
function handleMessage(event) {
if (event.data === 'ping') {
self.postMessage('pong!');
}
}
self.addEventListener('message', handleMessage);
// main.js
const worker = new Worker('worker.js');
worker.addEventListener('message', event => {
console.log(event.data);
});
worker.postMessage('ping'); // "pong!"
"main.js" and "worker.js" are separate files
@mehdi_vasigh
// Vite allows you to import Workers as constructors
// by adding '?worker' to the end of the path!
import PingWorker from './ping.worker?worker';
const worker = new PingWorker();
worker.postMessage('ping');
// Parcel allows you to pass a relative path to a module
// to the Worker constructor
const worker = new Worker('./ping.worker');
worker.postMessage('ping');
A bundler can help you emit separate files for your main bundle and your Worker bundle.
Different bundlers and frameworks support Workers in different ways!
@mehdi_vasigh
import { useQuery } from 'react-query';
import MarkdownWorker from './markdown.worker?worker'
const worker = new MarkdownWorker();
// Our Worker renders Markdown to HTML. This function
// wraps the exchange in a Promise (simplified code)
function renderMarkdownToHtml(markdown) {
return new Promise((resolve) => {
worker.onmessage = e => {
const html = e.data;
resolve(html)
}
worker.postMessage(markdown);
})
}
function MarkdownPreview({ markdown }) {
const { data } = useQuery(['markdown', markdown], () => {
return renderMarkdownToHtml(markdown)
});
return <div dangerouslySetInnerHTML={{ __html: data || '' }} />;
}
React Query is great at managing async functions. We can take advantage of that with Worker tasks as well!
// worker.js
import * as Comlink from 'comlink';
class MyClass {
ping() {
return 'pong!';
}
}
Comlink.expose(MyClass);
// main.js
import * as Comlink from 'comlink';
import PingWorker from './ping.worker?worker';
const MyClass = Comlink.wrap(new PingWorker());
const instance = await new MyClass();
await instance.ping(); // pong!
Comlink takes away much of the boilerplate code of Web Workers, helping you write code that feels more natural.
@mehdi_vasigh
@mehdi_vasigh
Questions? Ask me now or on Twitter!