perf: skip reactive overhead during SSR and avoid V8 hidden class mutations#91562
Open
benfavre wants to merge 2 commits intovercel:canaryfrom
Open
perf: skip reactive overhead during SSR and avoid V8 hidden class mutations#91562benfavre wants to merge 2 commits intovercel:canaryfrom
benfavre wants to merge 2 commits intovercel:canaryfrom
Conversation
Collaborator
|
Allow CI Workflow Run
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>
0b27c1f to
9bcb7aa
Compare
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
Contributor
Author
Test Verification
All tests run on the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 clearsparsedUrl.queryby loop-deleting every key, then repopulating viaObject.assign. This performs Ndeleteoperations, 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
🤖 Generated with Claude Code