Объяснение потоков для веб-разработчиков
Когда вы вызываете 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()возможна, но имеет неравномерную поддержку браузерами. Потоковые ответы — это хорошо поддерживаемый, практичный паттерн, на который стоит ориентироваться сегодня.
Discover how at OpenReplay.com.
Построение конвейеров данных с помощью 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(), когда нужно преобразовать данные, и позвольте противодавлению управлять потоком за вас.
Часто задаваемые вопросы
Да. ReadableStream, WritableStream, TransformStream и методы конвейеризации поддерживаются во всех современных браузерах, включая Chrome, Firefox, Safari и Edge. Потоковая передача тел ответов fetch через response.body также широко поддерживается. Потоковые тела запросов с fetch имеют более ограниченную поддержку, поэтому проверьте таблицы совместимости, прежде чем полагаться на эту функцию.
Если какой-либо этап в цепочке конвейера выбрасывает ошибку, ошибка распространяется по конвейеру. Читаемая сторона переходит в состояние ошибки, а записываемая сторона прерывается. Вы можете обработать это, передав объект опций с сигналом или перехватив промис, возвращаемый из pipeTo. Для циклов ручного чтения оберните вызовы read в блоки try-catch.
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, традиционный подход вполне подходит.
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.
Check our GitHub repo and join the thousands of developers in our community.