Skip to content

perf(headers): O(1) case-insensitive lookup in HeadersAdapter#91559

Open
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/headers-adapter-fast-lookup
Open

perf(headers): O(1) case-insensitive lookup in HeadersAdapter#91559
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/headers-adapter-fast-lookup

Conversation

@benfavre
Copy link
Contributor

Summary

  • Replace O(n) Object.keys().find() with O(1) Map.get() in the HeadersAdapter Proxy traps (get, set, has, deleteProperty)
  • Build a Map<lowercase, originalKey> once in the constructor from initial headers
  • Keep the map in sync on set (adds entries) and deleteProperty (removes entries)
  • Handle out-of-band mutations on the underlying IncomingHttpHeaders object by falling back to a linear scan on cache miss and caching the result

Motivation

The HeadersAdapter is used on every SSR request to wrap Node.js IncomingHttpHeaders. Every call to .get(), .has(), .set(), or delete on the adapter triggers a Proxy trap that does:

Object.keys(headers).find(o => o.toLowerCase() === lowercased)

This is O(n) per access where n = number of headers. A typical HTTP request carries 10-20 headers, and during SSR rendering headers are accessed many times (cookies, content-type, accept, authorization, user-agent, x-forwarded-for, custom headers, etc.). The cumulative cost of repeated linear scans over every header key on every access adds up in high-throughput SSR scenarios.

This is the same class of optimization highlighted in the TanStack Start blog post about 5x SSR throughput — avoiding O(n) operations in hot paths is one of the most impactful patterns for SSR performance.

The fix pre-computes a Map<string, string> (lowercase -> original key) and uses Map.get() for O(1) lookups. The map is kept consistent through set/delete operations and handles the edge case where external code mutates the IncomingHttpHeaders object directly (tested in the existing test suite).

Test plan

  • All 15 existing HeadersAdapter unit tests pass, including the "mutated out of band" case that validates the fallback path
  • No new tests needed — this is a pure performance optimization with identical observable behavior

🤖 Generated with Claude Code

…sAdapter

The HeadersAdapter Proxy handler was calling
`Object.keys(headers).find(o => o.toLowerCase() === lowercased)` on every
`get`, `set`, `has`, and `deleteProperty` trap. This is O(n) per access
where n is the number of headers.

Headers are accessed many times during every SSR request (cookies,
content-type, accept, user-agent, authorization, custom headers, etc.),
making this a significant hot-path bottleneck. With a typical request
carrying 10-20 headers and each being accessed multiple times during
rendering, the cumulative cost of repeated linear scans is substantial.

This commit replaces the linear scan with a `Map<string, string>` that
maps lowercase keys to their original casing, turning every access into
an O(1) `Map.get()`. The map is:

- Built once in the constructor from the initial headers
- Kept in sync by `set` (adds new entries) and `deleteProperty` (removes)
- Handles out-of-band mutations on the underlying IncomingHttpHeaders
  object by falling back to a linear scan on cache miss and caching the
  result for subsequent accesses

Inspired by TanStack Start's findings on avoiding O(n) operations in SSR
hot paths as a key factor in achieving higher throughput.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: 5c3fce9

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

1 similar comment
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: 5c3fce9

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@benfavre
Copy link
Contributor Author

Test Verification

  • headers.test.ts: 15/15 passed (case-insensitive, sealed, out-of-band mutation)
  • All existing HeadersAdapter behavior preserved

All tests run on the perf/combined-all branch against canary. Total: 203 tests across 13 suites, all passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants