Repository: vercel/next.js
Pull request: Ensure getServerInsertedHTML skips rendering correctly

### Problem

Both `polyfillTags` and `traceMetaTags` are only rendered if they haven't already been flushed, however the current check to skip rendering (lines 81-89) doesn't take this into account – only the render logic does (lines 93-98).

This results in `renderToReadableStream` being called (potentially thousands of times – [for each chunk](https://github.com/vercel/next.js/blob/498349c375e2602f526f64e8366992066cfa872c/packages/next/src/server/stream-utils/node-web-streams-helper.ts#L368) in the [head insertion transform stream in the transformer chain](https://github.com/vercel/next.js/blob/498349c375e2602f526f64e8366992066cfa872c/packages/next/src/server/stream-utils/node-web-streams-helper.ts#L834)), constructing new `ReadableStream`s each time, just to render empty strings.

It's present in canary/v16 and I suspect it was introduced in v15 (so can be backported). The logic check for v14 looks correct.

### Solution

This PR ensures the shortcut check lines up with the render logic by just zeroing tag arrays after they've been used. There are a number of ways to achieve the same result – including just adding the `hasFlushedInitially` check to the shortcut logic – however this would result in the logic still being split in two places, so I've chosen one way to ensure the logic is centralized. Happy with whatever method is preferred here.

### Results

This results in a 10-38% perf improvement end-to-end, especially for large renders. Eg, in [theo's benchmark](https://github.com/t3dotgg/cf-vs-vercel-bench/tree/main/next-bench) it brings the number of `renderToReadableStream` calls from 1745 here down to just 1, which has a noticeable impact on end-to-end performance

I've [created a repo to highlight/reproduce the problem](https://github.com/mhart/next-server-inserted-fix) based off that benchmark. The results from there:

- Node.js: 1.18x faster
- Deno: 1.38x faster
- workerd: 1.24x faster
- Bun: 1.15x faster




Changed files:
filename: packages/next/src/server/app-render/make-get-server-inserted-html.tsx
patch: @@ -26,12 +26,14 @@ export function makeGetServerInsertedHTML({
   basePath: string
 }) {
   let flushedErrorMetaTagsUntilIndex = 0
-  // flag for static content that only needs to be flushed once
-  let hasFlushedInitially = false
 
-  const polyfillTags = polyfills.map((polyfill) => {
+  // These only need to be rendered once, they'll be set to empty arrays once flushed.
+  let polyfillTags = polyfills.map((polyfill) => {
     return <script key={polyfill.src} {...polyfill} />
   })
+  let traceMetaTags = (tracingMetadata || []).map(({ key, value }, index) => (
+    <meta key={`next-trace-data-${index}`} name={key} content={value} />
+  ))
 
   return async function getServerInsertedHTML() {
     // Loop through all the errors that have been captured but not yet
@@ -69,12 +71,6 @@ export function makeGetServerInsertedHTML({
       }
     }
 
-    const traceMetaTags = (tracingMetadata || []).map(
-      ({ key, value }, index) => (
-        <meta key={`next-trace-data-${index}`} name={key} content={value} />
-      )
-    )
-
     const serverInsertedHTML = renderServerInsertedHTML()
 
     // Skip React rendering if we know the content is empty.
@@ -90,12 +86,9 @@ export function makeGetServerInsertedHTML({
 
     const stream = await renderToReadableStream(
       <>
-        {
-          /* Insert the polyfills if they haven't been flushed yet. */
-          hasFlushedInitially ? null : polyfillTags
-        }
+        {polyfillTags}
         {serverInsertedHTML}
-        {hasFlushedInitially ? null : traceMetaTags}
+        {traceMetaTags}
         {errorMetaTags}
       </>,
       {
@@ -105,7 +98,9 @@ export function makeGetServerInsertedHTML({
       }
     )
 
-    hasFlushedInitially = true
+    // The polyfills and trace metadata have been flushed, so they don't need to be rendered again
+    polyfillTags = []
+    traceMetaTags = []
 
     // There's no need to wait for the stream to be ready
     // e.g. calling `await stream.allReady` because `streamToString` will

filename: test/e2e/app-dir/cache-components-errors/cache-components-errors.test.ts
patch: @@ -3251,13 +3251,13 @@ describe('Cache Components Errors', () => {
                      at ScrollAndFocusHandler (bundler:///<next-src>)
                      at RenderFromTemplateContext (bundler:///<next-src>)
                      at OuterLayoutRouter (bundler:///<next-src>)
-                   339 |  */
-                   340 | function InnerLayoutRouter({
-                 > 341 |   tree,
+                   332 |  */
+                   333 | function InnerLayoutRouter({
+                 > 334 |   tree,
                        |   ^
-                   342 |   segmentPath,
-                   343 |   debugNameContext,
-                   344 |   cacheNode,
+                   335 |   segmentPath,
+                   336 |   debugNameContext,
+                   337 |   cacheNode,
                  To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "/use-cache-private-without-suspense" in your browser to investigate the error.
                  Error occurred prerendering page "/use-cache-private-without-suspense". Read more: https://nextjs.org/docs/messages/prerender-error