Conversation
This was referenced Sep 25, 2026
Member
|
This process cannot be bypassed, and the proposed changes do not allow setting default values properly according to the specifications. |
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.
This relates to...
Per-request cost of
new Request()and offetch(), which constructs one per call. Follows #5701 (skip empty RequestInit work), which touched the same constructor.Rationale
Two steps in the constructor build state that is thrown away at once.
String input builds the inner request twice. For
new Request(string), step 4 creates a full request (makeRequest: a ~40-field object plus aHeadersListand itsMap), and step 12 immediately copies it into a new one, copying the header list again. Nothing can observe the first request, and every field step 12 reads from it is a default.makeRequestalready fills every missing field with its default (init.x ?? default), so step 4 now passes only{ urlList: [parsedURL] }into step 12. One line.A Request input with an init but no
init.headersround-trips the header list. Fornew Request(request, { method: 'PUT' })(retries, middleware), step 32 copies the header list, empties it and appends the copy back, which leaves it unchanged. The steps now run only wheninit.headersexists. The branch that handled aHeadersListin step 32.4 only ever received that copy, because theHeadersInitconverter turns aHeadersobject into a list of its entries, so it is removed (-6 lines).Changes
lib/web/fetch/request.js: the two changes above.test/fetch/request.js: two tests that pin what the second change relies on: a derived request keeps a copy of the headers including multipleset-cookievalues and stays independent of its source; aHeadersobject ininit.headerskeeps all of its entries. Both pass onmaintoo.Features
N/A
Bug Fixes
N/A
Breaking Changes and Deprecations
N/A
Verification (this branch vs
328ab843)Headline numbers are standalone: one revision per process, 8 alternating process pairs, minimum of 100 runs per process, with an identical-code control (base against base) run the same way. That is what you will measure when you build each side separately.
new Request(url)clone()+new Request(req, { method })request-cloneandrequest-initvary more between processes of the same revision than the effect: their identical-code controls span about ±19% and ±33%, and two standalone runs ofrequest-clone(-14.2%, -8.2%) both fall inside its control. So a standalone run cannot resolve them. The paired harness can: focused runs (the suite limited to these cases, 200 paired iterations) measured request-clone -15.0% / -16.4% and request-init -8.5% / -9.8%, against focused identical-code controls within ±4.2%. Those numbers come from two revisions in one process; they are marked as such here and not used as headlines.For completeness, the paired runs this branch was verified with (they decide experiments, they do not set the claim):
parse-headersshowed +3.72% in full run 2 and -0.67% in run 1. The change does not touch that code, and standalone it is equal (4.22/4.29/4.19 ms base, 4.23/4.23/4.21 ms branch): an in-process artefact of loading two revisions into one process.External cross-check:
benchmarks/fetch/request-creation.mjs(mitata, one revision per process, three fresh processes each) measurednew Request(input)at 286-292 ns onmainand 229-259 ns on this branch (-11..-21%), consistent with the standalone runs above.Retained memory per instance is unchanged (request-url 1496 B, request-init 4211 B): the removed objects were garbage.
Observable surface
set-cookie, independence after appends on either side, no-cors mode, aHeadersobject asinit.headers) are identical.sortedMap) until either list changes, instead of dropping it in step 32.3. That was already the case fornew Request(request)without an init.Invariants this relies on
makeRequestdefaults every field it is not given (init.x ?? default, and a freshHeadersListwhenheadersListis missing). A new request field without a default would readundefinedfor string input.HeadersInitconverter turns aHeadersobject into an entry list (the existing workaround from fetch: don't re-lowercase HeadersList #3159). If aHeadersobject ever reachedfill(), it would be handled as a record. The new test forHeadersininit.headersfails in that case.Open questions for maintainers (not in this PR)
abortlistener, and Node'sEventTargetchecks for duplicates by walking the list. 4 000 / 16 000 / 64 000new Request(url, { signal })on one live signal took 32 ms / 559 ms / 13.3 s (step ratios 17x and 24x instead of 4x). A dependent signal (AbortSignal.any, as the current DOM spec's "create a dependent abort signal" does and as theTODOat step 28 suggests) would make it O(1), but a dependent signal aborts after the source's own listeners, which changes the order relative to user listeners added later.init.headersproduceTypeError: undefined: undefined.headers["a"] is a symbol, ..., because the RequestInit/ResponseInit converters run without a prefix and argument name.WPT (
npm run test:wpt: /fetch, /mimesniff, /xhr, /websockets, /eventsource) was run locally on328ab843and on this branch: per test and per case, pass/fail is identical across all 1 282 test files.Note on CI:
Test with Node.js 22 compiled --without-intlalready fails onmainat328ab843, before this change.Reproducing the numbers
The harness (A/B runner, guard, differential, focused and standalone runners) lives in
perf/on the campaign branch: zirkelc/undici@afde9423/perf (the plan and the experiment log with all discarded experiments are in the same directory). It needs Node 24 (it runs.mtsdirectly) and no install beyondnpm ci.A row marked
?has an interquartile band that crosses zero in that run; judge by two runs that agree, and by the standalone numbers.perf/cases.mts, the workloads these numbers come fromStatus
Companion PRs from the same campaign: #5902 (perf(websocket): mask frames without a mask array, four bytes per step), #5903 (perf(webidl): check ByteString code units with a native scan), #5904 (perf(cookies): split each cookie pair once in getCookies)