A Comparison of Web Workers
Thomas Hunter II
CEO, Working on Radar.Chat
Adapted from Multithreaded JavaScript:
bit.ly/3mvoSjp
Multithreading in JavaScript
It is the nature of JavaScript – and its
ecosystem – to be single-threaded.
- For the longest time no multithreading in JS
- Could pull off basic message passing via iframes
- Now we have Web Workers and shared memory
- Presentation from perspective of multithreading
What is a JavaScript Environment?
- Isolated collection of variables, globals
- Different versions of
Object
, prototypes
- Each environment incurs overhead (~6MB Node.js)
- Object instances cannot be shared
- Serialized versions, JSON, can be passed
- No Web Workers can access DOM,
document
SharedArrayBuffer
data can be shared
- Hand-waving over complexities of contexts/realms
What is a Dedicated Worker?
- Dedicated Workers have exactly one parent
- Can be loaded as a hierarchy
- Each worker is a new JavaScript environment
Dedicated Workers in the Page
console.log('hello from main.js');
const worker = new Worker('worker.js');
worker.onmessage = (msg) => {
console.log('from worker:', msg.data);
};
worker.postMessage('message to worker');
console.log('hello from end of main.js');
Dedicated Workers in the Worker
// worker.js
console.log('hello from worker.js');
self.onmessage = (msg) => {
console.log('from main:', msg.data);
// perform a heavy calculation
postMessage('message from worker');
};
Dedicated Worker Output
Log | Location |
hello from main.js |
main.js |
hello from end of main.js |
main.js |
hello from worker.js |
worker.js |
from main: message to worker |
worker.js |
from worker: message from worker
| main.js |
Why use a Dedicated Worker?
- Gives access to an additional thread
- Offload CPU intensive work
- Prevent scroll-jank
- Note: Worker dies when parent dies
What is a Shared Worker?
- Shared Workers can have multiple parents
- Allows communication across same-origin windows
Shared Workers in the Page(s)
// red.html and blue.html
const worker = new SharedWorker('shared.js');
worker.port.onmessage = (event) => {
console.log('EVENT', event.data);
};
worker.port.postMessage('hello, world');
Shared Workers in the Worker
const ID = Math.floor(Math.random() * 999999);
console.log('shared.js', ID);
const ports = new Set();
self.onconnect = (event) => {
const port = event.ports[0];
ports.add(port);
console.log('CONN', ID, ports.size);
port.onmessage = (event) => {
console.log('MESSAGE', ID, event.data);
for (let p of ports) {
p.postMessage([ID, event.data]);
}
};
};
Shared Worker Output
Log | Location |
shared.js 123456 | shared.js |
CONN 123456 1 | shared.js |
CONN 123456 2 | shared.js |
MESSAGE 123456 hello, world | shared.js |
EVENT [ 123456, "hello, world" ] | red.html |
EVENT [ 123456, "hello, world" ] | blue.html |
Why use a Shared Worker?
- You need to communicate across pages
- You want variable contexts to outlive a page
- You want a cross-page singleton source of truth
- ❌ You don't need to support Safari
- Consider
BroadcastChannel
as an alternative
- Note: Worker dies when last parent dies
What is a Service Worker?
- The most complex of the Web Workers
- Intercept / proxy requests made to server
- Can have zero parents, run in background
- Can share state between same-origin windows
Service Workers in the Page(s)
navigator.serviceWorker.register('/sw.js', {
scope: '/' // URL range that worker can control
});
navigator.serviceWorker.oncontrollerchange = () => {
console.log('controller change');
};
async function makeRequest() {
const result = await fetch('/data.json');
const payload = await result.json();
console.log(payload);
}
Service Workers in the Worker
// sw.js part 1
let counter = 0;
self.oninstall = (event) => {
console.log('service worker install');
};
self.onactivate = (event) => {
console.log('service worker activate');
// allow immediate control of opened pages
event.waitUntil(self.clients.claim());
};
Service Workers in the Worker
// sw.js, part 2
self.onfetch = (event) => {
console.log('fetch', event.request.url);
if (event.request.url.endsWith('/data.json')) {
counter++;
return void event.respondWith(
new Response(JSON.stringify({counter}), {
headers: { 'Content-Type': 'text/json' }
})
);
}
// fallback to normal HTTP request
event.respondWith(fetch(event.request));
};
Service Worker Output
Log | Location |
service worker install | sw.js |
service worker activate | sw.js |
controller change | main.js |
makeRequest(); |
fetch http://localhost:5000/data.json | sw.js |
Object { counter: 1 } | main.js |
Why use a Service Worker?
- Cache network assets when offline
- Perform background syncs of updated content
- Push notifications
- PWA / "Add to Homescreen" on Android & iOS
- Note: Worker might die when last parent dies
Web Worker Comparison Matrix
| Dedicated | Shared | Service |
Thread | ✅ | ✅ | ✅ |
No HTTPS | ✅ | ✅ | ❌ |
Safari | ✅ | ❌ | ✅ |
HTTP Proxy | ❌ | ❌ | ✅ |
Parents | 1 | >= 1 | >= 0 |
Death | With Parent | Last Parent | Tricky |
A Comparison of Web Workers