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.
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
政采云技术
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.