Обычный fetch работает так: браузер ждёт, пока сервер отправит весь ответ, и только потом отдаёт данные вашему коду. Для небольших JSON-ов это нормально. Но если ответ большой, или сервер генерирует его постепенно (как это делают языковые модели при стриминге), ждать полного ответа — плохая идея. Streams API позволяет читать ответ прямо по мере поступления.

В ReadableStream каждый ответ fetch имеет свойство body — это ReadableStream. Из него можно читать данные кусками (chunks) по мере их поступления:

const response = await fetch('/api/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();

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

Метод reader.read() возвращает промис с объектом { done, value }. value — это Uint8Array (сырые байты), поэтому нужен TextDecoder для перевода в строку. Когда данные кончаются, done становится true.

Зачем это нужно

Самый популярный сценарий сегодня — стриминг ответа от LLM-сервисов. ChatGPT, Claude и другие модели отправляют текст постепенно, слово за словом. Это именно поток: сервер держит соединение открытым и шлёт данные по мере генерации.

Другие сценарии:

  • загрузка больших файлов с отображением прогресса
  • чтение CSV или JSON построчно без загрузки всего файла в память
  • Server-Sent Events через fetch
Нажмите кнопку, чтобы увидеть потоковое чтение...

Отмена стриминга

Читатель потока можно отменить в любой момент через reader.cancel() или через AbortController:

const controller = new AbortController();
const response = await fetch('/api/stream', {
  signal: controller.signal
});

const reader = response.body.getReader();

// Позже — отменяем поток
controller.abort();

Трансформация потока

Streams API позволяет создавать цепочки трансформаций через TransformStream. Например, декодировать байты в текст прямо в потоке:

const stream = response.body
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(new TransformStream({
    transform(chunk, controller) {
      controller.enqueue(chunk.toUpperCase());
    }
  }));

Это мощный инструмент для обработки больших данных без загрузки всего в память.

ReadableStream и fetch().body поддерживаются во всех современных браузерах. TextDecoderStream и pipeThrough — тоже. В Node.js нативный fetch со стримингом доступен с версии 18.