Comlink: turning postMessage into an RPC call for WebWorkers
Comlink makes WebWorkers enjoyable.
At a glance
- What is it?
- Comlink is a 1.1 kB library from GoogleChromeLabs that wraps WebWorkers, SharedWorkers and other postMessage endpoints in an ES6 Proxy so remote values look local. It is for front-end engineers who want work off the UI thread without writing a message protocol by hand.
- Who is it for?
- Adopt Comlink if you already need WebWorkers and want to stop writing message-type switchboards by hand, and if your target browsers have ES6 Proxy (the README lists Chrome 56+, Edge 15+, Firefox 52+, Opera 43+, Safari 10.1+, Samsung Internet 6.0+). Do not adopt it if you need a documented versioning policy or a maintained release cadence: the newest release in the repository is v4.4.2 from 2024-11-07, and the README does not describe how upgrades should be rolled out.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem Comlink solves: postMessage is a protocol, not an API
A WebWorker gives you a second thread. It does not give you a way to call into that thread. The WebWorker API hands you postMessage and a message event, which means every interaction between the main thread and the worker has to be encoded as a message with a shape both sides agree on, and every reply has to be matched back to the call that produced it. For a single function that is fine. For an object with a handful of methods, across several workers, the bookkeeping becomes the bulk of the code.
Comlink replaces that bookkeeping with an ES6 Proxy. The README describes it as an RPC implementation for postMessage and ES6 Proxies, and that is the whole idea: one side exposes a value, the other side wraps the endpoint and gets back a proxy that mirrors the exposed value. The stated audience is anyone who wants the main thread idle so the page stays responsive, with the README pointing specifically at low-end mobile phones. The README also states the size, about 2.5 kB raw, about 1.2 kB gzipped, about 1.1 kB brotli, so the cost of the abstraction is small relative to the worker code you would otherwise write.
How the proxy and the message channel fit together
Two functions carry the design. Comlink.expose(value, endpoint?, allowedOrigins?) publishes a value on an endpoint, where the endpoint is anything with a postMessage-like interface. Comlink.wrap(endpoint) takes the other end of that channel and returns a proxy that has the properties and functions of the exposed value. The README is explicit that access and invocation are asynchronous, so a function returning a number returns a promise for a number, and it offers the rule of thumb that if you are using the proxy you should put await in front of it. Thrown exceptions are caught on the worker side and re-thrown on the calling side, which is what makes the proxy usable inside ordinary try/catch code.
The default data path is structured cloning. Every function parameter, return value and object property value is copied, which the README compares to deep copying and notes has limitations, pointing at the structured clone table. Two escape hatches exist. Comlink.transfer(value, transferables) moves a value instead of copying it, provided it is or contains a Transferable, and the README's example wraps a Uint8Array and passes [data.buffer]. Comlink.proxy(value) neither copies nor transfers, sending a proxy so both threads work on the same value; the README recommends it for callbacks, since functions are neither structured cloneable nor transferable. The optional allowedOrigins argument on expose takes an array of strings or RegExps and defaults to the special case of ['*'] for all origins, which is worth noticing: the permissive case is the default.
Installing Comlink and running a counter in a worker
The README gives one install command, from npm:
npm install --save comlinkThe published package exposes dist/umd/comlink.js as main, dist/esm/comlink.mjs as module and dist/umd/comlink.d.ts as types, so a bundler picks up the ESM build and TypeScript gets declarations without extra configuration. The README examples load the library from unpkg rather than from node_modules, which is the quickest way to try it, but a project with a build step should import the npm package instead.
The first real use is the counter example. On the main thread, create the worker, wrap it, and read a property through the proxy:
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
async function init() {
const worker = new Worker("worker.js");
const obj = Comlink.wrap(worker);
alert(`Counter: ${await obj.counter}`);
await obj.inc();
alert(`Counter: ${await obj.counter}`);
}
init();Inside the worker, define the object and expose it. Note that the worker loads the UMD build with importScripts, because the README's worker example is a classic worker, not a module worker:
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
const obj = {
counter: 0,
inc() {
this.counter++;
},
};
Comlink.expose(obj);What you should see is two alerts, the first showing 0 and the second showing 1, with the increment happening on the worker thread. If you use a SharedWorker instead, the README requires two changes: pass worker.port to Comlink.wrap, and call Comlink.expose inside the onconnect callback with event.ports[0] as the endpoint. It also mentions that Chrome exposes shared worker DevTools at chrome://inspect/#workers, which is where you would look when the proxy appears to return nothing.
Where Comlink stops: cloning, browser support and the missing upgrade story
The most common failure is not a bug in Comlink, it is the structured clone algorithm doing exactly what it says. Send a class instance, a function, or a DOM node as an argument and the copy either fails or arrives stripped of its prototype. The README addresses this by pointing at the structured clone table and by offering transfer() and proxy(), but it does not enumerate the cases that break. The repository root contains structured-clone-table.md, so the detail exists, just not in the README's main flow.
Browser support is a hard boundary. The README lists Chrome 56+, Edge 15+, Firefox 52+, Opera 43+, Safari 10.1+ and Samsung Internet 6.0+, and states that browsers without ES6 Proxy support can use the proxy-polyfill. That polyfill is a real dependency with its own behaviour, and the README does not describe what degrades when the proxy is emulated rather than native, so support for old browsers is a claim to verify rather than take on faith.
Release cadence is the other thing to weigh. The most recent release listed is v4.4.2 from 2024-11-07, after v4.4.1 and v4.4.0 in February 2023. The repository is not archived and the last push was on 2026-09-18, so work is happening, but the README does not document a versioning policy, a deprecation process, or a rollback path, and the CHANGELOG.md is the only place to look for what changed between versions. If your project needs a documented migration story before it takes a dependency, Comlink does not currently supply one.
Comlink versus writing your own postMessage layer
The alternative is not another library so much as the thing Comlink abstracts away: a hand-written message protocol. It usually looks like a switch on a type field, a map of pending call IDs to resolvers, and a matching switch on the worker side. That approach has real advantages. Every message is explicit, so nothing crosses the thread boundary that you did not write down. It is also easy to version, easy to log, and easy to test without a browser, and it works on any browser that has WebWorkers, with no proxy requirement at all.
The difference in approach is where the complexity sits. Comlink moves it into a proxy, so call sites read like async function calls and the message shapes are generated for you. A hand-written layer keeps the complexity visible in the protocol definition. For a worker with two methods, the hand-written version is smaller and has no polyfill question. For a worker exposing an object with many methods, or several workers with overlapping interfaces, the proxy version stops being a convenience and starts being the reason the code is readable. Comlink's own README examples are small on purpose, and the docs/examples directory is where the larger shapes live; it is worth reading those before deciding that your case is the simple one.
Maintenance, licence and the cost of upgrading
Comlink is published under Apache-2.0, and the LICENSE file sits in the repository root. Apache-2.0 permits commercial and closed-source use and includes an express patent grant, with the usual obligations around retaining notices and stating changes. That is a general description of the licence, not legal advice; if you redistribute the library or modify it, read the LICENSE file and the NOTICE handling requirements yourself.
Upgrade cost is low in the ordinary sense and unclear in the formal one. The runtime is about 1.1 kB brotli according to the README, so there is little to gain by deferring updates on size grounds. The package has no runtime dependencies, only devDependencies such as rollup, typescript, karma and mocha, so installing it does not pull a tree of transitive packages. What is missing is a stated compatibility policy: the README does not say which versions are supported, whether breaking changes follow semver, or how a caller should detect a proxy that has stopped responding. The version numbers in package.json and the CHANGELOG.md are the only signals available, which means the upgrade decision rests on reading the changelog rather than on a documented guarantee.
Editorial conclusion
Adopt Comlink if you already need WebWorkers and want to stop writing message-type switchboards by hand, and if your target browsers have ES6 Proxy (the README lists Chrome 56+, Edge 15+, Firefox 52+, Opera 43+, Safari 10.1+, Samsung Internet 6.0+). Do not adopt it if you need a documented versioning policy or a maintained release cadence: the newest release in the repository is v4.4.2 from 2024-11-07, and the README does not describe how upgrades should be rolled out. Before committing, read the transfer handler section of the README and the structured-clone-table.md file in the repository root, because the copy-versus-transfer decision is where most of the surprises live.
Frequently asked questions
What does Comlink do?
It turns the postMessage API of WebWorkers into an RPC interface using ES6 Proxies, so values exposed in one thread can be used from the other as if they were local, with all access and invocation being asynchronous. The README describes it as a tiny library, about 1.1 kB brotli, that hides the fact that you are working with a worker.
How do I install Comlink?
The README gives a single npm command, npm install --save comlink. The published package points main at dist/umd/comlink.js, module at dist/esm/comlink.mjs and types at dist/umd/comlink.d.ts, so bundlers and TypeScript pick up the right build.
Why does a function called through a Comlink proxy return a promise?
Because the proxy crosses a thread boundary, so access and invocation are inherently asynchronous. The README's rule of thumb is that if you are using the proxy, put await in front of it, and it notes that exceptions are caught and re-thrown on the other side.
How does Comlink handle callbacks and functions passed between threads?
Functions are neither structured cloneable nor transferable, so the README recommends wrapping them with Comlink.proxy(value). That sends a proxy instead of copying or transferring, and both threads then work on the same value.
Does Comlink work with a SharedWorker?
Yes, with two requirements from the README: use the SharedWorker's port property when calling Comlink.wrap, and call Comlink.expose inside the onconnect callback, passing event.ports[0] as the endpoint.
Can Comlink move data between threads instead of copying it?
Yes, if the value is or contains a Transferable. The README shows Comlink.transfer(data, [data.buffer]) with a Uint8Array, which transfers rather than structured-clones the buffer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/googlechromelabs-comlink)