Skip to content

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 unchanged
console.log(arr); // [1, 2, 3] — still here

What 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 MB
worker.postMessage(buffer, [buffer]); // transfer ownership
console.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]);
  • 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 thread
  • OffscreenCanvas — 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.

Browser Storage
OptionBenefitCost
Structured clone (default)Easy to use, no ownership to manage yourself, supports many typesCopies 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 buffersThe source side is detached immediately — you must design the code to never touch that buffer again
  • Cloning a large ArrayBuffer unnecessarily — if you don’t need the original buffer afterward, transfer it instead with postMessage(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 (byteLength becomes 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 postMessage must be an array — passing a bare buffer without wrapping it in [] means no transfer happens at all, and the data is cloned normally instead.
What algorithm does postMessage use by default to copy data?
After calling `worker.postMessage(buffer, [buffer])`, what is `buffer.byteLength` in the main thread?
Which of the following is NOT a transferable type?