Browser Workers, postMessage and MessageChannel
TL;DR
- Start with simple Web Workers if you just need a thread to help with computation
- Consider Shared Worker if you need cross-page sharing
- Use Service Worker when you need offline functionality, push notifications, caching, proxying, etc. Service Workers are commonly mentioned when talking about PWA
postMessage
for workers
window.postMessage
- window.postMessage()
- Used for communication between windows, commonly for iframe and parent window communication
// main window to spec iframe
const iframe1 = document.getElementById('iframe1');
iframe1.contentWindow.postMessage("hello to iframe1", "https://target1.com");
// window and iframes
// window
// └── iframeX
// └── iframeY
// iframe Y to iframe X
window.parent.postMessage("hello", "https://parent.com");
// iframe Y to root
window.top || window.parent.parent
// window to popup
popup = window.open("https://popup.com");
popup.postMessage("hello", "https://popup.com");
// popup to window
window.opener.postMessage("hello", "https://opener.com");
Browser Workers
- web worker (dedicated worker, one-to-one)
- service worker (PWA, affecting windows by scope)
- shared worker (shared among windows)
- Web Worker and Shared Worker will continue running for a while after the last page or window is closed, before eventually terminating.
- Service Worker will continue running for a while after the page is closed and will terminate once the browser determines it’s no longer needed.
Web Worker for heavy computing
- one page(tab), one web worker
- It can’t be shared. Consider use shared worker.
// main thread
// get web worker
const worker = new Worker('worker.js');
// main thread send data to web worker
worker.postMessage({ data: 'heavy computation' });
// receive data from web worker
worker.onmessage = (event) => {
console.log('From Worker:', event.data);
};
// web worker
// receive data from main thread
self.addEventListener('message', (event) => {
console.log('From Main Thread:', event.data);
// give it back to main thread
const result = `Processed: ${event.data.data}`;
self.postMessage(result);
});
service worker
- One scope (window, page, tab, depends on sw setup), one service worker
/app
: may have its own service worker
/admin
: may have its own sw
- or
/app
and/admin
may share the same scope if scope is set to/
- HTTPS only
- Hijacks requests, like a middleman
// main thread
// register
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.log('SW registration failed'));
// send message to sw
navigator.serviceWorker.ready.then(registration => {
registration.active.postMessage({
from: 'main',
type: 'greeting',
data: 'Hello from Main Thread'
});
});
// receive data from sw
navigator.serviceWorker.addEventListener('message', event => {
console.log('Main thread received:', event.data);
});
// service worker
// installed
self.addEventListener('install', (event) => {
console.log('Service Worker installed.');
self.skipWaiting(); // starting
});
// then activated (after installed)
self.addEventListener('activate', (event) => {
console.log('Service Worker activated.');
return self.clients.claim();
});
// receive data from main thread
self.addEventListener('message', async (event) => {
console.log('SW received:', event.data);
if (event.data && event.data.from === 'main') {
// give it back to main thread
// https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
const clients = await self.clients.matchAll({ type: 'window' });
for (const client of clients) {
client.postMessage({
from: 'sw',
type: 'response',
data: 'Hello back from sw'
});
}
}
});
// hijack request
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname === '/api/data') {
// stop /api/data here.
const fakeResponse = new Response(JSON.stringify({
message: 'self-defined respons',
time: new Date().toISOString()
}), {
headers: { 'Content-Type': 'application/json' }
});
event.respondWith(fakeResponse);
} else {
// next()
event.respondWith(fetch(event.request));
}
});
Shared Worker
Can't use worker.postMessage
, only worker.port.postMessage()
// main thread
const worker = new SharedWorker('/shared.js');
// 與 worker 的連線是透過 port
worker.port.start();
// main thread send
worker.port.postMessage({ type: 'hello', from: 'main thread' });
// main thread receive
worker.port.onmessage = (event) => {
console.log('From shared worker:', event.data);
};
// shared worker for only one window
onconnect = function(e) {
const port = e.ports[0];
// receive from main thread
port.onmessage = function(event) {
console.log('Received from main:', event.data);
// to main thread
port.postMessage({ msg: 'Hello back from shared worker!' });
};
port.start(); // 開始連線
};
Shared worker for multiple windows:
const connections = new Map(); // windowId -> port
onconnect = function(e) {
const port = e.ports[0]; // This is a NEW MessagePort for this connection
let windowId = null;
port.onmessage = function(event) {
// window send message:
// worker.port.postMessage({
// type: 'sendTo', // ← self define
// id: 'windowA', // ← self define
// targetWindow: 'windowD', // ← self define
// message: 'Hello D!' // ← self define
// });
const { type, id, targetWindow, message } = event.data;
if (type === 'register') {
windowId = id; // e.g., 'windowA', 'windowB', 'chat-room-1'
connections.set(windowId, port);
} else if (type === 'sendTo') {
// Send to specific window by ID (not array index!)
const targetPort = connections.get(targetWindow);
if (targetPort) {
targetPort.postMessage({ from: windowId, message });
}
} else if (type === 'broadcast') {
// Send to all connected windows
connections.forEach((p, id) => {
if (id !== windowId) { // don't send to self
p.postMessage({ from: windowId, message });
}
});
}
};
port.start();
};
MessageChannel
Most workers use postMessage
to talk.
But sometimes we need MessageChannel
to make a private two-way connection that lasts longer, something postMessage alone cannot do.
MessageChannel
is more valuable for shared and service workers since they serve multiple pages and need private channels.
For dedicated workers (web workers), postMessage
is sufficient in 99% of cases.
https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
// normal
iframe.contentWindow.postMessage('hello', 'https://trusted-site.com');
// Any script on the same origin can receive the message
// if it listens to 'message' events.
// with port transferred.
const channel = new MessageChannel();
iframe.contentWindow.postMessage('hello', 'https://trusted-site.com', [channel.port2]);
// Even pages/scripts on the same origin can't access the message
// unless they hold the transferred port (e.g., port2).
Summary
- Web: A background helper that does heavy tasks and doesn’t care about network stuff.
- Shared: A worker that lets different tabs share messages.
- Service: like a gatekeeper that controls all network traffic in and out.