Back to Hub
NETWORK ARCHITECTURE •

Everything the Browser Knows About the Network.

Most frontend developers know exactly one networking API: fetch(). But the browser's networking toolkit goes much deeper — streaming responses, resumable background downloads, dictionary-compressed payloads, and even raw QUIC connections. This guide tours the full stack, with the honest limits of each API spelled out.

Beyond await fetch(): streaming responses

fetch() doesn't have to be all-or-nothing. The response body is a ReadableStream — you can process data as it arrives, which is how you build progress bars, live logs, and chat-style token streaming:

const res = await fetch('/api/big-report');
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  appendChunk(decoder.decode(value, { stream: true }));
  updateProgress(value.length);
}

Need to transform a stream mid-flight — say, decrypting or decompressing chunks? Pipe it through a TransformStream:

const upper = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase()); // toy transform
  }
});
await res.body.pipeThrough(upper).pipeTo(writableStream);

Downloads that survive tab closes: Background Fetch

Regular fetch() dies with your tab. The Background Fetch API hands large downloads (podcasts, datasets, asset packs) to the browser itself, which keeps downloading even if the user navigates away — and shows native progress UI:

const reg = await navigator.serviceWorker.ready;
const bgFetch = await reg.backgroundFetch.fetch(
  'asset-pack-42',
  ['/assets/pack-1.zip', '/assets/pack-2.zip'],
  { title: 'Downloading asset pack', icons: [{ src: '/icon.png', sizes: '256x256' }] }
);
bgFetch.addEventListener('progress', () => {
  const pct = bgFetch.downloaded / bgFetch.downloadTotal;
  updateProgressBar(pct);
});

Honest limits: it requires a registered service worker, it's Chromium-only for now, and it's designed for downloads (uploads aren't supported). For everyday small requests, plain fetch() is still the right tool.

Shrinking repeat payloads: compression dictionaries

If your app downloads many similar files — versions of a WASM module, map tiles, ML weights — most bytes repeat. Compression Dictionary Transport (rolling out in Chromium) lets the browser reuse a previously-downloaded file as a Brotli/Zstandard dictionary, so each subsequent download only transfers the diff:

// Server marks a response as usable-as-dictionary:
// Use-As-Dictionary: match="/app-v1.wasm"
// Later requests can then reference it and download far less.
const res = await fetch('/app-v2.wasm'); // mostly delta bytes

This is an emerging standard — check current browser support before depending on it — but the direction is clear: the browser is becoming a smarter, delta-aware download manager.

When to use what: small JSON → plain fetch(). Progressive rendering → streams. Multi-hundred-MB downloads the user might background → Background Fetch. Near-identical repeated payloads → compression dictionaries.

WebTransport: QUIC without WebSockets' baggage

WebSockets run over TCP, so one dropped packet stalls every stream on the connection (head-of-line blocking). WebTransport runs over HTTP/3's QUIC, giving you independent, multiplexed streams where a lost packet only delays its own stream:

const wt = new WebTransport('https://example.com:4433/sync');
await wt.ready; // QUIC handshake done
// Independent bidirectional stream — a stall here
// doesn't block your other streams:
const stream = await wt.createBidirectionalStream();
const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode('hello quic'));

The honest part: WebTransport needs a server that speaks HTTP/3 — you can't use it against a plain HTTPS endpoint, and there's no "WebTransport echo server" built into browsers for testing. It's the right tool for game networking, live media, and low-latency sync when you control the server. For everything else, WebSockets remain the pragmatic default.

Mocking APIs with service workers

Prototyping against an API that doesn't exist yet (or is blocked by CORS)? A service worker can intercept fetch and return synthetic responses — no backend, no proxy, fully offline:

// sw.js
self.addEventListener('fetch', (event) => {
  if (new URL(event.request.url).pathname === '/api/users') {
    event.respondWith(Response.json([{ id: 1, name: 'Ada' }]));
  }
});

This is genuinely useful inside a client-side IDE: build the whole frontend against mocked endpoints, then swap in the real API later without changing a line of UI code.

Prototype Network-Heavy Frontends.

Streams, mocks, and offline patterns — test them all in a zero-setup browser IDE.

Launch NitroIDE