Skip to content

stream: speed up async iteration over WHATWG byte streams#64291

Open
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:webstream-byte-iteration-perf
Open

stream: speed up async iteration over WHATWG byte streams#64291
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:webstream-byte-iteration-perf

Conversation

@mcollina

@mcollina mcollina commented Jul 4, 2026

Copy link
Copy Markdown
Member

for await / reader.read() loops over byte streams were ~4x slower than over default streams:

  • replace the ReflectGet(view.constructor.prototype, ...)-based ArrayBufferView getters with primordial getters (~3.5x faster at the call level, and no longer spoofable via a user-defined .constructor)
  • extend the buffered fast paths in ReadableStreamDefaultReader.read() and the async iterator to byte controllers, skipping the per-chunk read request + PromiseWithResolvers when data is queued
  • consolidate the reader predicate cascade in the byte controller's enqueue into a single pass; reuse the async iterator's read request object

benchmark/webstreams (--runs 10): readable-async-iterator type=bytes +16.3% (***), readable-read byob +9.1% (***), all other rows neutral. WPT streams/compression/encoding results unchanged.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. web streams labels Jul 4, 2026
mcollina added 2 commits July 4, 2026 23:29
for await / reader.read() loops over byte streams were ~4x slower than
over default streams. Three per-chunk costs, none required by the spec:

- ArrayBufferViewGetBuffer/ByteLength/ByteOffset went through
  ReflectGet(view.constructor.prototype, ...), a reflective get that is
  ~3.5x slower than the original prototype getters from primordials and
  spoofable through a user-defined .constructor to boot.

- The buffered fast paths in ReadableStreamDefaultReader.read() and the
  async iterator only covered default controllers, so byte streams with
  queued data still allocated a read request and PromiseWithResolvers
  per chunk. Byte-queue dequeue is fully synchronous (it is the
  queue-filled arm of the byte controller's pull steps), so both fast
  paths now resolve directly from the byte queue.

- readableByteStreamControllerEnqueue re-ran the reader brand check and
  re-loaded the read request list four times per chunk across
  HasDefaultReader / ProcessReadRequestsUsingQueue / GetNumReadRequests
  / FulfillReadRequest; it now does a single pass. The async iterator
  also reuses its read request object across reads (at most one is ever
  in flight).

benchmark/webstreams interleaved same-day A/B, --runs 10:
readable-async-iterator bytes +16.3% (***), readable-read byob +9.1%
(***), all other rows neutral. Profiler harness: parked byte iteration
+14%, buffered byte iteration +37%, buffered byte read loop +18%,
default-stream rows at parity. WPT streams/compression/encoding
subtests identical to baseline.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina force-pushed the webstream-byte-iteration-perf branch from 2782884 to fa8ad29 Compare July 4, 2026 21:29
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 5, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 5, 2026
@nodejs-github-bot

nodejs-github-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

CI: https://ci.nodejs.org/job/node-test-pull-request/74561/
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1881/
Benchmark GHA: https://github.com/nodejs/node/actions/runs/28751317798

Results
                                                                       confidence improvement accuracy (*)   (**)  (***)
webstreams/creation.js kind='ReadableStream.tee' n=50000                              -0.02 %       ±0.50% ±0.67% ±0.87%
webstreams/creation.js kind='ReadableStream' n=50000                                   3.76 %       ±5.22% ±6.95% ±9.05%
webstreams/creation.js kind='ReadableStreamBYOBReader' n=50000                        -0.20 %       ±1.58% ±2.11% ±2.75%
webstreams/creation.js kind='ReadableStreamDefaultReader' n=50000                     -0.60 %       ±1.53% ±2.04% ±2.66%
webstreams/creation.js kind='TransformStream' n=50000                                  0.50 %       ±1.39% ±1.85% ±2.41%
webstreams/creation.js kind='WritableStream' n=50000                                   0.27 %       ±1.57% ±2.09% ±2.71%
webstreams/js_transfer.js n=10000 payload='ReadableStream'                             0.13 %       ±0.53% ±0.70% ±0.92%
webstreams/js_transfer.js n=10000 payload='TransformStream'                            0.23 %       ±0.47% ±0.63% ±0.81%
webstreams/js_transfer.js n=10000 payload='WritableStream'                             0.20 %       ±0.60% ±0.80% ±1.04%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=1024 n=500000                -0.26 %       ±1.52% ±2.03% ±2.64%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=2048 n=500000                 0.42 %       ±1.42% ±1.89% ±2.46%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=4096 n=500000         **     -1.53 %       ±1.13% ±1.51% ±1.96%
webstreams/pipe-to.js highWaterMarkW=1024 highWaterMarkR=512 n=500000                 -1.05 %       ±1.20% ±1.60% ±2.09%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=1024 n=500000                -0.86 %       ±1.26% ±1.68% ±2.19%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=2048 n=500000                 0.25 %       ±1.36% ±1.80% ±2.35%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=4096 n=500000                 0.53 %       ±1.02% ±1.36% ±1.77%
webstreams/pipe-to.js highWaterMarkW=2048 highWaterMarkR=512 n=500000                  0.08 %       ±1.35% ±1.80% ±2.34%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=1024 n=500000                 0.00 %       ±1.30% ±1.73% ±2.26%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=2048 n=500000                -0.75 %       ±1.26% ±1.68% ±2.19%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=4096 n=500000                -1.00 %       ±1.51% ±2.01% ±2.61%
webstreams/pipe-to.js highWaterMarkW=4096 highWaterMarkR=512 n=500000                  0.13 %       ±1.33% ±1.77% ±2.30%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=1024 n=500000                  0.25 %       ±1.21% ±1.61% ±2.09%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=2048 n=500000                  0.00 %       ±1.29% ±1.72% ±2.24%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=4096 n=500000                  0.64 %       ±1.22% ±1.63% ±2.12%
webstreams/pipe-to.js highWaterMarkW=512 highWaterMarkR=512 n=500000                   0.04 %       ±1.14% ±1.51% ±1.97%
webstreams/readable-async-iterator.js type='bytes' n=100000                   ***     13.42 %       ±0.83% ±1.11% ±1.44%
webstreams/readable-async-iterator.js type='normal' n=100000                           0.87 %       ±3.92% ±5.22% ±6.80%
webstreams/readable-read-buffered.js bufferSize=1 n=100000                            -0.95 %       ±2.10% ±2.79% ±3.64%
webstreams/readable-read-buffered.js bufferSize=10 n=100000                           -1.40 %       ±4.38% ±5.83% ±7.59%
webstreams/readable-read-buffered.js bufferSize=100 n=100000                          -1.80 %       ±5.12% ±6.81% ±8.86%
webstreams/readable-read-buffered.js bufferSize=1000 n=100000                         -2.79 %       ±4.64% ±6.17% ±8.04%
webstreams/readable-read.js type='byob' n=100000                              ***      8.58 %       ±0.97% ±1.29% ±1.69%
webstreams/readable-read.js type='normal' n=100000                                    -0.42 %       ±2.33% ±3.10% ±4.04%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 33 comparisons, you can thus
expect the following amount of false-positive results:
  1.65 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.33 false positives, when considering a   1% risk acceptance (**, ***),
  0.03 false positives, when considering a 0.1% risk acceptance (***)

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@aduh95 aduh95 added author ready PRs that have at least one approval, no pending requests for changes, and a CI started. commit-queue Add this label to land a pull request using GitHub Actions. labels Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no pending requests for changes, and a CI started. commit-queue Add this label to land a pull request using GitHub Actions. needs-ci PRs that need a full CI run. web streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants