Frontend Development 17 min read

Web Multithreading with Comlink: Analysis, Implementation, and Practical Examples

This article explains JavaScript's single‑threaded nature, introduces the event loop and Web Workers, examines the Comlink library for RPC‑style multithreading, provides detailed source‑code analysis, and demonstrates practical use cases such as exporting large Excel files without blocking the UI.

政采云技术
政采云技术
政采云技术
Web Multithreading with Comlink: Analysis, Implementation, and Practical Examples

Introduction : JavaScript runs on a single thread, so heavy tasks can block the UI, but asynchronous APIs and the event loop give the perception of multithreading.

Event Loop : The event loop moves completed async callbacks into a task queue; when the call stack is empty, queued callbacks are executed, preventing main‑thread blockage. CPU‑intensive and I/O‑intensive tasks are distinguished.

Web Worker : Modern browsers support the Web Worker API, which enables true background threads. The article lists typical constraints (DOM/BOM limits, same‑origin policy, message‑based communication, script loading, and resource cleanup).

Simple Example – HTML page and worker script:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <p id="first"></p>
  <p id="second"></p>
  <p id="third"></p>
  <script>
    // 第一个文本
    document.querySelector('#first').innerHTML = 'First'
    // 第二个文本
    const second = document.querySelector('#second')
    if (window.Worker) {
      second.innerHTML = '...'
      const worker = new Worker('worker.js')
      worker.postMessage({ uuid: new Date().getTime() })
      worker.onmessage = function(e) { second.innerHTML = e.data }
      worker.onerror = function(e) { second.innerHTML = 'Error occured!' }
    } else {
      second.innerHTML = 'Not support Web Worker!'
    }
    // 第三个文本
    document.querySelector('#third').innerHTML = 'Third'
  </script>
</body>
</html>
onmessage = function(e) {
  const time = Math.random() * 3000
  // 模拟复杂计算
  setTimeout(() => {
    postMessage(`Second ${time.toFixed(0)} ms, ID is ${e.data.uuid}`)
  }, time)
}

Multiple onmessage handling (illustrates the need for a switch/if inside a single handler):

// 若要在线程脚本中执行多个操作,通常需要这么写
onmessage = function(e) {
  if (condition1) // do something
  if (condition2) // do something
  if (condition3) // do something
  ...
}

Promise‑based async example (fetch‑then):

fetchSometing().then(res => {
  // do something
})

Worker postMessage example :

worker.postMessage();
worker.onmessage = function(e) {
  // do something
}

Comlink Overview : Comlink provides RPC‑style communication for Web Workers. The key calls are Comlink.wrap(worker) on the main thread and Comlink.expose(obj) inside the worker.

Source Code Analysis – wrap :

export function wrap
(ep: Endpoint, target?: any): Remote
{
  return createProxy
(ep, [], target) as any;
}

function createProxy
(
  ep: Endpoint,
  path: (string | number | symbol)[] = [],
  target: object = function() {}
): Remote
{
  let isProxyReleased = false;
  const proxy = new Proxy(target, {
    get(_target, prop) {
      if (prop === "then") {
        if (path.length === 0) {
          return { then: () => proxy };
        }
        const r = requestResponseMessage(ep, {
          type: MessageType.GET,
          path: path.map(p => p.toString()),
        }).then(fromWireValue);
        return r.then.bind(r);
      }
      return createProxy(ep, [...path, prop]);
    },
    set(_target, prop, rawValue) { /* ... */ },
    apply(_target, _thisArg, rawArgumentList) { /* ... */ },
    construct(_target, rawArgumentList) { /* ... */ },
  });
  return proxy as any;
}

Source Code Analysis – expose :

export function expose(obj: any, ep: Endpoint = self as any) {
  ep.addEventListener("message", function callback(ev: MessageEvent) {
    if (!ev || !ev.data) { return }
    const { id, type, path } = { path: [] as string[], ...(ev.data as Message) };
    const argumentList = (ev.data.argumentList || []).map(fromWireValue);
    let returnValue;
    try {
      const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);
      const rawValue = path.reduce((obj, prop) => obj[prop], obj);
      switch (type) {
        case MessageType.GET:
          returnValue = rawValue;
          break;
        case MessageType.SET:
          // ...
          break;
        case MessageType.APPLY:
          // ...
          break;
        case MessageType.CONSTRUCT:
          // ...
          break;
        case MessageType.ENDPOINT:
          // ...
          break;
        case MessageType.RELEASE:
          // ...
          break;
        default:
          return;
      }
    } catch (value) {
      returnValue = { value, [throwMarker]: 0 };
    }
    Promise.resolve(returnValue)
      .catch(value => ({ value, [throwMarker]: 0 }))
      .then(returnValue => {
        const [wireValue, transferables] = toWireValue(returnValue);
        ep.postMessage({ ...wireValue, id }, transferables);
        if (type === MessageType.RELEASE) {
          ep.removeEventListener("message", callback as any);
          closeEndPoint(ep);
        }
      });
  } as any);
  if (ep.start) { ep.start(); }
}

Excel Export Case : Using Comlink to generate a 100 000‑row Excel file in a worker, avoiding UI blockage.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <script src="https://unpkg.com/comlink/dist/umd/comlink.js"></script>
  <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
  <script src="https://unpkg.com/file-saver/dist/FileSaver.min.js"></script>
</head>
<body>
  <button id="btn">Download</button>
  <p id="time"></p>
  <script>
    const button = document.querySelector('#btn');
    const worker = new Worker('worker.js');
    const getWorkBook = Comlink.wrap(worker);
    async function download() {
      button.disabled = true;
      const blob = await getWorkBook(100000);
      saveAs(blob, "test.xlsx");
      button.disabled = false;
    }
    button.addEventListener('click', download);
    setInterval(() => {
      document.querySelector('#time').innerHTML = new Date().toLocaleTimeString();
    }, 1000);
  </script>
</body>
</html>
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
importScripts("https://unpkg.com/xlsx/dist/xlsx.full.min.js");

const getWorkBook = (count) => {
  const aoa = [];
  for (let i = 0; i < count; i++) {
    const arr = [];
    for (let j = 0; j < 10; j++) {
      if (i === 0) {
        arr.push(`Column${j + 1}`);
        continue;
      }
      arr.push(Math.floor(Math.random() * 100));
    }
    aoa.push(arr);
  }
  const wb = XLSX.utils.book_new();
  const ws = XLSX.utils.aoa_to_sheet(aoa);
  XLSX.utils.book_append_sheet(wb, ws, 'Sheet');
  const data = XLSX.write(wb, { type: 'array' });
  return new Blob([data], { type: "application/octet-stream" });
};

Comlink.expose(getWorkBook);

Thoughts : Comlink abstracts the message‑port communication, making RPC over workers clean and extensible. The same pattern can be applied to iframe, window.open, or window.opener communication.

References :

Comlink – https://github.com/GoogleChromeLabs/comlink

Web Workers API – https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API

Web Worker tutorial – http://www.ruanyifeng.com/blog/2018/07/web-worker.html

frontendJavaScriptRPCweb workersmultithreadingComlink
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.