http: cut per-message outgoing overhead
Behavior-preserving performance work on lib/_http_outgoing.js, reducing
per-message and per-header overhead on the outgoing-message hot paths:
- Cache the lenient-header-validation decision per message. It was
re-derived on everysetHeader/appendHeadercall (and peraddTrailers
key) through a six-branchthis.req?.socket?.server?walk, although the
inputs are fixed once the message is constructed. The decision is now
resolved lazily on first use and cached on the instance. - Pre-filter the known-field matcher on (length, first character).
matchHeaderlower-cased every outgoing header name of length 4–17 just
to feed a switch with eight known fields. Ordinary headers
(X-Request-Id,Cache-Control, …) now skip the per-header
toLowerCase()allocation; the filter only rejects names that cannot
possibly match (| 0x20lower-cases ASCII letters and maps no other token
character onto a letter, and every known-field length is enumerated). - Load the
socketprototype accessor once per body write instead of
invoking the getter three times inwrite_. - Hoist repeated property loads the engine cannot fold across calls:
_headerin_send,headers.lengthacrossprocessHeadercalls,
_keepAliveTimeout/_maxRequestsPerSocket/_contentLengthin
_storeHeader,value.lengthinprocessHeader. - Flush corked chunked buffers through a shared callback runner instead
of allocating a fresh closure per flush.
The hoists mirror what Bun's fork of these files carries on top of the shared
lineage (bun/src/js/node/_http_outgoing.ts: write_ msgSocket local,
_send header local, _storeHeader/processHeader length locals,
runChunkCallbacks, flat one-shot setHeader validation); the matcher
pre-filter is profiling-driven on top of that set.
Benchmarks
The stock benchmark/http serving benchmarks route through a load generator
(test-double/autocannon); on my machine their run-to-run CV is 8–25%, which
cannot resolve effects of this size — across ~60 captured configs they show
consistently positive means and zero statistically significant
regressions (headers, incoming_headers, chunked, end-vs-write-end,
client-request-body, create-clientrequest, check_*; Welch t-test on
benchmark/compare.js CSVs, 30–90 runs).
The claimed effect is from mechanism benchmarks that drive the shipped
classes through the public API (http.createServer + new ServerResponse(req) + setHeader/flushHeaders), one fresh process per
sample, 30 interleaved samples per binary, Welch t-test:
| case | improvement | p |
|---|---|---|
flushHeaders() with five headers, per message | +8.25% | 6.2e-11 |
24-header response (_storeHeader dominated) | +4.26% | 6.7e-7 |
tight setHeader loop (three headers × 2e6) | +2.64% | 2.0e-3 |
Sign-stable in an independent 15-sample repeat (+6.8% / +4.7% / +0.5%).
Raw CSVs and the analysis scripts are available on request.
Correctness
- No observable behavior change: header order/content, error types, and
event ordering are preserved; the validation-leniency inputs are fixed at
message construction, so caching the derived boolean is not observable
through supported usage. - All
test/parallel/test-http-*,test-http.js, andtest-https-*pass. - The new
test-http-outgoing-known-header-casing.jslocks the matcher
semantics: known fields keep being recognized in any casing
(CoNnEcTiOn,CONTENT-LENGTH), and unknown names sharing a known
field's length and first letter (Dote,Xonnection) are emitted
verbatim without affecting framing. The test also passes on the
unpatched binary.
AI assistance disclosure
This is an AI-assisted pull request. The survey of Bun's implementation, the
implementation, the benchmark methodology and analysis, the regression test,
and this description were produced by an AI agent (Grok, xAI) operating
under my direction; commits carry Assisted-by: Grok. I reviewed and take
responsibility for every change.