Messaging & structured cloning
Send data safely between windows, workers, iframes, and tabs with structuredClone, MessageChannel, BroadcastChannel, and transfer lists.
- 01Copy more kinds of dataUse structuredClone to copy dates, maps, sets, buffers, errors, and objects that point to themselves.
- 02Pick a way to send messagesUse MessageChannel for a private line between two places, and BroadcastChannel to tell every listener on your origin.
- 03Hand over big dataExplain how a transfer list hands an ArrayBuffer over, why the sender's copy becomes empty, and why that can be faster than copying.
Sending data to another place
A web page often has to send data to another place. For example:
- A page sends data to an
iframe: a page shown inside your page. - A page sends work to a worker: a script that runs in the background.
- A settings tab tells your other open tabs that the theme changed.
These places can't share one object. So the browser sends them a copy of the data. For some data, it can hand the data over instead.
The browser copies the data with a set of rules called the structured clone algorithm. "Clone" means copy. The same rules are used by:
structuredClone(), which copies a value inside one script.postMessage(), which sends a message to a window, an iframe, or a worker.MessagePortandBroadcastChannel, two more ways to send messages.- IndexedDB, the browser's database, when it saves a value.
These rules copy many kinds of JavaScript values. They can even copy an object that points to itself. For some binary data, they can also transfer it: hand it over instead of copying it.
When you send a message to another window, say who may receive it. The second argument of postMessage is the target origin. An origin is the start of a web address: the scheme, host, and port, like https://widget.example. The browser delivers the message only if the receiving window has that origin. Never use "*", which means "any origin", for a secret message.
When you receive a message, check two things before you trust event.data:
event.origin: the origin of the page that sent it.event.source: the window that sent it.
The code below is a small model of those two checks.
const trustedOrigin = "https://widget.example";const trustedWindow = "widget-window";const event = { origin: trustedOrigin, source: trustedWindow, data: { type: "ready" } }; function receive(event) { if (event.origin !== trustedOrigin || event.source !== trustedWindow) return "ignored"; return event.data.type;} console.log(receive(event));console.log(receive({ ...event, origin: "https://evil.example" }));Line 6 returns "ignored" if the origin or the window is wrong. Line 10 passes the trusted message, so it prints ready. Line 11 sends the same message from https://evil.example, so it prints ignored.
The model uses plain strings in place of real windows, so it gives the same result every time. In a real page, use the iframe or popup window you created in place of trustedWindow. When you send, pass the same trusted origin as the second argument of postMessage.
You need to send a form to another office. If you mail a photocopy, you keep the original. If you mail the original, the other office has it and you don't. Messages in JavaScript make the same choice. Most data is copied. Some special objects, such as an ArrayBuffer (a block of raw bytes), can be transferred instead.
- In real life: Mailing a photocopy
- In JavaScript: Copying: both sides have their own data
- In real life: Mailing the original
- In JavaScript: Transferring: the receiver gets the data, and the sender can't use it any more
- In real life: A thick binder takes a long time to copy
- In JavaScript: A big
ArrayBuffertakes a long time to copy - In real life: The courier refuses some things
- In JavaScript: Some values can't be copied, and you get a
DataCloneError
The Windows, iframes & postMessage lesson showed how to send a message. This lesson is about the data inside the message: what can be copied, what can't, and what can be handed over.
structuredClone copies more kinds of data than JSON
INTERACTIVEstructuredClone(value) makes a deep copy of a value. A deep copy copies the object and everything inside it, so the copy shares nothing with the original. It uses the same rules as messages.
You may know JSON.stringify and JSON.parse from the JSON lesson. JSON turns data into text and back, and many types get lost on the way. structuredClone keeps them. It can copy:
Date,Map,Set, andRegExpobjects.ArrayBufferobjects and typed arrays such asUint8Array.BlobandFileobjects.- Common
Errorobjects. - Objects that point to themselves. These are called circular references.
It copies data, not everything. It does not keep:
- The methods of your own classes. The copy doesn't get the class's prototype, the object where those methods live.
- Getters. A getter is a function that runs when you read a property.
- A property's settings, such as whether it can be changed.
It reads the object's own properties, saves their current values, and builds new objects from them.
Picture movers who list every item in your flat, pack it, and set up matching furniture in a new flat. The new flat looks the same, but it is separate. If you move a chair in the new flat, the old flat doesn't change. In the same way, changing a copied array doesn't change the original array. And an object made from your class arrives as plain data, without its methods.
- In real life: Pack furniture, dishes, and boxes
- In JavaScript: Copy dates, maps, sets, arrays, buffers, and nested objects
- In real life: Set up the furniture in the new flat
- In JavaScript: Build new objects, not the same objects as before
- In real life: Refuse things that can't be packed
- In JavaScript: Throw an error for functions, DOM nodes, symbols,
WeakMap, andWeakSet - In real life: Leave the brand label behind
- In JavaScript: Your class's prototype and methods don't come along
const cart = { items: ["tea"] };const copy = structuredClone(cart);copy.items.push("bread");console.log(cart.items.join(","));console.log(copy.items.join(","));Line 1 makes a cart with a list of items. Line 2 copies the cart. The copy gets its own new list. Line 3 adds bread to the copy's list only. So line 4 prints tea, and line 5 prints tea,bread.
structuredClone builds a brand-new cart with its own list inside. Line 3 adds bread to the copy only, so the first cart still holds just tea.const prices = new Map([["tea", 3]]);const tags = new Set(["hot"]);const copy = structuredClone({ prices, tags });console.log(copy.prices.get("tea"));console.log(copy.tags.has("hot"));Line 3 copies an object that holds a Map and a Set. The copies are still a real Map and a real Set. So line 4 prints 3, and line 5 prints true. JSON would turn both of them into empty objects, {}.
const original = makeValue(choice);const copy = structuredClone(original);inspect(copy);Choose a value and press Inspect clone.
Pick a value, read the code, then press Inspect clone. Your browser's own structuredClone makes the result.
Try the "Class instance" choice. A class instance is an object made with new from your own class. Its fields are copied, but its method is lost, because the prototype isn't copied.
Then try "Object with a getter". The getter runs once, during the copy. The copy stores the number it returned as a normal value. The getter itself is gone.
What can't be copied, and what changes
SORTA simple rule: data can be copied, but behavior and live browser objects usually can't. Here is why each of these fails:
- A function is behavior. It also remembers the variables around it.
- A DOM node, such as a button, belongs to one live page.
- A symbol is a unique value. A copy would be a different value.
- A
WeakMaporWeakSetcan lose entries at any time, when the browser cleans up objects nobody uses. So its contents can't be listed and copied.
For these values, structuredClone throws a DataCloneError. It doesn't pretend it copied them.
- Date
- Map with object keys
- Set
- Object that points to itself
- RegExp with lastIndex = 5
- Object with a getter
- Object made from your own class
- Hidden (non-enumerable) property
- Function
- DOM node
- Symbol value
- WeakMap
Sort each value by what structuredClone does with it. Watch for values that copy but come out a little different.
const order = { onDone: () => "saved" };structuredClone(order);The function on line 1 is behavior, not data. So line 2 throws a DataCloneError. The fix is to send plain data that names the action, such as { type: "save" }. The receiver then runs its own function for that action.
structuredClone refuses it. Plain data, such as type: "save", copies without trouble.For plain objects and objects made from your own classes, the copy takes only the object's own enumerable properties. Enumerable means the property shows up in Object.keys(). So:
- Properties that are not enumerable are left out.
- A getter becomes a plain value.
- Property settings, such as writable and configurable, are not kept.
The Enumerability & ownership and Property flags & descriptors lessons explain these words in more detail.
One more change to know about. A RegExp (a regular expression) is copied, but its lastIndex is set back to 0. In a global search, lastIndex is where the next search starts. So the copy starts searching from the beginning again.
MessageChannel: a private line
INTERACTIVEnew MessageChannel() makes two linked ports, port1 and port2. A port is one end of the line. Both ports are MessagePort objects. A message posted on one port arrives at the other.
You keep one port and transfer the other to a worker or an iframe. Now the two sides have a private line. They don't need the shared message listener on window any more.
A normal window message is like calling out across a busy hall. Anyone who is listening may hear it. A MessageChannel is like handing a friend one of a pair of walkie-talkies. Only the two of you can talk on it.
- In real life: Two walkie-talkies set to talk to each other
- In JavaScript:
port1andport2from oneMessageChannel - In real life: You give one to a friend
- In JavaScript: You transfer a port to an iframe or a worker
- In real life: You speak into one, and your friend hears it
- In JavaScript:
postMessageon one port fires amessageevent on the other - In real life: You switch the walkie-talkie off
- In JavaScript:
port.close()closes the line
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.
new MessageChannel() makes two ports. This lab transfers port2 to an iframe, then uses port1 to talk to it in private. The parent listens with addEventListener and start(). The iframe uses onmessage.
The lab listens for messages in two ways. You'll see both in real code:
- The parent page uses
addEventListener("message", ...). With this way, you must callport.start()before messages arrive. - The iframe sets
port.onmessage = .... Settingonmessagestarts the port for you.
Calling start() when you don't need to does no harm, and it makes the code easier to read.
BroadcastChannel: a message for everyone
INTERACTIVEBroadcastChannel is for announcements to everyone, not private talks. You make one with a name, like new BroadcastChannel("updates"). Every channel with the same name on the same origin hears messages from the others. That includes channels in other tabs, windows, iframes, and workers.
The channel object that sends a message does not get its own message.
Picture the intercom in a block of flats. One announcement reaches everyone listening on that channel in the building. In the same way, BroadcastChannel works well for messages like "settings changed", "log out everywhere", or "cache updated".
- In real life: Everyone tuned to channel 3 hears the announcement
- In JavaScript: Every
BroadcastChannelwith the same name and origin gets the message - In real life: The speaker already knows what they said
- In JavaScript: The channel that sends doesn't get its own message
- In real life: Only people in this building hear it
- In JavaScript: A site with a different origin can't hear your channel
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 with the same name and origin hears messages from the others. The channel that sends doesn't get its own message. Open this lesson in a second tab to see another tab hear it too.
MessageChannel joins exactly two ends. A BroadcastChannel message goes to every other channel with the same name, but not back to the sender.Open this lesson in a second tab and press Create channels there too. The second tab is on the same origin, so its channels hear the messages from this tab as well.
Transferring or copying
INTERACTIVECopying makes separate data on both sides. Transferring hands an object over: after a transfer, only the receiver can use it.
Only some objects can be transferred. They are called transferable objects. The clearest example is an ArrayBuffer, a block of raw bytes. After a transfer, the sender's buffer is detached: it is empty and can't be used. Its byteLength, the size in bytes, becomes 0. The receiver now has 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);- Press a button to copy or transfer a buffer.
A copy leaves the original usable. A transfer hands the bytes over: the sender's ArrayBuffer is detached, so its byteLength becomes 0. Times change from one browser and computer to another, so compare the sizes first.
moved, so buffer is left with 0.A transfer list is an array of the objects to hand over instead of copy. You'll use it in two places:
structuredClone(value, { transfer: [buffer] })port.postMessage(value, [buffer])
Use a transfer list for big images, sound clips, or other binary data that the sender doesn't need any more.
The table compares the three tools in this lesson. Each row asks one question. Each column gives the answer for one tool.
| What you want to know | structuredClone | MessageChannel | BroadcastChannel |
|---|---|---|---|
| What is it best for? | Making a deep copy of data inside one script. | Sending messages between exactly two places. | Telling every listener on the same origin. |
| How are the two sides joined? | They aren't. It just returns a copy. | By two linked MessagePort objects. | By the same channel name on the same origin. |
| Does the sender get its own message? | It doesn't send messages. | Only if the other port sends one back. | No. The channel that posts doesn't get its own message. |
| Can it take a transfer list? | Yes: structuredClone(value, { transfer }). | Yes: port.postMessage(value, transfer). | No. Its postMessage only copies. |
When the message arrives
STEP THROUGHpostMessage returns right away. The receiver's message event runs later, after the current code finishes. Posting a message puts its delivery in a queue. It doesn't stop your code and jump to the receiver. The Event loop lesson explains this queue.
Step through the order of postMessage. Pick copy or transfer, then predict which log comes first and how many bytes 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 line 8. In copy mode, the sender still has its buffer, so line 9 logs 4. In transfer mode, line 8 takes the buffer away at once, so line 9 logs 0. In both modes, the receiver's event runs last and sees all 4 bytes.
Where you will use this
PRACTICALYou'll use these tools when a web app splits its work across several places. For example:
- A canvas sends its pixels to a worker to be changed.
- An iframe gets a private port to reply on.
- A settings page tells your other tabs to refresh.
- A worker sends back data it has read, so the page doesn't freeze while it waits.
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);};Line 1 starts a worker. Lines 4–7 send it a job:
- Line 5 is the message. Small details such as
widthandheightare copied. - Line 6 is the transfer list. The big
pixelsbuffer is handed over, not copied.
After sending, the page must not read pixels, because the worker now owns it. When the worker is done, it can transfer a new buffer back. Line 9 handles that reply.
- Need a deep copy inside one script? Use
structuredClone. - Need a private line with exactly two ends? Use
MessageChannel. - Need to tell every tab or worker on your origin? Use
BroadcastChannel. - Need to send big binary data that you won't use again? Add a transfer list.
Common misconceptions
"structuredClone is just JSON.parse(JSON.stringify(...)), but nicer."
Not true. JSON turns data into text. On the way it loses dates, maps, sets, and properties set to undefined, and it throws on objects that point to themselves. structuredClone copies all of these, and it never makes text.
"A deep copy keeps my class's methods."
Not true. Built-in types such as Date and Map stay the same type. Your own class does not. The copy gets the object's own enumerable fields only. It doesn't get the methods or the private fields (the ones that start with #).
"postMessage runs the receiver straight away."
Not true. postMessage returns before the receiver's message event runs. That's why the order experiment logs the line after postMessage first.
"BroadcastChannel reaches every website."
Not true. It only reaches the same origin. A channel named "updates" on your site is separate from a channel with the same name on another site.
"Transfer is just a faster copy."
Not true. A transfer hands the data over. The sender's ArrayBuffer becomes detached, with a size of 0. Code that still tries to read it will break.
"If an object can hold it, it can be copied."
Not true. Objects can hold functions, DOM nodes, symbols, and WeakMaps. structuredClone refuses all of these. They are behavior, live parts of a page, unique values, or collections that can lose entries.
Practice: copy it, send it, hand it over
5 EXERCISESPredict both lines. Then run the code in your browser console or in 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 line prints a, because the copy has its own array. The second line prints true, because structuredClone can copy a Date.
What sizes print after you transfer an ArrayBuffer? Its byteLength is its size in bytes.
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 buffer prints 16. A transfer doesn't copy the bytes; it hands them over.
A teammate writes worker.postMessage({ done: () => alert("ok") }). What error name will they get?
// Don't send a function. Send data that names the action:
port.postMessage({ type: "save", payload: formData });
// The receiver runs its own function when type is "save".A function value throws DataCloneError. Send data that names the action. Then the receiver calls its own function.
Predict what happens to a getter when you copy an object. Object.getOwnPropertyDescriptor(copy, "answer") returns the property's settings, including its get function if it has one.
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 copy stores a plain value, so the property's get is undefined.
You have an iframe. After a first hello, you want a private line just for your page and the iframe. Which tool makes 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. Transfer one port to the iframe, and the two pages have a private line.
Quiz: check your understanding
8 QUESTIONSEach choice explains why it is right or wrong. Read the wrong ones too: they show the most common mistakes.
Question 1 of 8Which sentence best describes structuredClone?
Choose an answer to see the explanation.
Question 2 of 8What does this Date copy 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 a RegExp's 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 this getter copy 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 tool is the pair of walkie-talkies?
Choose an answer to see the explanation.
Question 6 of 8What does a transfer do to the sender's ArrayBuffer?
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 8Channels A, B, and C share one name. Who gets a message that A posts?
Choose an answer to see the explanation.
Question 8 of 8What does this transfer code print?
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
structuredClonemakes a deep copy. It keeps many types that JSON loses:Date,Map,Set, typed arrays,Errorobjects, and objects that point to themselves.- It refuses behavior and live objects: functions, DOM nodes, symbols,
WeakMap, andWeakSet. - An object made from your own class arrives as plain data. Its methods, getters, and property settings are not kept.
MessageChannelgives you two private ports.BroadcastChannelsends to every other channel with the same name and origin.- A transfer list hands data over. After a transfer, the sender's
ArrayBufferhas abyteLengthof0.
Remember the one-liner.
Structured cloning is how the browser copies data for structuredClone and for messages; a transfer list hands data over instead of copying it.
Up next: Web Components. You'll build your own HTML elements with custom elements, Shadow DOM, templates, and slots.