Conversation
`setHeader()` stored headers in a `{ __proto__: null }` literal and
`OutgoingMessage` let EventEmitter create the same literal for its
listener table. V8 creates null-prototype literals in dictionary mode,
so every response paid for a hash table allocation on the first
`setHeader()`/`on()` call and for dictionary lookups on every header
access, including the `for...in` in `_storeHeader()`.
Use a fast-mode object with an empty null-prototype chain for the
headers, and preset the listener table with the common events, as
streams already do.
Hello-world server (`res.setHeader()` + `res.end()`), one core:
40.9k -> 50.0k req/s (+22%); CPU 24.4 -> 20.0 us/req; 4374 -> 3905 B
of young-gen allocation per request. Responses are byte-identical.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
The existing `setHeader` and `setHeaderWH` cases set `Content-Length` explicitly, and `normal` passes every header to `writeHead()`. The very common handler shape `res.setHeader(...)` + `res.end(body)`, where `end()` derives the framing, exercises a different path and was not covered. Signed-off-by: Matteo Collina <hello@matteocollina.com>
Collaborator
|
Review requested:
|
mcollina
marked this pull request as ready for review
September 30, 2026 20:53
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #66420 +/- ##
==========================================
- Coverage 90.39% 90.38% -0.01%
==========================================
Files 792 792
Lines 275580 275704 +124
Branches 52840 52857 +17
==========================================
+ Hits 249104 249193 +89
- Misses 16897 16937 +40
+ Partials 9579 9574 -5
🚀 New features to boost your workflow:
|
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.
setHeader()stored headers in a{ __proto__: null }literal andOutgoingMessageletEventEmittercreate the same literal for its listener table. V8 creates null-prototype literals in dictionary mode, so every response paid for a hash table allocation on the firstsetHeader()/on()call, for runtime-miss stores (the profile showsStoreIC::Store,Factory::NewStoreHandlerandTieringManager::NotifyICChangedin steady state) and for dictionary lookups on every header access, including thefor...inin_storeHeader().This PR uses a fast-mode object with an empty null-prototype chain for the headers (same keys, same enumeration, no inherited properties), and presets the listener table with the common events, as
Readable/Writablealready do.Hello-world server (
res.setHeader('Content-Type', ...)+res.end('Hello World')), server pinned to one core,wrk -t2 -c50on two other physical cores, 4 interleaved rounds:The
_eventspreset accounts for ~2% of that; the header container for the rest. With 8 request headers and a handler that reads two of them: 36.2k → 43.2k req/s. Responses are byte-identical.How much the dictionary-mode container costs depends on the response pattern (same harness, 2 rounds each):
setHeader('Content-Type')+end(body)setHeader('Content-Type')+setHeader('Content-Length')+end(body)writeHead(200, { ... })+end(body)(nosetHeader(),benchmark/fixtures"normal")The existing
benchmark/httpfixtures either setContent-LengththroughsetHeader()or pass every header towriteHead(), so they only show the smaller gains. The second commit adds asetHeaderImplicitcase tohttp/set-header.js(res.setHeader('Content-Type', ...)+res.end(body), framing left toend()).benchmark/compare.js --runs 10 --filter set-header http:simple.js,headers.jsandincoming_headers.jsare unchanged within noise.—-
Ai generated, humanly reviewed.