postMessage and Transferables
How postMessage copies data — structured clone
Section titled “How postMessage copies data — structured clone”By default, postMessage deep-clones the value using the structured clone algorithm. This is more capable than JSON.stringify: it handles circular references, TypedArray, Blob, Map, Set, Date, and more. The original object on the sending side is completely untouched — both sides end up with independent copies.
The cost of structured clone is proportional to the size of the data. Cloning a few small objects is negligible. Cloning a 10 MB ArrayBuffer means copying 10 MB across thread boundaries — measurably slow for real-time workloads.
const arr = [1, 2, 3];worker.postMessage(arr); // arr is cloned — original is unchangedconsole.log(arr); // [1, 2, 3] — still hereWhat structured clone cannot copy: functions and DOM nodes. Trying to send either of these throws a DataCloneError.
Transferables — zero-copy ownership transfer
Section titled “Transferables — zero-copy ownership transfer”To avoid copying large buffers, pass them as transferables in the second argument to postMessage. Instead of cloning, the browser moves ownership of the underlying memory to the receiver. The source side is detached immediately — its byteLength drops to 0 and any attempt to read or write it throws.
const buffer = new ArrayBuffer(1024 * 1024); // 1 MBworker.postMessage(buffer, [buffer]); // transfer ownershipconsole.log(buffer.byteLength); // 0 — detached!The second argument is an array of transferable objects. Every item in that array must also appear as (or inside) the first argument — otherwise the browser won’t know where to route it. You can mix regular data with a transferable in the same message:
worker.postMessage({ kind: 'frame', data: imageBuffer }, [imageBuffer]);When to use transferables
Section titled “When to use transferables”- Large
ArrayBuffers (images, audio, binary blobs) — transferables are orders of magnitude faster than clone for multi-MB data MessagePort— transfer a port to route messages directly between two workers without going through the main threadOffscreenCanvas— transfer canvas rendering to a worker for off-thread 2D or WebGL drawing- For small plain objects, structured clone is fine and simpler — don’t over-engineer
Runnable: transfer an ArrayBuffer, observe detachment
Section titled “Runnable: transfer an ArrayBuffer, observe detachment”This demo creates a 1 MB ArrayBuffer, fills byte 0 with the value 42, then transfers it to a worker. Notice the byteLength printed before and after the transfer call.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Structured clone (default) | Easy to use, no ownership to manage yourself, supports many types | Copies the entire value — time and memory cost scales with buffer size |
Transferables (postMessage(data, [buffer])) | Zero-copy, pointer-only ownership transfer — very fast for large buffers | The source side is detached immediately — you must design the code to never touch that buffer again |
Common mistakes
Section titled “Common mistakes”- Cloning a large
ArrayBufferunnecessarily — if you don’t need the original buffer afterward, transfer it instead withpostMessage(data, [buffer]); it’s faster and uses far less memory than letting structured clone copy the whole thing. - Trying to read or write a buffer after transferring it — the buffer is detached immediately (
byteLengthbecomes 0). Accessing it again doesn’t throw, it just returns empty data — switch your reference to the new buffer the receiving side constructs instead. - Forgetting the second argument to
postMessagemust be an array — passing a bare buffer without wrapping it in[]means no transfer happens at all, and the data is cloned normally instead.