Skip to content

stream: speed up reads and iteration over default WHATWG streams#64320

Open
mcollina wants to merge 3 commits into
nodejs:mainfrom
mcollina:webstream-iterator-perf-round6
Open

stream: speed up reads and iteration over default WHATWG streams#64320
mcollina wants to merge 3 commits into
nodejs:mainfrom
mcollina:webstream-iterator-perf-round6

Conversation

@mcollina

@mcollina mcollina commented Jul 6, 2026

Copy link
Copy Markdown
Member

Skip the size algorithm call when it is the default one, and stop re-running the full ShouldCallPull predicate at per-chunk call sites where its inputs are already established.

readable-async-iterator type=normal: +16.6% (***)
readable-read-buffered: +13.9% to +27.5% (**/***)
pipe-to: +3.7% to +6.0% (15/16 configs significant)

The first two commits are #64291; only the last commit is new here.

mcollina added 3 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>
Skip the size algorithm call and its result validation when the size
algorithm is the default one, and stop re-running the full
ShouldCallPull predicate at per-chunk call sites where its inputs are
already established.

readable-async-iterator type=normal: +16.6% (***)
readable-read-buffered: +13.9% to +27.5% (**/***)
pipe-to: +3.7% to +6.0% (15/16 configs significant)

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@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 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. web streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants