Skip to content

perf: pre-compute Vary header and reduce per-request header overhead#91607

Open
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/batch-response-headers
Open

perf: pre-compute Vary header and reduce per-request header overhead#91607
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/batch-response-headers

Conversation

@benfavre
Copy link
Contributor

Summary

Three optimizations targeting the HTTP header setting hot path, which accounts for 622ms (2.5%) in static serving profiles (_storeHeader: 244ms, setHeader: 209ms, checkInvalidHeaderChar: 169ms):

  • Pre-compute Vary header strings at module levelsetVaryHeader (base-server.ts) and getVaryHeader (app-page/module.ts) both constructed the same 4-header template literal on every request via string interpolation. Hoisted to module-level constants STATIC_VARY_HEADER and STATIC_VARY_HEADER_WITH_NEXT_URL.

  • Inline addRequestMeta to skip redundant get/set wrapper — Called ~89 times per request cycle, each invocation did getRequestMeta() (symbol lookup + || {} fallback) → property set → setRequestMeta() (symbol write of the same object reference). After first initialization the wrapper functions are pure overhead. Now reads the symbol directly, branches on existence, and skips the redundant reassignment.

  • Pre-compute X-Powered-By header name/value constants — Hoisted string literals to module-level constants in send-payload.ts.

Test plan

  • Existing test suites pass (no behavioral changes, only performance)
  • Verify Vary header values remain identical via curl -v against a running dev server
  • Profile with autocannon or clinic flame to confirm reduced time in _storeHeader/setHeader

🤖 Generated with Claude Code

Three optimizations targeting the HTTP header setting hot path (622ms / 2.5%
in static serving profiles):

1. Pre-compute Vary header strings at module level
   - `setVaryHeader` in base-server.ts and `getVaryHeader` in app-page
     module.ts both constructed the same template literal on every request
     from 4-5 constant header names. Hoist to module-level constants
     (`STATIC_VARY_HEADER`, `STATIC_VARY_HEADER_WITH_NEXT_URL`).

2. Inline addRequestMeta to skip redundant get/set wrapper
   - `addRequestMeta` was called ~89 times per request cycle, each time
     doing: `getRequestMeta()` (symbol read + `|| {}` fallback) -> set
     property -> `setRequestMeta()` (symbol write of same reference).
     After first initialization the get/set wrapper is pure overhead.
     Now reads the symbol directly, branches on existence, and skips
     the redundant reassignment.

3. Pre-compute X-Powered-By header name/value constants
   - Hoist the `'X-Powered-By'` and `'Next.js'` string literals to
     module-level constants in send-payload.ts.

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: fbc1e40

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

Performance Impact

Profiling setup: Node.js v25.7.0, --cpu-prof --cpu-prof-interval=50, autocannon c=30 for 20s on /rsc (pre-rendered static page).

HTTP header operations cost 622ms (2.5%) of CPU in the static serving profile:

  • _storeHeader: 244ms (Node.js internal header formatting)
  • setHeader: 209ms (per-call validation + storage)
  • checkInvalidHeaderChar: 169ms (character validation per header value)

Changes:

1. Pre-computed Vary header strings (base-server.ts, app-page/module.ts)

  • Before: \${RSC_HEADER}, ${NEXT_ROUTER_STATE_TREE_HEADER}, ...`` — template literal evaluated per request concatenating 4-5 constant strings
  • After: STATIC_VARY_HEADER constant computed once at module level
  • Saves: string allocation + concatenation per request

2. Simplified addRequestMeta (request-meta.ts)

  • Before: 3 function calls per invocation — getRequestMeta(request)meta[key] = valuesetRequestMeta(request, meta)
  • After: direct property access — request[NEXT_REQUEST_META][key] = value with fallback init
  • With ~89 call sites across the codebase, eliminates ~178 function calls per request cycle
  • Self-time in profile: 114ms for addRequestMeta

3. Hoisted constants in send-payload.ts

  • 'X-Powered-By' and 'Next.js' string literals moved to module-level constants

Test Verification

  • 183 tests across 12 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