Lesson 0003 · ~12 minutes

Transfer: move ownership, don’t copy bytes

One skill: decide clone vs transfer, write the transfer list correctly, and predict detachment.

Win for this lesson

You can say: “large binary the sender is done with → transfer .buffer; both sides still need the bytes → clone; BroadcastChannel / IDB have no transfer list.”

1. You already met this on workers

Workers lesson 3: worker.postMessage(u8, [u8.buffer]) moves the ArrayBuffer; main sees byteLength === 0. This track owns the same move on structuredClone() — same realm, no worker required — and the list rules that bite everywhere.

const u8 = new Uint8Array(1024);

// Copy bytes — both sides keep a buffer
const copied = structuredClone(u8);

// Move bytes — source detaches
const moved = structuredClone(u8, { transfer: [u8.buffer] });
// u8.byteLength === 0
// moved.byteLength === 1024

MDN: after transfer the original no longer owns the resource. The memory is not duplicated. That is the point.

2. The list is a marker, not a second payload

Two syntaxes, one transfer list:

Call List
structuredClone(value, options) { transfer: [buf] }
worker.postMessage(value, transfer) [buf] (second argument)

Three rules from MDN:

  1. Typed arrays are not transferable. Their .buffer is. { transfer: [u8] } throws DataCloneError.
  2. The resource must be in the value. The list only says how to send. Transfer a buffer that is not reachable from value and it still detaches — the clone never sees it.
  3. Detached is terminal. You cannot clone or transfer that buffer again. Transfer is irreversible (WHATWG).

3. When transfer wins — and when it does not

Transfer

Large binary; sender is done. Image pipelines, file parse buffers, “freeze the original so nothing else can write it” (MDN’s same-realm example).

Clone

Both sides still need the bytes, or the value is ordinary data (Dates, Maps, DTOs). Default for small payloads.

No list

BroadcastChannel, IndexedDB, history.pushState — structured clone only. You cannot transfer into storage or a BC post.

Default rule (steal this)

If the hard part is “these bytes should exist in only one place after this call”, transfer. If both sides must keep using them, clone. If the API has no transfer list, design for a copy — or don’t put megabytes on that bus.

Still not a document, still not a class

Transfer moves a resource inside a structured clone. It does not make HTTP bytes, and it does not revive Invoice.total(). Same two facts as lessons 1–2.

4. Lab — watch byteLength

Each click builds a fresh 16-byte view. Run all five.

5. Practice — predict the move

Equal-length options. Feedback is immediate.

Snippet A

After structuredClone(u8, { transfer: [u8.buffer] }), u8.byteLength is:

Snippet B

structuredClone(u8, { transfer: [u8] }) — the view itself is in the list.

Snippet C

structuredClone({ n: 1 }, { transfer: [orphan] })orphan is an ArrayBuffer not on the object.

Snippet D

Fan-out a 16MB pixel buffer to other same-origin tabs via BroadcastChannel.

Snippet E

You already transferred u8.buffer. Now structuredClone(u8).

6. What to remember

Ask your teacher If a “buffer is detached” error just showed up in a worker pipeline — or you are unsure about ImageBitmap / OffscreenCanvas — ask.

Primary source (read next)

MDN — Transferable objects. Thread transfer, structuredClone({ transfer }), and “the list does not send the resource.” Then the structuredClone transfer examples.