12k
All articles

Объяснение потоков для веб-разработчиков

Обработка fetch-ответов по частям с помощью Web Streams API, ReadableStream и TransformStream снижает нагрузку на память и повышает производительность.

OpenReplay Team
OpenReplay Team
Объяснение потоков для веб-разработчиков

Когда вы вызываете fetch() и ожидаете ответ, браузер уже получает эти данные по частям. Web Streams API предоставляет вашему JavaScript-коду доступ к этим частям по мере их поступления, вместо того чтобы ждать, пока весь ответ загрузится, прежде чем вы сможете с ним работать.

Этот переход — от «ждать всё» к «обрабатывать по мере поступления» — вот в чём суть потоков.

Ключевые выводы

  • Web Streams API позволяет обрабатывать данные инкрементально по мере их поступления, вместо буферизации целых ответов в памяти.
  • ReadableStream, WritableStream и TransformStream — три основных примитива, композируемые строительные блоки для конвейеров данных.
  • response.body из fetch() — наиболее распространённая точка входа: ReadableStream, который можно читать порциями.
  • Используйте pipeThrough() и pipeTo() для объединения преобразований и выводов в цепочки, со встроенной автоматической обработкой противодавления.

Почему загрузка всего сразу — это проблема

Традиционный подход к получению данных выглядит так:

const response = await fetch('/large-dataset.json')
const data = await response.json()
// Nothing happens until all bytes are downloaded and parsed

Для небольших объёмов данных это нормально. Для JSON-файла размером 50 МБ или долго выполняющегося API-ответа вы держите всё это в памяти, прежде чем обработать хотя бы одну запись. На устройствах с ограниченными ресурсами или медленных соединениях это означает медленный UI, высокую нагрузку на память и разочарованных пользователей.

Потоки позволяют начать работу с данными в момент поступления первой порции.

Три основных примитива Web Streams API

Web Streams API построен вокруг трёх классов:

  • ReadableStream — источник, из которого вы читаете данные
  • WritableStream — назначение, в которое вы записываете данные
  • TransformStream — находится посередине, читая с одной стороны и записывая преобразованные данные в другую

Данные перемещаются через эти потоки порциями (chunks) — небольшими частями, обрабатываемыми по одной за раз. Порция может быть Uint8Array байтов, строкой или любым JavaScript-значением, в зависимости от потока.

Потоковая передача Fetch: инкрементальное чтение ответа

Большинство ответов fetch() предоставляют своё тело как ReadableStream через response.body. Это наиболее распространённая точка входа в JavaScript-потоки для фронтенд-разработчиков.

async function processLargeResponse(url) {
  const response = await fetch(url)
  const reader = response.body.getReader()
  const decoder = new TextDecoder()

  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      console.log(decoder.decode(value, { stream: true }))
    }
  } finally {
    reader.releaseLock()
  }
}

reader.read() возвращает промис, который разрешается с { value, done }. Когда done равно true, поток завершён. Этот паттерн позволяет обрабатывать многомегабайтный ответ порция за порцией, без буферизации всего целиком.

Примечание о потоковых телах запросов: Передача ReadableStream в качестве тела запроса fetch() возможна, но имеет неравномерную поддержку браузерами. Потоковые ответы — это хорошо поддерживаемый, практичный паттерн, на который стоит ориентироваться сегодня.

Построение конвейеров данных с помощью pipeThrough() и pipeTo()

По-настоящему мощными потоки становятся при композиции. Вы можете пропустить ReadableStream через один или несколько экземпляров TransformStream и направить результат в WritableStream.

fetch('./data.txt').then((response) =>
  response.body
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(new TransformStream({
      transform(chunk, controller) {
        controller.enqueue(chunk.toUpperCase())
      }
    }))
    .pipeTo(new WritableStream({
      write(chunk) {
        document.body.textContent += chunk
      }
    }))
)

Этот конвейер декодирует байты в текст, преобразует каждую порцию в верхний регистр, затем записывает её в DOM — всё инкрементально, без ожидания полного ответа.

pipeThrough() соединяет ReadableStream с TransformStream и возвращает новый ReadableStream. pipeTo() соединяет ReadableStream с WritableStream и возвращает промис, который разрешается по завершении потока.

Противодавление: как потоки избегают перегрузки

Когда потребитель обрабатывает данные медленнее, чем производитель их генерирует, потоки применяют противодавление (backpressure) — сигнал, который распространяется обратно по цепочке конвейера, сообщая источнику замедлиться. Это происходит автоматически при использовании pipeTo() и pipeThrough(). Это одна из главных причин предпочесть конвейеры ручному чтению порций в цикле.

Встроенные потоки, которые стоит знать

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

  • TextDecoderStream / TextEncoderStream — преобразование между байтами и строками
  • CompressionStream / DecompressionStream — сжатие или распаковка данных gzip или deflate на лету
  • Blob.stream() — чтение любого Blob или File как ReadableStream

Современный Node.js также поддерживает Web Streams API, поэтому конвейеры, которые вы создаёте для браузера, легко переносятся в серверные окружения.

Заключение

Web Streams API предоставляет фронтенд-разработчикам композируемый, эффективный по памяти способ обработки данных, поступающих со временем. ReadableStream и TransformStream — примитивы, которые вы будете использовать чаще всего, особенно в сочетании с fetch() для инкрементальной обработки ответов. Начните с response.body, используйте pipeThrough(), когда нужно преобразовать данные, и позвольте противодавлению управлять потоком за вас.

Часто задаваемые вопросы

Могу ли я использовать Web Streams API во всех основных браузерах?

Да. ReadableStream, WritableStream, TransformStream и методы конвейеризации поддерживаются во всех современных браузерах, включая Chrome, Firefox, Safari и Edge. Потоковая передача тел ответов fetch через response.body также широко поддерживается. Потоковые тела запросов с fetch имеют более ограниченную поддержку, поэтому проверьте таблицы совместимости, прежде чем полагаться на эту функцию.

Что происходит, если в середине потока в конвейере возникает ошибка?

Если какой-либо этап в цепочке конвейера выбрасывает ошибку, ошибка распространяется по конвейеру. Читаемая сторона переходит в состояние ошибки, а записываемая сторона прерывается. Вы можете обработать это, передав объект опций с сигналом или перехватив промис, возвращаемый из pipeTo. Для циклов ручного чтения оберните вызовы read в блоки try-catch.

Чем Web Streams отличаются от потоков Node.js?

Node.js изначально поставлялся со своим собственным API потоков с классами Readable, Writable и Transform. Web Streams API — это отдельный стандарт, разработанный для браузеров. Современные версии Node.js поддерживают оба. Web Streams API использует модель на основе pull с промисами, в то время как классические потоки Node используют событийную модель push. Код, написанный для Web Streams API, переносим между браузерными и серверными окружениями.

Когда следует избегать использования потоков и просто буферизовать полный ответ?

Если ответ небольшой, скажем, менее нескольких сотен килобайт, буферизация с помощью response.json или response.text проще и вполне эффективна. Потоки добавляют ценность при работе с большими объёмами данных, данными реального времени или ситуациями, когда вы хотите отображать частичные результаты до поступления полного ответа. Для простых API-вызовов, возвращающих компактный JSON, традиционный подход вполне подходит.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.