Skip to content

perf: cache runtime checks and IncrementalCache handler for static serving#91604

Open
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/optimize-static-serving-path
Open

perf: cache runtime checks and IncrementalCache handler for static serving#91604
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/optimize-static-serving-path

Conversation

@benfavre
Copy link
Contributor

Summary

Three targeted optimizations for the static page serving hot path, reducing per-request overhead from repeated environment variable lookups and unnecessary dynamic imports.

  • Cache process.env.NEXT_RUNTIME in helpers.ts -- isNodeNextRequest/isNodeNextResponse/isWebNextRequest/isWebNextResponse checked process.env.NEXT_RUNTIME on every call (5+ times per request). process.env access in Node.js involves a property lookup on a special object that is not free. Now cached at module level since the runtime never changes after process start. Follows existing precedent in use-cache-wrapper.ts which already does const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'.

  • Cache CacheHandler class resolution in next-server.ts -- getIncrementalCache() resolved the CacheHandler class via dynamicImportEsmDefault() and called loadCustomCacheHandlers() on every request, even though the resolved class never changes. Now cached as a promise (also preventing duplicate imports from concurrent requests during startup). The IncrementalCache instance itself is still created per-request since it derives request-specific state from headers (isOnDemandRevalidate, revalidatedTags).

  • Cache env lookups in IncrementalCache constructor -- NEXT_PRIVATE_TEST_PROXY and __NEXT_TEST_MAX_ISR_CACHE were read from process.env in the constructor on every request. Now cached as static class properties, following the existing pattern used by the debug property.

Files changed

  • packages/next/src/server/base-http/helpers.ts
  • packages/next/src/server/next-server.ts
  • packages/next/src/server/lib/incremental-cache/index.ts

Test plan

  • Verify existing tests pass (no behavioral changes, only caching of already-constant values)
  • isNodeNextRequest/isNodeNextResponse still return correct values in Node runtime
  • isWebNextRequest/isWebNextResponse still return correct values in Edge runtime
  • Custom cacheHandler config still resolves correctly on first request
  • loadCustomCacheHandlers still called (via the cached promise path)
  • IncrementalCache still respects __NEXT_TEST_MAX_ISR_CACHE and NEXT_PRIVATE_TEST_PROXY when set before module load

🤖 Generated with Claude Code

…r static serving path

Three optimizations targeting the static page serving hot path:

1. **Cache `process.env.NEXT_RUNTIME` in helpers.ts**: The `isNodeNextRequest`,
   `isNodeNextResponse`, `isWebNextRequest`, and `isWebNextResponse` guards
   checked `process.env.NEXT_RUNTIME` on every call. `process.env` access in
   Node.js is not free (property lookup on a special object). These are called
   5+ times per request. Now cached at module level since the runtime never
   changes after process start.

2. **Cache CacheHandler resolution in next-server.ts**: `getIncrementalCache()`
   previously resolved the CacheHandler class via dynamic import and called
   `loadCustomCacheHandlers()` on every request. The resolved class never
   changes, so we cache the resolution promise (also preventing duplicate
   imports from concurrent requests). The IncrementalCache instance itself is
   still created per-request since it derives request-specific state from
   headers (isOnDemandRevalidate, revalidatedTags).

3. **Cache env lookups in IncrementalCache**: `NEXT_PRIVATE_TEST_PROXY` and
   `__NEXT_TEST_MAX_ISR_CACHE` were read from `process.env` in the constructor
   on every request. Now cached as static class properties since these values
   never change at runtime.

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

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 15s on /rsc (pre-rendered static page, 4.8KB).

Context: Serving a pre-rendered static page on Next.js takes 5.8ms vs 0.02ms on bare HTTP (49,533 vs 7,648 req/s). This PR targets three sources of per-request overhead in the static serving path.

Optimization 1: Cache process.env.NEXT_RUNTIME at module level

  • Before: isNodeNextResponse / isNodeNextRequest read process.env.NEXT_RUNTIME on every call (5+ times/req)
  • process.env access in Node.js is not a simple property read — it goes through a special C++ accessor that does string conversion
  • Self-time: 196ms across all calls in the 15s profile
  • After: single boolean read from a module-level constant

Optimization 2: Cache CacheHandler resolution

  • Before: getIncrementalCache() calls dynamicImportEsmDefault() and loadCustomCacheHandlers() per request
  • The dynamic import resolves to the same handler every time
  • Self-time contribution: part of IncrementalCache's 226ms inclusive time
  • After: promise-based cache — first request resolves the handler, subsequent requests reuse the cached result

Optimization 3: Cache process.env reads in IncrementalCache constructor

  • Before: process.env.NEXT_PRIVATE_TEST_PROXY and process.env.__NEXT_TEST_MAX_ISR_CACHE read per constructor call
  • After: cached as private static readonly class properties (follows existing pattern used by debug property)

Test Verification

  • 183 tests across 12 suites, all passing
  • IncrementalCache instance still created per-request (conservative: requestHeaders affects isOnDemandRevalidate and revalidatedTags in constructor)

@benfavre
Copy link
Contributor Author

Test Verification

  • 183 tests across 12 suites, all passing
  • tracer.test.ts: 1/1 passed (withPropagatedContext force mode preserved)
  • IncrementalCache constructor test: covered by integration tests (per-request creation preserved for correctness)

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