Channel Messaging

new MessageChannel() creates two entangled ports: whatever you postMessage() into one arrives at the other. A port is transferable, so you can hand one end to a worker, an iframe or a service worker (SW Messaging) and let the two ends talk privately afterwards. A BroadcastChannel (Broadcast Channel) shouts at every context on the origin and window.postMessage() drops everything into one shared inbox; a channel is point-to-point.

A private two-way pipe between the page and a workerCSS
// tally-worker.js
self.onmessage = ({ ports }) => {
  const port = ports[0];                             // the transferred end
  let total = 0;
  port.onmessage = ({ data }) => port.postMessage((total += data));
};
// page: <script type="module">
const worker = new Worker('tally-worker.js');
const { port1, port2 } = new MessageChannel();
worker.postMessage('here is your end', [port2]);     // transferred, not cloned
port1.onmessage = ({ data }) => console.log('running total', data);
for (const n of [5, 10, 20]) port1.postMessage(n);
Output
running total 5
running total 15
running total 35

Assigning onmessage implicitly calls port.start(); with addEventListener you must call it yourself or nothing is delivered. A transferred port is detached in the sender. Baseline Widely available since Firefox 41 555 shipped it in 2015.