perf: improve Headers class performance - #5699
Conversation
Add WHATWG Headers unit tests and a fetch/headers benchmark so Node can track the API after the implementation change lands in undici. Refs: nodejs/undici#5699 Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com> Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5699 +/- ##
==========================================
+ Coverage 93.47% 93.48% +0.01%
==========================================
Files 110 110
Lines 38846 39104 +258
==========================================
+ Hits 36310 36557 +247
- Misses 2536 2547 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
mcollina
left a comment
There was a problem hiding this comment.
Adding the benchmarks somewhere will be good.
Also, lint is failing.
@KhafraDev I'm generically ok with these improvements. However it's quite substantial in the code changes. Take a look.
Add WHATWG Headers unit tests and a fetch/headers benchmark so Node can track the API after the implementation change lands in undici. Refs: nodejs/undici#5699 Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
|
Addressed the review feedback:
|
|
Can you fix the metadata of the commits? Cursor cannot signoff commits. |
2899626 to
89e1b3f
Compare
|
Rewrote the commits so author, committer, and Signed-off-by are Yagiz Nizipli yagiz@nizipli.com. |
89e1b3f to
cb75094
Compare
Speed up the WHATWG Headers hot path:
- Combine ByteString conversion, token validation, and lowercasing
into a single pass, with a fast path for common lowercase names.
- Store header values as strings instead of {name, value} objects.
- Copy an existing Headers list without re-validating every entry.
- Skip ToString in the ByteString converter when the value is already
a string.
Assisted by Cursor
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Keep the Headers hot-path changes, but put the fetch spec comments back and drop the unrelated ByteString converter change. Assisted by Cursor Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Address review feedback: - remove unused appendHeader (lint) - add get/set/append/has/construct benches - clear sortedMap instead of the old private symbol Assisted by Cursor Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
cb75094 to
ab9bd62
Compare
|
@tsctx @KhafraDev ptal |
tsctx
left a comment
There was a problem hiding this comment.
One of the main advantages of the original implementation is that it closely follows the specification, with each step represented explicitly in the code. This keeps the maintenance cost low, makes it straightforward to adapt the implementation to future specification changes, and makes it easier to trace errors back to the relevant step.
The proposed changes also introduce a separate implementation of logic that is currently shared. This duplicates the same behavior and error messages across multiple places, increasing the maintenance burden and creating a risk of inconsistencies over time.
Maintaining a close correspondence with the specification is particularly important because the specification is a living standard. Although Headers is relatively stable compared with other areas, it may still evolve over time.
While performance improvements are certainly appealing, I believe the overall ROI of these changes is low. For Headers, iteration performance appears to be more important in typical usage. Operations such as get and append are generally performed only a limited number of times per instance, so optimizing them does not appear to provide much practical benefit, especially when weighed against the additional complexity and maintenance cost.
There are also several behavioral regressions that could result in unexpected behavior. Some of the proposed changes are valuable. However, given these trade-offs, I do not think the changes as a whole are worthwhile.
This relates to...
N/A
Rationale
Headersis on the fetch / Request / Response hot path. The current implementation pays for WebIDL conversion, token validation, andtoLowerCase()as separate passes, and stores every entry as a{ name, value }object.Changes
content-type,accept,authorization, …) onget/has/delete.Mapinstead of{ name, value }objects. Original casing is kept in a side table only when it differs from the lowercase name.Headersinstance by cloning the list instead of re-validating every entry.lib/web/fetch/headers.js.No intended behavioral change. Guard handling,
set-cookie/getSetCookie(), iteration order, and original-name casing on the wire are preserved.Features
N/A
Bug Fixes
N/A
Breaking Changes and Deprecations
N/A
Benchmark
Measured with the source
Headersclass (Node v22.14.0, 200k iterations, paired old vs new):get(custom name)get(content-type)hassetnew Headers(headers)new Headers()appendStatus
Tested locally:
All 70 tests passed, including the 4000-iteration
toSortedArrayfast/slow paths and the fetch cloning cases.