@jialin.huang
FRONT-ENDBACK-ENDNETWORK, HTTPOS, COMPUTERCLOUD, AWS, Docker
To live is to risk it all Otherwise you are just an inert chunk of randomly assembled molecules drifting wherever the Universe blows you

© 2024 jialin00.com

Original content since 2022

back
RSS

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

  1. window.postMessage()
  1. 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

  1. one page(tab), one web worker
  1. 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.

EOF