In a browser, what is the difference between calling `worker.postMessage(buffer)` and `worker.postMessage(buffer, [buffer])` for an ArrayBuffer — what does the worker receive in each case, and what is left in the sending context?
answer
- Second argument changes copy into move
- Ownership handoff, not a share
- Sender is left holding nothing
- byteLength drops to zero
- Old views detach along with the buffer
basics
~20 sWithout a transfer list the ArrayBuffer is copied, so both sides hold their own bytes. With the buffer in the transfer list its memory is handed over instead: the worker gets the original bytes and the sender's buffer is detached, byteLength 0.
solid answer
~40 s`worker.postMessage(buffer)` serializes the message with the structured clone algorithm, so the worker gets a **copy** — two allocations, and a memcpy proportional to the buffer size. `worker.postMessage(buffer, [buffer])` names the buffer in the transfer list, so ownership of the underlying memory moves to the receiving agent instead of being duplicated. That is O(1) regardless of size: nothing is copied, only the pointer changes hands. The price is that the sender's `ArrayBuffer` is **detached** — `buffer.byteLength` becomes 0, any `TypedArray` view over it reports `length` 0, and further use throws or silently reads nothing. So transfer is a move, not a share: after the call the sender must treat the buffer as gone. The same transfer list is accepted as `postMessage(buffer, { transfer: [buffer] })` and by `structuredClone`.
code
javascript · 12 linesconst src = 'onmessage = (e) => postMessage(e.data.byteLength);';
const url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' }));
const worker = new Worker(url);
const buffer = new ArrayBuffer(1024);
const view = new Uint8Array(buffer);
worker.onmessage = (e) => console.log('worker saw', e.data); // 1024
worker.postMessage(buffer, [buffer]);
console.log(buffer.byteLength); // 0 - detached
console.log(view.length); // 0 - the view detached toogo deeper
Know that postMessage copies by default and that a second array argument moves the buffer instead. Be able to say the sender's buffer is unusable after a transfer.
Explain that transfer is O(1) while cloning is proportional to size, and describe detachment precisely: byteLength 0, views length 0, reuse throws. Mention the { transfer: [...] } options form.
Show the ownership discipline you would enforce in real code — ping-pong the same buffer between page and worker, never keep stale views, and measure whether the copy was actually the bottleneck before restructuring.
Own the messaging contract for a worker-based subsystem: decide which payloads are moved versus cloned, how buffer ownership is documented so no team writes through a detached view, and when the copy cost is small enough that simplicity wins.
## Two ways to hand data across a thread boundary Workers do not share a heap with the page. Every value you pass through `postMessage` has to be reconstructed on the other side, and the browser gives you exactly two mechanisms for that: **serialize (copy)** or **transfer (move)**. The default is copy. `worker.postMessage(buffer)` runs the structured clone algorithm over the message, producing an independent `ArrayBuffer` in the worker. Both agents now own a distinct block of memory with identical contents. That is safe and simple, and for small messages it is invisible. The second argument is the **transfer list**. Objects named there are not serialized at all — the browser detaches them from the sending agent and re-attaches the very same backing memory to the receiving one: ```js const buffer = new ArrayBuffer(64 * 1024 * 1024); worker.postMessage(buffer, [buffer]); ``` The cost of that call does not depend on 64 MB versus 64 bytes. No bytes move; the ownership record moves. ## The transfer list is a second argument, not a promotion of the message A very common misreading is that the transfer list *replaces* the message. It does not. The first argument is still the message that gets structured-cloned; the transfer list only says "while cloning, do not copy these — move them". That is why the buffer usually appears in both places. It also means you can transfer a buffer that sits deep inside an ordinary object: ```js worker.postMessage({ id: 7, pixels: buf }, [buf]); ``` Here `{ id: 7, pixels: … }` is cloned normally, and `buf` inside it is moved. There is an equivalent options form, `worker.postMessage(message, { transfer: [buf] })`, and `structuredClone(value, { transfer: [buf] })` uses the same idea for a same-thread clone. ## Detachment: what the sender is actually left with After a successful transfer the sender's `ArrayBuffer` object still exists as a JavaScript value, but it is **detached** — a live handle to nothing: ```js const buf = new ArrayBuffer(1024); const view = new Uint8Array(buf); worker.postMessage(buf, [buf]); buf.byteLength; // 0 view.length; // 0 view[0]; // undefined new Uint8Array(buf); // TypeError ``` Note that views created **before** the transfer are detached too — they borrow the same backing store. This is the source of the classic bug: code transfers a buffer once, then keeps writing through an old `Uint8Array` on the next frame, and the writes vanish without an error because element writes on a detached view are silently ignored. Transferring the same buffer twice throws a `DataCloneError`, because by the second call it is already detached and there is nothing left to move. ## Choosing between them Copy when the data is small, when you need to keep using it, or when the message is an ordinary object graph. Transfer when the payload is a large binary block and you are genuinely done with it on this side: decoded audio, image pixel data, a file read into an `ArrayBuffer`, a compressed chunk on its way to a worker for parsing. For a few kilobytes the difference is noise; for tens of megabytes on a 60 fps loop, a copy per message is exactly the main-thread stall you were offloading work to avoid. The practical pattern when both sides need the buffer repeatedly is **ping-pong ownership**: the page transfers the buffer in, the worker fills it and transfers the same buffer back in its reply. Only one side ever owns it, so there is no copying and no coordination problem — each side simply must not touch the buffer between sending and getting it back. If both sides truly need concurrent access to the same bytes, transfer is the wrong tool; that is what shared memory is for, and it carries very different requirements. ## Related things that are transferable `ArrayBuffer` is the one people meet first, but the list also includes `MessagePort`, `ImageBitmap`, `OffscreenCanvas`, and the stream types `ReadableStream`, `WritableStream` and `TransformStream`. What they have in common is that they wrap a resource whose ownership can be moved rather than duplicated. A `SharedArrayBuffer` is deliberately **not** transferable: sending it does not detach anything, because sharing, not moving, is its entire point. ## Interview framing The question behind the question is whether you know that worker communication has a cost model. "Workers are free because they are on another thread" is wrong: every message pays serialization on the sender and deserialization on the receiver, and the transfer list is the escape hatch for the one case — binary buffers — where the platform can skip that work entirely.
- If the message is an object with the buffer nested inside it, does the buffer still get transferred?Yes, as long as the buffer itself appears in the transfer list. The message object is structured-cloned normally, and any transferable named in the list is moved rather than copied wherever it appears in the graph. So `postMessage({ id, pixels: buf }, [buf])` clones the wrapper object and hands over `buf`'s memory.
- What happens if you transfer the same ArrayBuffer twice?The second call throws a `DataCloneError`. After the first transfer the buffer is detached, and a detached buffer cannot be serialized or transferred — there is no backing memory left to hand over. The usual fix is a ping-pong protocol where the worker transfers the buffer back before you reuse it.
- Does transferring make the worker's message handler run any sooner?No. Transfer removes the copy cost, not the queuing. The message is still delivered as a task on the receiving agent's event loop, so it runs after whatever that agent is currently doing. Transfer improves throughput and removes a main-thread stall; it does not change delivery ordering or latency guarantees.
Copying is photocopying a document and mailing the copy; transferring is putting the original in the envelope. Fast, but your desk is empty afterwards.
saying these in an interview costs you the question
- Thinking postMessage always shares memory with the worker
- Believing the transfer list replaces the message argument
- Expecting the sender's buffer to still be readable afterwards
- Assuming views created earlier survive the transfer
- Calling a copy free because workers are on another thread