10LOC
Web Platformadvanced

Transferable objects with postMessage for zero-copy worker handoff

Published June 15, 2027

const { port1, port2 } = new MessageChannel();

port2.onmessage = (event: MessageEvent<ArrayBuffer>) => {
  console.log("received", event.data.byteLength, "bytes, zero-copy");
};

const buffer = new ArrayBuffer(1024 * 1024 * 16);
port1.postMessage(buffer, [buffer]);

console.log(buffer.byteLength); // 0 — ownership transferred, not copied

What

postMessage(data, transferList)'s second argument moves ownership of the listed objects to the receiver instead of structured-cloning them — after the call, the sender can no longer use them.

Why it matters

By default, postMessage — to a Worker, through a MessagePort, or to another window — structured-clones its payload. For a large ArrayBuffer (image data, audio samples, a big typed array), that means copying every byte, which is slow and briefly doubles memory use. Passing the buffer in the transfer list instead makes the handoff O(1): the receiver gets the exact same underlying memory, and the sender's reference is detached, its byteLength dropping to 0. It's the difference between sending a copy and handing over the original.

How it works

  • MessageChannel creates a connected pair of ports (port1/port2). The transfer mechanism is identical across Worker.postMessage, window.postMessage, and ports — a channel just demonstrates it in one file, with no separate worker script needed.
  • The second argument to postMessage, [buffer], is the transfer list. Every transferable object in it (ArrayBuffer, MessagePort, ImageBitmap, and a handful of others) is detached from the sender and re-attached on the receiver — no bytes are copied.
  • buffer.byteLength reads 0 immediately after the call, on that same buffer variable — direct proof the sender no longer owns the memory, not just that a copy happened somewhere else.

Gotchas

  • Only certain types are transferable. A plain object, array, or a TypedArray view itself isn't — though the .buffer an array view wraps is. Using an already-transferred buffer afterward throws.
  • If the sender still needs the data after sending it, transfer is the wrong tool — omit the transfer list and let it clone, or send a copy on purpose.
  • Structured cloning still applies to the rest of the message. Only the objects named in the transfer list are moved; everything else in the payload is cloned as usual.

Related

  • Web Worker — the most common place this shows up: handing large binary data to or from a background thread without copying it.
  • Node's worker_threadsworker.postMessage has the same transfer-list mechanic on the server side.