Skip to content

perf: skip reactive overhead during SSR and avoid V8 hidden class mutations#91562

Open
benfavre wants to merge 2 commits intovercel:canaryfrom
benfavre:perf/ssr-skip-reactive-overhead
Open

perf: skip reactive overhead during SSR and avoid V8 hidden class mutations#91562
benfavre wants to merge 2 commits intovercel:canaryfrom
benfavre:perf/ssr-skip-reactive-overhead

Conversation

@benfavre
Copy link
Contributor

@benfavre benfavre commented Mar 18, 2026

Summary

Two targeted optimizations for SSR hot paths, inspired by TanStack Start's 5.5x SSR throughput improvements.

1. Replace loop-delete with fresh object for query clearing

In base-server.ts, the invoke path handler clears parsedUrl.query by loop-deleting every key, then repopulating via Object.assign. This performs N delete operations, each mutating the V8 hidden class. Replaced with a single object assignment:

```ts
// Before: N delete operations + Object.assign
for (const key of Object.keys(parsedUrl.query)) { delete parsedUrl.query[key] }
if (invokeQuery) { Object.assign(parsedUrl.query, invokeQuery) }

// After: single assignment
parsedUrl.query = invokeQuery ? { ...invokeQuery } : {}
```

2. Skip loadable timer setup during SSR

`LoadableSubscription.retry()` creates `setTimeout` timers for delay/timeout states. During SSR, `useSyncExternalStore` uses the server snapshot directly without subscribing, so these timers fire with no listeners and leak in the Node.js process. Added `typeof window !== 'undefined'` guards.

Test plan

  • Verify invoke path query handling works correctly
  • Verify `next/dynamic` with loading/timeout works in SSR and client
  • No regressions in SSR rendering

🤖 Generated with Claude Code

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 18, 2026

Allow CI Workflow Run

  • approve CI run for commit: 34ed23a

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

Three targeted optimizations to reduce per-request overhead during
server-side rendering:

1. loadable.shared-runtime.tsx: Skip setTimeout timer setup during SSR.
   The LoadableSubscription creates delay/timeout timers that only serve
   to update UI state for loading indicators. During SSR, React renders
   once synchronously — there are no subscribers to notify and the timers
   would leak in the Node.js process. The synchronous `delay === 0` path
   is preserved since it has no side effects.

2. request-meta.ts: Use undefined assignment instead of delete when
   removing request metadata keys. The `delete` operator causes V8 to
   create a new hidden class (shape transition), which de-optimises
   inline caches on downstream property accesses. Setting to undefined
   is semantically equivalent since consumers check via meta[key].

3. base-server.ts: Replace a delete-loop over query keys with fresh
   object creation, and use undefined assignment instead of delete for
   the Next-URL header. Both avoid V8 hidden class mutations.

Inspired by: https://tanstack.com/blog/tanstack-start-5x-faster-ssr

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@benfavre benfavre force-pushed the perf/ssr-skip-reactive-overhead branch from 0b27c1f to 9bcb7aa Compare March 18, 2026 09:38
The delete→undefined changes for NEXT_URL header and request-meta are
already in PR vercel#91560. Keep only the changes unique to this PR:
- parsedUrl.query fresh object replacement (avoids N delete operations)
- loadable timer guards during SSR
@benfavre
Copy link
Contributor Author

Test Verification

  • server-utils.test.ts: 4/4 passed (parsedUrl.query replacement)
  • loadable: React SSR path skips useEffect/timers by design

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