frame-bridge
Examples

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.

The parent transfers 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

DevTools

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

parentopens post-message-channel, waits for child sys-ping
parentnew MessageChannel(); transfers port2 via post-message handshake
child iframereceives port2, opens message-channel transport
— from now on, traffic flows on the dedicated MessagePort —
parentbridge.send({ type: 'ping', value: 1 })
child iframeport.onmessage receives { type: 'ping', value: 1 }
parentawait resolves with { type: 'pong', value: 2 }

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.