Skip to content

Commit 0b27c1f

Browse files
benfavreclaude
andcommitted
perf: skip reactive overhead during SSR and avoid V8 hidden class mutations
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 and the timers would leak in the Node.js process. 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 0b27c1f

File tree

3 files changed

+30
-23
lines changed

3 files changed

+30
-23
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: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -193,22 +193,27 @@ class LoadableSubscription {
193193
const { _res: res, _opts: opts } = this
194194

195195
if (res.loading) {
196-
if (typeof opts.delay === 'number') {
197-
if (opts.delay === 0) {
198-
this._state.pastDelay = true
199-
} else {
200-
this._delay = setTimeout(() => {
201-
this._update({
202-
pastDelay: true,
203-
})
204-
}, opts.delay)
196+
// Skip timer setup on the server — SSR renders once and never
197+
// re-renders, so delay/timeout UI states provide no value and the
198+
// timers would leak in the Node.js process.
199+
if (typeof window !== 'undefined') {
200+
if (typeof opts.delay === 'number') {
201+
if (opts.delay === 0) {
202+
this._state.pastDelay = true
203+
} else {
204+
this._delay = setTimeout(() => {
205+
this._update({
206+
pastDelay: true,
207+
})
208+
}, opts.delay)
209+
}
205210
}
206-
}
207211

208-
if (typeof opts.timeout === 'number') {
209-
this._timeout = setTimeout(() => {
210-
this._update({ timedOut: true })
211-
}, opts.timeout)
212+
if (typeof opts.timeout === 'number') {
213+
this._timeout = setTimeout(() => {
214+
this._update({ timedOut: true })
215+
}, opts.timeout)
216+
}
212217
}
213218
}
214219

0 commit comments

Comments
 (0)