Streaming responses

Потоковые ответы (Streaming responses) позволяют получать данные от сервера частями, по мере их готовности, вместо того чтобы ждать завершения всей операции. Это критически важно для работы с большими объёмами данных, интерактивных приложений и реалтайм-интерфейсов. В JavaScript поддержка потоков реализована через Streams API, который является стандартной частью современных браузеров и Node.js.


Основы Streams API

Streams API делится на несколько ключевых концепций:

  • ReadableStream — поток, из которого можно читать данные.
  • WritableStream — поток, в который можно записывать данные.
  • TransformStream — поток, который преобразует данные, проходящие через него.
  • Byte streams и Text streams — потоки могут работать как с бинарными, так и с текстовыми данными.

Каждый поток состоит из двух основных компонентов: источника (source) и контроллера (controller). Источник отвечает за генерацию данных, контроллер — за управление потоком, его состояние и выдачу чанков (chunks).


Создание и чтение ReadableStream

Создание потока выглядит следующим образом:

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue('Hello, ');
    controller.enqueue('world!');
    controller.close();
  }
});

const reader = stream.getReader();

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

readStream();

Объяснение ключевых моментов:

  • controller.enqueue(value) — добавляет данные в поток.
  • controller.close() — сигнализирует о завершении потока.
  • stream.getReader() — получает объект для чтения данных чанками.
  • Метод reader.read() возвращает объект { done, value }. Если поток закончился, done будет true.

Использование Fetch с потоками

Метод fetch в браузерах поддерживает потоковое чтение ответа через response.body, который является ReadableStream. Это позволяет обрабатывать данные по мере их поступления.

Пример чтения JSON-данных частями:

const response = await fetch('https://example.com/large-data.json');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  result += decoder.decode(value, { stream: true });
}

const data = JSON.parse(result);
console.log(data);

Ключевые моменты:

  • TextDecoder позволяет декодировать бинарные чанки в строку.
  • Опция { stream: true } обеспечивает правильное объединение чанков.
  • Потоковое чтение позволяет начать обработку данных до того, как весь файл будет загружен.

Преимущества потоковых ответов

  1. Снижение задержки — первые данные доступны сразу, не дожидаясь полного ответа.
  2. Меньшее потребление памяти — нет необходимости загружать весь объём данных в память.
  3. Интерактивные интерфейсы — можно отображать результаты пользователю по мере их поступления.

Пример: потоковое получение текста от API

async function streamText(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const textChunk = decoder.decode(value, { stream: true });
    process.stdout.write(textChunk);
  }
}

streamText('https://example.com/streaming-endpoint');

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


TransformStream для обработки данных

TransformStream позволяет модифицировать поток данных на лету:

const upperCaseStream = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

const readable = new ReadableStream({
  start(controller) {
    controller.enqueue('hello ');
    controller.enqueue('world');
    controller.close();
  }
});

const transformed = readable.pipeThrough(upperCaseStream);
const reader = transformed.getReader();

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

Поток сначала создаёт данные, затем через pipeThrough передаёт их в TransformStream, который модифицирует их.


Работа с Node.js

В Node.js потоки имеют схожую модель, но API отличается:

import fs from 'fs';

const readable = fs.createReadStream('large-file.txt');
readable.on('data', chunk => {
  console.log('Получен chunk:', chunk.toString());
});

readable.on('end', () => {
  console.log('Поток завершён');
});

Node.js предоставляет события data, end и error для управления потоками.


Асинхронные итераторы

Современный подход к потокам — использование асинхронных итераторов:

const response = await fetch('https://example.com/stream');
const decoder = new TextDecoder();

for await (const chunk of response.body) {
  console.log(decoder.decode(chunk, { stream: true }));
}

Преимущества:

  • Код становится более читаемым и линейным.
  • Обрабатывается каждый чанок без необходимости явно использовать getReader() и read().

Контроль состояния потока

  • locked — показывает, захвачен ли поток читателем.
  • cancel(reason) — позволяет прекратить поток преждевременно.
  • pipeTo(destination) — перенаправляет поток в другой WritableStream.

Эти методы позволяют строить сложные конвейеры обработки данных.


Потоковые ответы в реальном времени

Применение потоков особенно важно при работе с:

  • Веб-сокетами и SSE (Server-Sent Events).
  • Генеративными API, где данные создаются постепенно.
  • Большими файлами, мультимедиа, логами и аналитикой.

С потоками данные можно не просто загружать, но и фильтровать, изменять или визуализировать сразу, не дожидаясь завершения всей операции.


Потоковые ответы создают фундамент для современных веб-приложений, позволяя строить быстрые, отзывчивые и ресурсосберегающие системы.