Skip to content

http: avoid dictionary-mode objects in responses - #66420

Open
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:http-fast-mode-objects
Open

mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:http-fast-mode-objects

Conversation

@mcollina

@mcollina mcollina commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

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, for runtime-miss stores (the profile shows StoreIC::Store, Factory::NewStoreHandler and TieringManager::NotifyICChanged in steady state) and for dictionary lookups on every header access, including the for...in in _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/Writable already do.

Hello-world server (res.setHeader('Content-Type', ...) + res.end('Hello World')), server pinned to one core, wrk -t2 -c50 on two other physical cores, 4 interleaved rounds:

req/s user µs/req sys µs/req alloc B/req
main 40.9k 15.0 9.4 4374
this PR 50.0k 11.3 8.7 3905

The _events preset 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):

handler main this PR
setHeader('Content-Type') + end(body) 41.7–42.4k 51.3–51.8k
setHeader('Content-Type') + setHeader('Content-Length') + end(body) 47.1–49.6k 50.0–51.7k
writeHead(200, { ... }) + end(body) (no setHeader(), benchmark/fixtures "normal") 46.1–50.0k 50.2–50.9k

The existing benchmark/http fixtures either set Content-Length through setHeader() or pass every header to writeHead(), so they only show the smaller gains. The second commit adds a setHeaderImplicit case to http/set-header.js (res.setHeader('Content-Type', ...) + res.end(body), framing left to end()). benchmark/compare.js --runs 10 --filter set-header http:

                                                                        confidence improvement accuracy (*)   (**)   (***)
http/set-header.js duration=5 res='normal' benchmarker='wrk'                            2.53 %       ±4.03% ±5.61%  ±7.86%
http/set-header.js duration=5 res='setHeader' benchmarker='wrk'                 **      6.13 %       ±4.03% ±5.55%  ±7.61%
http/set-header.js duration=5 res='setHeaderImplicit' benchmarker='wrk'        ***     16.46 %       ±5.10% ±7.06%  ±9.78%
http/set-header.js duration=5 res='setHeaderWH' benchmarker='wrk'                       4.78 %       ±5.76% ±7.89% ±10.75%

simple.js, headers.js and incoming_headers.js are unchanged within noise.

—-

Ai generated, humanly reviewed.

`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>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/http
  • @nodejs/net
  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added http Issues and PRs related to the http subsystem. needs-ci PRs that need a full CI run. labels Sep 30, 2026
@mcollina
mcollina marked this pull request as ready for review September 30, 2026 20:53
@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.38%. Comparing base (8bf7793) to head (3aacac4).
⚠️ Report is 7 commits behind head on main.

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     
Files with missing lines Coverage Δ
lib/_http_outgoing.js 97.98% <100.00%> (+0.02%) ⬆️

... and 35 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.

@mcollina
mcollina requested review from pimterry and ronag October 1, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

http Issues and PRs related to the http subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants