Messaging & structured cloning
Send data safely between windows, workers, iframes, and tabs with structuredClone, MessageChannel, BroadcastChannel, and transfer lists.
- 01Clone richer dataUse structuredClone for Dates, Maps, Sets, buffers, errors, and circular references.
- 02Choose a message pathPick MessageChannel for a private line and BroadcastChannel for same-origin announcements.
- 03Move big bytesExplain when transfer lists detach ArrayBuffers and why that can be faster than copying.
Data crossing a boundary
Modern JavaScript is rarely alone. A page talks to an iframe. A window sends work to a worker. A settings tab tells every other open tab that the theme changed. Those messages need a safe way to carry data across a boundary without sharing the same object by reference.
The browser solves that with the structured clone algorithm. It is the set of rules behind structuredClone(), postMessage(), MessagePort, BroadcastChannel, IndexedDB values, and worker messages. It copies many real JavaScript data types, preserves cycles, and can also transfer ownership of some binary objects instead of copying them.
If you send a photocopy of a form, you keep your original. If you mail the original form, the other office has it and you do not. JavaScript messages make the same choice: copy most data, or transfer ownership of special objects such as ArrayBuffer.
- In real life: Mailing a photocopy
- In JavaScript: Copying with structured clone: both sides have their own data
- In real life: Mailing the original
- In JavaScript: Transferring: the receiver owns it and the sender no longer can use it
- In real life: A big binder takes time to copy
- In JavaScript: Large ArrayBuffers can be expensive to clone
- In real life: The courier must know what can be shipped
- In JavaScript: Uncloneable values throw
DataCloneError
Where the analogy stops: Real documents can be scanned or returned later. A transferred ArrayBuffer is detached immediately; the sender cannot read those bytes unless someone sends another buffer back.
This lesson sits between Windows, iframes & postMessage and Web Components in the curriculum. Those neighboring lessons are text-only in this batch, so this one focuses on the shared data-moving rules you will use in both windows and workers.
structuredClone copies richer data than JSON
INTERACTIVEstructuredClone(value) deep-copies a value using browser messaging rules. Compared with JSON, it keeps many JavaScript-specific types: Date, Map, Set, RegExp, ArrayBuffer, typed arrays, Blob, File, common Error objects, and circular references.
It is still a data copier, not a magic duplicator of everything. It does not preserve custom prototypes, property descriptors, accessor functions, or methods from your own class instances. It reads own enumerable properties, stores their current values, and rebuilds supported built-ins on the other side.
Imagine movers who carefully list every item in your apartment, pack it, and rebuild matching furniture in a new apartment. The copy is useful and separate, but it is not the exact original. That is why changing a cloned array does not change the source array, and why a class instance arrives as plain data.
- In real life: Pack furniture, dishes, and labeled boxes
- In JavaScript: Copy Dates, Maps, Sets, arrays, buffers, and nested objects
- In real life: Rebuild furniture in the new house
- In JavaScript: Create fresh objects that are not the same references
- In real life: Refuse things that cannot be packed
- In JavaScript: Throw for functions, DOM nodes, symbols, WeakMap, and WeakSet
- In real life: Leave the designer brand tag behind
- In JavaScript: Your class prototype and methods are not preserved
Where the analogy stops: A moving company might move a lamp and the electricity still works. structuredClone does not move behavior: methods, closures, and DOM connections are outside the data box.
const original = makeValue(choice);const copy = structuredClone(original);inspect(copy);Choose a value and press Inspect clone.
Pick a value, read the code, then inspect. The result comes from your browser's real structuredClone implementation.
Try the class instance and getter options. The class example keeps its own fields but loses its method because the prototype was not shipped. The getter example runs the getter once and stores the returned number as a regular value. That is very different from copying a live accessor.
What cannot be cloned, and what changes
SORTThe rule of thumb is: data can travel; behavior and live host objects usually cannot. A function closes over a scope. A DOM node is attached to a document. A symbol represents identity. A WeakMap depends on garbage-collection reachability. The clone algorithm refuses these with a DataCloneError instead of pretending it copied them.
- Date
- Map with object keys
- Set
- Object that points to itself
- RegExp with lastIndex = 5
- Object property with a getter
- Instance of your own class
- Non-enumerable property
- Function
- DOM node
- Symbol value
- WeakMap
Sort each value into the result you expect from structuredClone. Pay attention to the changed-but-not-throwing cases.
For plain objects and your own instances, structured cloning walks own enumerable properties. Non-enumerable properties disappear, accessors become data values, and property attributes such as writable/configurable are not preserved. The Enumerability & ownership and Property flags & descriptors lessons go deeper on those words.
One small built-in surprise: RegExp objects clone, but lastIndex is reset to 0. If you are mid-way through a global regular-expression search, do not rely on the clone continuing from the same spot.
MessageChannel and MessagePort
INTERACTIVEMessageChannel creates two linked MessagePort objects: port1 and port2. Posting a message on one port delivers it to the other. You can keep one port and transfer the other to a worker or iframe, creating a private line that does not rely on global window message listeners.
A normal window message is like calling across a lobby: everyone nearby might have a listener. A MessageChannel is more like handing someone the other half of a private walkie-talkie pair.
- In real life: Two walkie-talkies tuned to each other
- In JavaScript:
port1andport2from oneMessageChannel - In real life: Hand one radio to another person
- In JavaScript: Transfer a port to an iframe or worker
- In real life: Speak into one; the other hears
- In JavaScript:
postMessageon one port firesmessageon the other - In real life: Turn the radio off
- In JavaScript:
port.close()stops the line
Where the analogy stops: Real walkie-talkies can be overheard. A MessageChannel port is only usable by code that has been given that port, though the messages still use structured cloning rules.
const channel = new MessageChannel();channel.port1.onmessage = (event) => log("port1", event.data);channel.port1.start();iframe.contentWindow.postMessage("connect", "*", [channel.port2]);channel.port1.postMessage("hello iframe");- Press Connect iframe to transfer port2.
MessageChannel creates two ports. This lab transfers port2 to a sandboxed iframe, then uses port1 to talk privately. port1 uses addEventListener plus start; the iframe uses onmessage.
The lab uses both styles you will see in real code. The parent uses addEventListener("message", ...), so it calls port.start(). The iframe uses port.onmessage = ..., which starts delivery automatically in browsers. Calling start() anyway is harmless and often makes the intent clear.
BroadcastChannel
INTERACTIVEBroadcastChannel is for announcements, not private conversations. Every browsing context on the same origin that opens a channel with the same name can hear messages from the others: tabs, windows, iframes, and workers. The object that sends a message does not receive its own message.
Picture an apartment building intercom. One announcement goes to everyone listening to that channel in the building. BroadcastChannel is great for “settings changed”, “log out everywhere”, or “cache updated” messages.
- In real life: Everyone tuned to channel 3 hears the announcement
- In JavaScript: Every same-origin
BroadcastChannelwith the same name receives it - In real life: The person speaking already knows what they said
- In JavaScript: The sending channel object does not receive its own message
- In real life: Only this building hears it
- In JavaScript: Different origins do not share channels
Where the analogy stops: A building intercom might reach every apartment no matter who owns it. BroadcastChannel is stricter: same channel name is not enough; same origin is required.
const a = new BroadcastChannel("lesson-room");const b = new BroadcastChannel("lesson-room");const c = new BroadcastChannel("lesson-room");b.onmessage = (event) => log("B heard " + event.data);c.onmessage = (event) => log("C heard " + event.data);a.postMessage("standup at 10");- Press Create channels, then broadcast from A, B, or C.
Every BroadcastChannel object with the same name and origin hears messages from the others. The sending object does not receive its own message. Open this lesson in a second tab to see same-origin cross-tab delivery too.
Try opening this lesson in another tab after creating the channels. BroadcastChannel is one of the few experiments here that becomes more fun with two tabs, because the second tab is another same-origin listener.
Transferring vs copying
INTERACTIVECopying makes independent data on both sides. Transferring moves ownership of a transferable object. The clearest example is ArrayBuffer. After transfer, the sender’s buffer is detached; its byteLength becomes 0, and the receiver owns the bytes.
const buffer = new ArrayBuffer(8);const copy = structuredClone(buffer);console.log(buffer.byteLength, copy.byteLength); const moved = structuredClone(buffer, { transfer: [buffer] });console.log(buffer.byteLength, moved.byteLength);- Run a copy, a structuredClone transfer, or a postMessage transfer.
Copying leaves the original usable. Transferring moves ownership: the sender's ArrayBuffer is detached, so byteLength becomes 0. Timings vary by browser and machine, so compare the byte lengths first.
Transfer lists appear in two places you will use often: structuredClone(value, { transfer: [buffer] }) and port.postMessage(value, [buffer]). They are perfect for large images, audio chunks, or binary protocol frames when the sender is done with the data.
| Question | structuredClone | MessageChannel | BroadcastChannel |
|---|---|---|---|
| Best for | Deep-copying JavaScript data inside one realm | Sending one message to one other context | Announcing to all same-origin listeners |
| Connection | No connection; returns a copy | Two linked MessagePort objects | Same string channel name and same origin |
| Sender receives its own message? | Not a messaging API | Only if the other port sends back | No, the BroadcastChannel object that posts does not receive its own message |
| Transfer list | Yes, with structuredClone(value, { transfer }) | Yes, with port.postMessage(value, transfer) | No practical transfer list in the common browser API |
Message delivery order
STEP THROUGHpostMessage returns immediately. The receiving message event runs later, after the current task finishes. That timing connects this lesson to the Event loop lesson: posting a message schedules work; it does not pause the current line of code and jump to the handler.
Step through postMessage order. Switch between copying and transferring, then predict which log appears first and what byteLength the sender keeps.
script
const buffer = new ArrayBuffer(4); port1.onmessage = (event) => { console.log("message event:", event.data.byteLength);}; port2.postMessage(buffer);console.log("after postMessage:", buffer.byteLength);The setting changes one visible line. In copy mode, line 9 sees the sender’s buffer still attached. In transfer mode, line 8 detaches the sender immediately, so line 9 logs 0. In both modes, the receiver’s event appears later and sees the four bytes.
Where you will use this
PRACTICALThese APIs show up whenever a web app splits work across places: a canvas sends pixels to a worker, an iframe receives a private reply port, a settings page tells other tabs to refresh, or a worker returns parsed data without blocking the main interface.
const worker = new Worker("image-worker.js", { type: "module" });const pixels = canvasBuffer; worker.postMessage( { job: "blur", width, height, pixels }, [pixels],); worker.onmessage = (event) => { drawPixels(event.data.pixels);};The important design choice is visible on lines 3–6. The message object carries ordinary metadata by copy, while the big pixel buffer is in the transfer list. The main page should not read that buffer after sending it; ownership moved to the worker. When the worker finishes, it can transfer a new buffer back.
- Need a deep copy inside one script? Use
structuredClone. - Need one private two-ended line? Use
MessageChannel. - Need same-origin announcements across tabs or workers? Use
BroadcastChannel. - Need to send huge binary data and give it up? Add a transfer list.
Common misconceptions
“structuredClone is JSON.parse(JSON.stringify(...)), but nicer.”
No. JSON makes text and loses Dates, Maps, Sets, undefined properties, circular references, and more. structuredClone copies many richer values without creating text.
“A deep clone preserves class behavior.”
Built-in brands such as Date and Map survive. Your own class prototype does not. The clone gets own enumerable fields, not methods or private fields.
“postMessage runs the receiver immediately.”
It returns before the receiver’s message event runs. That is why the order experiment logs the line after postMessage first.
“BroadcastChannel is global to every website.”
It is same-origin. A channel named "updates" on your site is separate from a channel with the same name on another origin.
“Transfer is just a faster copy.”
Transfer changes ownership. The sender’s ArrayBuffer is detached, so code that expects to keep reading it will break.
“If it can be stored in an object, it can be cloned.”
Objects can hold functions, DOM nodes, symbols, and WeakMaps. structuredClone refuses those because they are behavior, live host objects, unique identities, or weak collections.
Practice: clone it, send it, move it
5 EXERCISESPredict both lines, then run the snippet in your console or with Node 22.
const original = { items: ["a"], when: new Date("2024-01-01T00:00:00.000Z") };
const copy = structuredClone(original);
copy.items.push("b");
console.log(original.items.join(""));
console.log(copy.when instanceof Date);The first log is a because the copied array is separate. The second log is true because Date is a supported structured-clone type.
What byte lengths print after transferring an ArrayBuffer?
const buffer = new ArrayBuffer(16);
const moved = structuredClone(buffer, { transfer: [buffer] });
console.log(buffer.byteLength);
console.log(moved.byteLength);The sender prints 0; the moved clone prints 16. Transfer is not a copy, it is ownership movement.
A teammate tries worker.postMessage({ done: () => alert("ok") }). What error name should they expect?
// Instead of sending a function:
port.postMessage({ type: "save", payload: formData });
// The receiver decides which local function handles type: "save".A function value throws DataCloneError. Send data that describes the action, then let the receiver call its own local function.
Predict what happens to a getter when an object is cloned.
const source = { get answer() { return 42; } };
const copy = structuredClone(source);
console.log(copy.answer);
console.log(typeof Object.getOwnPropertyDescriptor(copy, "answer").get);The getter returns 42, so copy.answer is 42. The clone stores a data property, so the descriptor's get is undefined.
You have an iframe and want a dedicated private connection after an initial handshake. Which API creates that line?
const channel = new MessageChannel();
iframe.contentWindow.postMessage("connect", "*", [channel.port2]);
channel.port1.postMessage({ type: "hello" });Use MessageChannel. It gives you two MessagePort objects, and transferring one port sets up a private conversation.
Quiz: check your understanding
8 QUESTIONSEach answer explains the rule it tests. Read the explanations for wrong answers too; they are the common bugs.
Question 1 of 8Which statement best describes structuredClone?
Choose an answer to see the explanation.
Question 2 of 8What does the Date clone check print?
Read the code, then predictconst copy = structuredClone(new Date("2024-01-01T00:00:00.000Z")); console.log(copy instanceof Date); console.log(copy.toISOString());Choose an answer to see the explanation.
Question 3 of 8What happens to RegExp lastIndex?
Read the code, then predictconst re = /go/g; re.lastIndex = 2; const copy = structuredClone(re); console.log(copy.source); console.log(copy.lastIndex);Choose an answer to see the explanation.
Question 4 of 8What does the getter clone snippet print?
Read the code, then predictconst original = { get score() { return 9; } }; const copy = structuredClone(original); console.log(copy.score); console.log(typeof Object.getOwnPropertyDescriptor(copy, "score").get);Choose an answer to see the explanation.
Question 5 of 8Which API is the private walkie-talkie pair?
Choose an answer to see the explanation.
Question 6 of 8What does transfer do to an ArrayBuffer sender?
Read the code, then predictconst buffer = new ArrayBuffer(8); const moved = structuredClone(buffer, { transfer: [buffer] }); console.log(buffer.byteLength); console.log(moved.byteLength);Choose an answer to see the explanation.
Question 7 of 8Who receives a BroadcastChannel message from channel A?
Choose an answer to see the explanation.
Question 8 of 8What does the message order snippet print first in transfer mode?
Read the code, then predictconst buffer = new ArrayBuffer(4); structuredClone(buffer, { transfer: [buffer] }); console.log("after transfer:", buffer.byteLength);Choose an answer to see the explanation.
Key takeaways
structuredClonedeep-copies many data values that JSON cannot, including Date, Map, Set, typed arrays, Error objects, and cycles.- It refuses behavior and live objects such as functions, DOM nodes, symbols, WeakMap, and WeakSet.
- Your own class instances arrive as plain enumerable data; prototypes, methods, accessors, and descriptors are not preserved.
MessageChannelgives two private ports;BroadcastChannelannounces to same-origin listeners with the same name.- A transfer list moves ownership. For
ArrayBuffer, the sender’sbyteLengthbecomes0.
Remember the one-liner.
Structured cloning is the browser’s safe data-packing system for copies and messages; transfer lists move ownership instead of making another copy.
Up next: Web Components, where custom elements, Shadow DOM, templates, and slots help you build reusable pieces of interface.