MessageChannel demo
Parent page and child iframe — connected via a transferred MessagePort. After a one-shot handshake over postMessage, traffic flows directly between the two ports — the lowest-latency option, and the only one that supports zero-copy Transferable payloads.
port2 to the child over a one-shot postMessage handshake. On the child, post-message-channel must be listed in enabledTransports — that registers the window.message listener which receives the transferred port. On the parent it's optional: the bridge auto-bootstraps post-message-channel for the duration of the handshake and tears it down afterwards if it wasn't otherwise enabled. The demo lists both transports on both sides for symmetry. Once the handshake finishes, the dedicated port carries all subsequent traffic — watch the message-channel badge light up in the parent header.DevTools — parent bridge
No bridge registered
How it works
Parent setup
<BridgeProvider<DemoMessage>
open={true}
channelName="frame-bridge-demo-mc"
role="parent"
// post-message-channel is the one-shot courier that delivers port2.
enabledTransports={["message-channel", "post-message-channel"]}
targetOrigin={window.location.origin}
>
<ParentControls />
<IframeBridgeHost
src="/iframe/messagechannel-child"
targetOrigin={window.location.origin}
/>
</BridgeProvider>Child setup (iframe page)
// app/(embed)/iframe/messagechannel-child/page.tsx
<BridgeProvider<DemoMessage>
open={true}
channelName="frame-bridge-demo-mc"
role="child"
enabledTransports={["message-channel", "post-message-channel"]}
targetOrigin={window.location.origin}
>
<ChildInner targetOrigin={window.location.origin} />
</BridgeProvider>
// Inside ChildInner:
useEffect(() => {
bridge.setTarget(window.parent, targetOrigin);
}, [bridge, targetOrigin]);Message flow
Transferable payloads
message-channel is the only transport that supports the transfer option of bridge.send(). The underlying MessagePort.postMessage takes a list of Transferable objects (e.g. ArrayBuffer, ImageBitmap, OffscreenCanvas) and hands ownership to the receiver — no clone, no copy. BroadcastChannel can't do this; calling postMessage with a transfer list throws.
const buf = new Uint8Array(1024);
buf[0] = 42;
// The buffer is *transferred*, not copied — buf.buffer.byteLength becomes 0
// on the sender after this call resolves. Only message-channel can do this.
const ack = await bridge.send(
{ type: "buffer", bytes: buf.buffer, size: 1024 },
{ transfer: [buf.buffer] }
);
console.log(buf.buffer.byteLength); // 0 — detached
console.log(ack); // { type: "buffer-ack", size: 1024, firstByte: 42 }Try Send buffer (1024 B) in the demo above — the log entry confirms the parent's ArrayBuffer.byteLength drops to 0 after the call, proving the transfer was zero-copy.