Codeskill

Learn to code, step by step

Streams and large data in the browser

The Streams API in the browser – reading and writing data chunk by chunk instead of waiting for an entire file or response to land in memory. Useful for large downloads, uploads, log tailing, and progressive rendering.

The problem with buffering everything

response.text() or response.json() waits until the full body arrives. A 200MB export can blow the tab’s memory budget and block the UI while parsing. Streams let you process data as it arrives.

ReadableStream from fetch

const response = await fetch('/api/export.csv');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('n');
  buffer = lines.pop() ?? '';

  for (const line of lines) {
    if (line.trim()) handleRow(parseCsvLine(line));
  }
}

The line-splitting pattern handles chunks that cut mid-line. Keep a buffer for the incomplete tail until the next chunk arrives.

Pipe through transforms

Streams compose with pipeThrough and pipeTo. A transform stream maps chunks – decode bytes to text, parse JSON lines, filter rows.

function lineSplitter() {
  let buffer = '';
  return new TransformStream({
    transform(chunk, controller) {
      buffer += chunk;
      const parts = buffer.split('n');
      buffer = parts.pop() ?? '';
      for (const line of parts) {
        controller.enqueue(line);
      }
    },
    flush(controller) {
      if (buffer) controller.enqueue(buffer);
    },
  });
}

await fetch('/api/log')
  .then((r) => r.body)
  .then((body) =>
    body
      .pipeThrough(new TextDecoderStream())
      .pipeThrough(lineSplitter())
      .pipeTo(new WritableStream({
        write(line) {
          appendLogLine(line);
        },
      }))
  );

Backpressure

Streams have backpressure: a slow consumer signals the producer to pause. If you manually read with getReader(), respect desiredSize on writable sides. Piping handles most of this for you – prefer pipeTo over manual loops when you can.

Streaming uploads

async function uploadFile(file) {
  const stream = file.stream();
  await fetch('/api/upload', {
    method: 'POST',
    body: stream,
    headers: {
      'Content-Type': file.type || 'application/octet-stream',
      'Content-Length': String(file.size),
    },
    duplex: 'half',
  });
}

Streaming uploads need server support and correct CORS headers. Not every hosting stack accepts a raw stream body – test your endpoint.

Generating streams in JavaScript

function countStream(from, to) {
  return new ReadableStream({
    start(controller) {
      for (let n = from; n <= to; n++) {
        controller.enqueue(n);
      }
      controller.close();
    },
  });
}

for await (const n of countStream(1, 5)) {
  console.log(n);
}

for await...of works on async iterables from streams in modern browsers – handy for readable consumption code.

Streams plus workers

Heavy parsing can happen in a worker while the main thread reads chunks from the network. Post each chunk (or batch of lines) to the worker instead of the full payload. Combine with transferables when using binary formats.

When not to bother

  • Small JSON API responses – response.json() is fine.
  • Data that must be random-access (sort entire set before display) – you may still stream into an indexed structure, but plan for memory.
  • Legacy browsers without stream support – feature-detect and fall back to buffered reads.

The next tutorial covers client-side security – XSS, CSRF awareness, and safe DOM APIs.

PreviousWeb Workers and off-main-thread work