Skip to content

Commit 9bcb7aa

Browse files
benfavreclaude
andcommitted
perf: skip unnecessary reactive setup during SSR
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>
1 parent 196ed2b commit 9bcb7aa

File tree

3 files changed

+16
-11
lines changed

3 files changed

+16
-11
lines changed

packages/next/src/server/base-server.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,14 +1541,11 @@ export default abstract class Server<
15411541
}
15421542
parsedUrl.pathname = normalizeResult.pathname
15431543

1544-
for (const key of Object.keys(parsedUrl.query)) {
1545-
delete parsedUrl.query[key]
1546-
}
1544+
// Replace the query object instead of loop-deleting every key.
1545+
// Using `delete` on object properties mutates V8 hidden classes,
1546+
// which de-optimises inline caches on downstream property accesses.
15471547
const invokeQuery = getRequestMeta(req, 'invokeQuery')
1548-
1549-
if (invokeQuery) {
1550-
Object.assign(parsedUrl.query, invokeQuery)
1551-
}
1548+
parsedUrl.query = invokeQuery ? { ...invokeQuery } : {}
15521549

15531550
finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl)
15541551
if (finished) return
@@ -2018,7 +2015,9 @@ export default abstract class Server<
20182015
if (!addedNextUrlToVary) {
20192016
// Remove `Next-URL` from the request headers we determined it wasn't necessary to include in the Vary header.
20202017
// This is to avoid any dependency on the `Next-URL` header being present when preparing the response.
2021-
delete req.headers[NEXT_URL]
2018+
// Use undefined assignment instead of delete to preserve V8 hidden class.
2019+
// Downstream code checks this header by value, not with the `in` operator.
2020+
req.headers[NEXT_URL] = undefined
20222021
}
20232022
}
20242023

packages/next/src/server/request-meta.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ export function removeRequestMeta<K extends keyof RequestMeta>(
372372
key: K
373373
) {
374374
const meta = getRequestMeta(request)
375-
delete meta[key]
375+
// Set to undefined instead of delete to preserve V8 hidden class.
376+
// All consumers use getRequestMeta(req, key) which returns meta[key],
377+
// so undefined is semantically equivalent to a missing key.
378+
meta[key] = undefined as RequestMeta[K]
376379
return setRequestMeta(request, meta)
377380
}
378381

packages/next/src/shared/lib/loadable.shared-runtime.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ class LoadableSubscription {
196196
if (typeof opts.delay === 'number') {
197197
if (opts.delay === 0) {
198198
this._state.pastDelay = true
199-
} else {
199+
} else if (typeof window !== 'undefined') {
200+
// Skip timers during SSR — the render is synchronous,
201+
// there are no subscribers to notify, and the timers
202+
// would leak in the Node.js process.
200203
this._delay = setTimeout(() => {
201204
this._update({
202205
pastDelay: true,
@@ -205,7 +208,7 @@ class LoadableSubscription {
205208
}
206209
}
207210

208-
if (typeof opts.timeout === 'number') {
211+
if (typeof opts.timeout === 'number' && typeof window !== 'undefined') {
209212
this._timeout = setTimeout(() => {
210213
this._update({ timedOut: true })
211214
}, opts.timeout)

0 commit comments

Comments
 (0)