Conversation
An object literal with computed symbol keys is rebuilt through the runtime every time it is evaluated, close to a microsecond each. pipeTo, tee and the byte tee materialized their internal read request as such a literal once per pipe or tee, and the byte tee's BYOB path did so on every read. The requests are now instances of one small class holding the three step functions. A source without pull() ran the pull bookkeeping anyway: two reaction closures and a microtask per read whose only effect was to clear the pulling flag. Push-style sources now skip it. The default tee's pull algorithm was an async function and the byte tee's returned a fresh resolved promise; both now return nothing, which reaches the controller's pull-fulfilled step at the same microtask position without the promise. ReadableStreamBYOBReader.prototype.read() was an async method, so the read request's promise was adopted through a wrapper (an extra promise and two microtask hops per read). It now returns the request's promise directly, as the spec does; argument errors still become rejections. A TransformStream without start() no longer allocates the start promise record: the sides adopt a promise that is already resolved either way. Signed-off-by: Matteo Collina <hello@matteocollina.com>
mcollina
marked this pull request as ready for review
September 20, 2026 12:59
jasnell
reviewed
Sep 20, 2026
|
|
||
| [kError](error) { | ||
| this.errorSteps(error); | ||
| } |
Member
There was a problem hiding this comment.
nit: if StepsReadRequest is internal, why use symbol properties?
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #66154 +/- ##
=======================================
Coverage 90.28% 90.29%
=======================================
Files 790 790
Lines 271974 272023 +49
Branches 51931 51920 -11
=======================================
+ Hits 245561 245625 +64
+ Misses 16901 16899 -2
+ Partials 9512 9499 -13
🚀 New features to boost your workflow:
|
jasnell
approved these changes
Sep 20, 2026
MattiasBuelens
approved these changes
Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 18 of the webstreams performance work (follows #66052). This one targets the fixed cost of wiring up a pipe or a tee, and a few per-read costs that survived the earlier rounds: an internal read request that was rebuilt through the runtime for every pipe, tee and BYOB tee read, a pull cycle that ran for sources that have no
pull(), and two async wrappers on the tee and BYOB read paths.Read requests as a class
An object literal with computed symbol keys (
{ [kChunk]() {}, [kClose]() {}, [kError]() {} }) is rebuilt through the runtime every time it is evaluated, close to a microsecond each (~840 ns vs ~8 ns for a class instance on this machine).pipeTo,teeand the byte tee materialized their internal read request that way once per pipe or tee, and the byte tee's BYOB path did so on every read. The requests are now instances of one small class holding the three step functions.Skip the pull cycle for sources without
pull()A source without
pull()still ran the pull bookkeeping on every read and once at start: two reaction closures and a microtask whose only effect was to clear thepullingflag. Both controllers now return early when the pull algorithm is the shared no-op. Push-style sources (start()enqueues everything,ReadableStream.from-style feeders,new ReadableStream()with no source) hit this on every read.Drop two async wrappers
The default tee's pull algorithm was an
async functionand the byte tee's returned a fresh resolved promise; both now return nothing, which reaches the controller's pull-fulfilled step at the same microtask position without the promise.ReadableStreamBYOBReader.prototype.read()was anasyncmethod, so the read request's promise was adopted through a wrapper (an extra promise and two microtask hops per read). It now returns the request's promise directly, as the spec does; argument errors still become rejections. BYOB reads therefore settle two microtasks earlier, which is the spec's timing.TransformStream without
start()The start promise record is no longer allocated when the transformer has no
start(): both sides adopt a promise that is already resolved either way.Benchmarks
node benchmark/compare.js --runs 20 webstreams(lifecycle, creation, tee, readable-read, readable-read-buffered, pipe-through, pipe-to, readable-async-iterator, from, js_transfer); only the significant rows listed, everything else is within noise:Three rows were flagged negative in the first run (
creation.js kind='ReadableStreamBYOBReader',pipe-through.js kind='default', onepipe-to.jsconfiguration); re-run at 30 runs they all sit inside their intervals (−2.6 % ±4.1 %, −1.2 % ±1.7 %, and the pipe-to rows mixed between −1.7 % and +2.4 %).The official benchmarks drive their sources through
pull(); the shapes this round is really about gain more. On this machine, interleaved runs of a small harness: a pull-drivenpipeTo+27 %,new ReadableStream().tee()+31 %,new ReadableStream()+34 % (the no-op pull cycle at start is gone),new TransformStream()+12 %, create → 4×1KB →pipeThrough→ close +18 %, BYOB reads +6 %.Ordering is unchanged apart from the BYOB timing above: a 48-scenario microtask-ordering stress (start variants, push-only sources, BYOB read timing and argument errors, byte tee with BYOB readers, transform without
start(), pipeTo shutdown paths) logs identically againstmainexcept that BYOB reads settle two ticks earlier; every consumer's own event sequence is byte-identical. WPT streams and the webstreams parallel batch are green.AI generated, humanly reviewed.