From 9bcb7aaf5c0aba3ea7855cd14814fe2b15f89f05 Mon Sep 17 00:00:00 2001 From: Benjamin Favre Date: Wed, 18 Mar 2026 09:38:05 +0000 Subject: [PATCH 1/2] perf: skip unnecessary reactive setup during SSR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/next/src/server/base-server.ts | 15 +++++++-------- packages/next/src/server/request-meta.ts | 5 ++++- .../src/shared/lib/loadable.shared-runtime.tsx | 7 +++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 66aecaf5fcba99..8f624e4bffb303 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1541,14 +1541,11 @@ export default abstract class Server< } parsedUrl.pathname = normalizeResult.pathname - for (const key of Object.keys(parsedUrl.query)) { - delete parsedUrl.query[key] - } + // Replace the query object instead of loop-deleting every key. + // Using `delete` on object properties mutates V8 hidden classes, + // which de-optimises inline caches on downstream property accesses. const invokeQuery = getRequestMeta(req, 'invokeQuery') - - if (invokeQuery) { - Object.assign(parsedUrl.query, invokeQuery) - } + parsedUrl.query = invokeQuery ? { ...invokeQuery } : {} finished = await this.normalizeAndAttachMetadata(req, res, parsedUrl) if (finished) return @@ -2018,7 +2015,9 @@ export default abstract class Server< if (!addedNextUrlToVary) { // Remove `Next-URL` from the request headers we determined it wasn't necessary to include in the Vary header. // This is to avoid any dependency on the `Next-URL` header being present when preparing the response. - delete req.headers[NEXT_URL] + // Use undefined assignment instead of delete to preserve V8 hidden class. + // Downstream code checks this header by value, not with the `in` operator. + req.headers[NEXT_URL] = undefined } } diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index 5bd595639e04d7..888b36acbabe93 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -372,7 +372,10 @@ export function removeRequestMeta( key: K ) { const meta = getRequestMeta(request) - delete meta[key] + // Set to undefined instead of delete to preserve V8 hidden class. + // All consumers use getRequestMeta(req, key) which returns meta[key], + // so undefined is semantically equivalent to a missing key. + meta[key] = undefined as RequestMeta[K] return setRequestMeta(request, meta) } diff --git a/packages/next/src/shared/lib/loadable.shared-runtime.tsx b/packages/next/src/shared/lib/loadable.shared-runtime.tsx index d82563ed97fcf6..c6152e1713c774 100644 --- a/packages/next/src/shared/lib/loadable.shared-runtime.tsx +++ b/packages/next/src/shared/lib/loadable.shared-runtime.tsx @@ -196,7 +196,10 @@ class LoadableSubscription { if (typeof opts.delay === 'number') { if (opts.delay === 0) { this._state.pastDelay = true - } else { + } else if (typeof window !== 'undefined') { + // Skip timers during SSR — the render is synchronous, + // there are no subscribers to notify, and the timers + // would leak in the Node.js process. this._delay = setTimeout(() => { this._update({ pastDelay: true, @@ -205,7 +208,7 @@ class LoadableSubscription { } } - if (typeof opts.timeout === 'number') { + if (typeof opts.timeout === 'number' && typeof window !== 'undefined') { this._timeout = setTimeout(() => { this._update({ timedOut: true }) }, opts.timeout) From 34ed23a220131e7d26590270a37fde1aae34b927 Mon Sep 17 00:00:00 2001 From: Benjamin Favre Date: Wed, 18 Mar 2026 09:46:07 +0000 Subject: [PATCH 2/2] fix: remove overlapping changes handled by PR #91560 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delete→undefined changes for NEXT_URL header and request-meta are already in PR #91560. Keep only the changes unique to this PR: - parsedUrl.query fresh object replacement (avoids N delete operations) - loadable timer guards during SSR --- packages/next/src/server/base-server.ts | 4 +--- packages/next/src/server/request-meta.ts | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 8f624e4bffb303..8a1fda1892ca86 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2015,9 +2015,7 @@ export default abstract class Server< if (!addedNextUrlToVary) { // Remove `Next-URL` from the request headers we determined it wasn't necessary to include in the Vary header. // This is to avoid any dependency on the `Next-URL` header being present when preparing the response. - // Use undefined assignment instead of delete to preserve V8 hidden class. - // Downstream code checks this header by value, not with the `in` operator. - req.headers[NEXT_URL] = undefined + delete req.headers[NEXT_URL] } } diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index 888b36acbabe93..5bd595639e04d7 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -372,10 +372,7 @@ export function removeRequestMeta( key: K ) { const meta = getRequestMeta(request) - // Set to undefined instead of delete to preserve V8 hidden class. - // All consumers use getRequestMeta(req, key) which returns meta[key], - // so undefined is semantically equivalent to a missing key. - meta[key] = undefined as RequestMeta[K] + delete meta[key] return setRequestMeta(request, meta) }