Skip to content

stream: speed up WHATWG web streams - #65273

Open
anonrig wants to merge 3 commits into
nodejs:mainfrom
anonrig:stream-speed-up-webstreams
Open

stream: speed up WHATWG web streams#65273
anonrig wants to merge 3 commits into
nodejs:mainfrom
anonrig:stream-speed-up-webstreams

Conversation

@anonrig

@anonrig anonrig commented Aug 13, 2026

Copy link
Copy Markdown
Member

Behavior-preserving performance work on node:stream/web. Spec
orchestration and brand checks stay in JS; the per-chunk data plane
drops Promise/microtask churn for the common sync pull/write case
and gets a small native helper on the hot path.

  • Skip async wrappers on sync source/sink algorithms.
    createPromiseCallback* now calls the user function and returns
    the raw result. Non-thenable results settle via queueMicrotask
    (same position as Promise.resolve().then) instead of allocating
    a Promise per pull/write/start.
  • pipeTo fills a default readable queue from sync pulls and
    still batches already-queued chunks into the destination. Further
    spec pull-fulfillment (tee, WPT) stays one pull per microtask.
  • pipeTo's shared write tracker completes sync sink writes in
    the fulfillment turn. Regular writer.write() keeps the spec
    one-completion-per-microtask order.
  • Native internalBinding('webstreams'): Fast API
    isNonThenable() on every pull/write/start result, and
    cloneAsUint8Array() as a single memcpy for byte-stream / tee
    clones.
  • Cheaper empty construction. new ReadableStream() /
    new WritableStream() skip validateObject on the shared empty
    sentinels. The writable AbortController is created lazily and
    materialized on abort(), so controller.signal observed after
    abort is still aborted with that reason.

Public constructors, methods, and WHATWG Streams behavior
(backpressure, BYOB, pipeTo, tee, errors, transfer) are unchanged.

Benchmarks

benchmark/compare.js --runs 10 of the in-repo webstreams/ suite
on the same machine, same out/Release/node family (pre-change
binary vs this tree). Rates are ops/sec.

Hot-path geometric mean of new/old across all configs of
pipe-to.js, readable-read.js, readable-read-buffered.js,
creation.js, readable-async-iterator.js, and tee.js:
1.94x (32 configs). Full suite including js_transfer.js:
1.84x (35 configs). No config has mean(new)/mean(old) < 1.0
(min 1.03 on js_transfer ReadableStream).

script config old new ratio
pipe-to HWM 512/512 2.15M 4.83M 2.25
pipe-to HWM 4096/4096 2.17M 4.85M 2.24
tee normal 1.12M 2.56M 2.29
readable-async-iterator normal 3.77M 8.59M 2.28
creation WritableStream 0.67M 1.37M 2.05
readable-read-buffered bufferSize=1000 5.61M 12.17M 2.17
readable-read normal 3.99M 5.37M 1.35
creation ReadableStream 1.35M 2.01M 1.49
js_transfer ReadableStream 59.0k 61.0k 1.03

Tests

  • test/wpt/test-streams.js
  • test/parallel/test-whatwg-readable*, writable*, transform*,
    webstreams*, test-webstreams*, test-global-webstreams.js
  • new test/parallel/test-whatwg-webstreams-hotpath.js (public
    read() / pipeTo, native helpers, abort-before-signal)
  • abort-before-signal case in test-whatwg-writablestream.js

AI assistance

This change was developed with assistance from Grok. I reviewed,
tested, and take responsibility for the submitted code.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/gyp

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 13, 2026
Avoid per-chunk async wrappers for sync pull/write/start, fill
default readable queues in pipeTo, and complete pipeTo writes
without one microtask per chunk. Add a native webstreams binding
with a Fast API isNonThenable check on the data plane and a
memcpy clone for byte views.

Empty stream construction skips redundant validation and lazily
creates the writable AbortController, materializing it on abort()
so controller.signal still reflects the abort reason.

Assisted-by: Grok
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Declare isNonThenable and cloneAsUint8Array on the new webstreams
binding and register it in InternalBindingMap.

Assisted-by: Grok
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@anonrig
anonrig force-pushed the stream-speed-up-webstreams branch from 6a3f89b to c796760 Compare August 13, 2026 23:23
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.54545% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.32%. Comparing base (e1cdcec) to head (8ce1778).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
src/node_webstreams.cc 78.26% 6 Missing and 4 partials ⚠️
lib/internal/webstreams/readablestream.js 93.75% 7 Missing ⚠️
lib/internal/webstreams/writablestream.js 98.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65273      +/-   ##
==========================================
+ Coverage   90.31%   90.32%   +0.01%     
==========================================
  Files         751      752       +1     
  Lines      249840   250266     +426     
  Branches    47180    47282     +102     
==========================================
+ Hits       225645   226064     +419     
- Misses      15587    15591       +4     
- Partials     8608     8611       +3     
Files with missing lines Coverage Δ
lib/internal/webstreams/transformstream.js 98.58% <100.00%> (+0.01%) ⬆️
lib/internal/webstreams/util.js 97.89% <100.00%> (+0.17%) ⬆️
src/node_binding.cc 82.42% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
lib/internal/webstreams/writablestream.js 99.47% <98.88%> (-0.05%) ⬇️
lib/internal/webstreams/readablestream.js 98.03% <93.75%> (-0.14%) ⬇️
src/node_webstreams.cc 78.26% <78.26%> (ø)

... and 47 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/internal/webstreams/readablestream.js
Comment thread lib/internal/webstreams/readablestream.js
Comment thread lib/internal/webstreams/readablestream.js Outdated
Comment thread lib/internal/webstreams/util.js
Comment thread lib/internal/webstreams/util.js
Comment thread lib/internal/webstreams/util.js Outdated
Comment thread src/node_webstreams.cc
@jasnell jasnell added the semver-major PRs that contain breaking changes and should be released in the next major version. label Aug 14, 2026
@jasnell

jasnell commented Aug 14, 2026

Copy link
Copy Markdown
Member

Defensively marking this semver-major. If you can show that the optimization does not change observable behavior, that can be dropped, but the change in microtask timing from one pull to the next is likely observable.

Comment thread lib/internal/webstreams/readablestream.js
@anonrig anonrig added the commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. label Aug 14, 2026
@anonrig
anonrig requested a review from jasnell August 14, 2026 14:08
@anonrig

anonrig commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

@jasnell I believe semver-major is no longer needed.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark GHA (webstreams): https://github.com/nodejs/node/actions/runs/31808780686

Defer the default controller on new ReadableStream() until first
use. Settle non-thenable pull/write with Promise.resolve().then so
a throw in the fulfillment callback is an unhandled rejection, as
on the promise path. Share a single no-op algorithm, format the
native binding, and cover Proxy thenables.

Also regenerate the ninja graph on gyp changes and reconfigure the
benchmark CI merge-commit build so new sources such as
src/node_webstreams.cc are linked.

Assisted-by: Grok
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@anonrig
anonrig force-pushed the stream-speed-up-webstreams branch from 8ce1778 to 27c4cf3 Compare August 14, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. semver-major PRs that contain breaking changes and should be released in the next major version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants