When JavaScript sends data to a Worker or stores it in IndexedDB, it can use the structured clone algorithm instead of sharing an ordinary object reference. Some objects can be included in a transfer list and moved rather than copied. Both operations can look like “send a message,” but they differ in whether a separate copy is created and whether the sender can keep using the original. Confusing the two can lead to unexpected memory costs or a buffer that is no longer usable by the sender.

Conceptual copy and buffer ownership transfer

Conceptual illustration, not a memory-layout diagram or performance measurement.

1. What structured clone copies

The HTML Standard defines structured serialization and deserialization. structuredClone() uses the algorithm to copy supported values. APIs such as Worker postMessage() and IndexedDB also use structured serialization to pass or store data across execution contexts. The receiving side reconstructs values rather than sharing the same ordinary object reference for both sides to mutate.

“Copy” does not mean every JavaScript object is reproduced with all its behavior. Function objects cannot be cloned; attempting to clone one throws a DataCloneError. Custom prototypes and descriptors are not preserved; getters can run during serialization. Supported graph cycles and repeated references are preserved within the copy. Build messages from values the API supports, and represent important behavior as explicit data instead of relying on methods hidden in a class instance.

2. Transferable objects move instead of duplicate

Some objects can be transferred. For example, a large ArrayBuffer can be sent to a Worker with worker.postMessage({ buffer }, [buffer]). The receiver gets a new buffer wrapper; the sender’s backing data is detached, including access through its views. This does not mean every wrapper property throws. This is an ownership move, not merely another spelling for a fast copy.

Transfer also differs from shared memory, where two contexts can access the same memory and need a synchronization design. SharedArrayBuffer cannot be transferred; supported serialization retains shared data, with isolation and storage restrictions. Serializable, transferable, and shared are distinct capabilities. Support for one does not imply support for the others.

3. Choosing a transfer list and managing lifetime

The list controls transfer, not payload membership. In the buffer examples, the payload must reach the buffer for the receiver to access it. Transfer the typed array’s buffer, not the view; duplicate or unsupported list entries can fail. Transfer can also be used with the cloning API, for example structuredClone(value, { transfer: [buffer] }). After transfer, design the sender’s data flow so it does not read the former buffer. If several stages need the same data, decide whether to copy it, move ownership, or use a separate shared-memory mechanism before implementing the pipeline.

Do not try to transfer every small object or assume that cloning every message has the same cost. Structured clone is the standard way to copy supported values; transfer is available only for specific object types. Measure message size and copying cost before optimizing, and check each type and destination rather than assume every omitted value can be cloned. Transfer is not universally zero-copy.

4. Practitioner perspective: validate the boundary

Surma’s dated deep-copy explanation distinguishes shallow copies from structured cloning; its historical performance claims are not benchmarks here. Document message shape, unsupported values, transfer targets and remaining sender data. Validate the received message’s shape and values. Structured clone is a data-passing algorithm, not input validation or a security policy; successful cloning does not prove the message is trustworthy or matches the application’s expectations.

Test that the receiver gets the expected buffer, that the sender can no longer access a transferred buffer, and that functions or unsupported values fail as expected. If both contexts must work with shared memory, evaluate the shared-memory API and its synchronization requirements separately. For objects whose behavior must survive, convert them to explicit plain data and reconstruct the needed behavior in the receiving context.

Sources