diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 56acc6127cfbe..2ab1d7f9c8fb3 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -33,7 +33,6 @@ import {
canSegmentBeOverridden,
matchSegment,
} from '../../client/components/match-segments'
-import { ServerInsertedHTMLContext } from '../../shared/lib/server-inserted-html'
import { stripInternalQueries } from '../internal-utils'
import {
NEXT_ROUTER_PREFETCH,
@@ -78,6 +77,7 @@ import { warn } from '../../build/output/log'
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
import { ComponentsType } from '../../build/webpack/loaders/next-app-loader'
import { ModuleReference } from '../../build/webpack/loaders/metadata/types'
+import { createServerInsertedHTML } from './server-inserted-html'
export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
@@ -1448,29 +1448,11 @@ export async function renderToHTMLOrFlight(
const { HeadManagerContext } =
require('../../shared/lib/head-manager-context') as typeof import('../../shared/lib/head-manager-context')
- const serverInsertedHTMLCallbacks: Set<() => React.ReactNode> = new Set()
- function InsertedHTML({ children }: { children: JSX.Element }) {
- // Reset addInsertedHtmlCallback on each render
- const addInsertedHtml = React.useCallback(
- (handler: () => React.ReactNode) => {
- serverInsertedHTMLCallbacks.add(handler)
- },
- []
- )
- return (
-
-
- {children}
-
-
- )
- }
+ // On each render, create a new `ServerInsertedHTML` context to capture
+ // injected nodes from user code (`useServerInsertedHTML`).
+ const { ServerInsertedHTMLProvider, renderServerInsertedHTML } =
+ createServerInsertedHTML()
getTracer().getRootSpanAttributes()?.set('next.route', pagePath)
const bodyResult = getTracer().wrap(
@@ -1506,9 +1488,16 @@ export async function renderToHTMLOrFlight(
}))
const content = (
-
-
-
+
+
+
+
+
)
let polyfillsFlushed = false
@@ -1553,13 +1542,6 @@ export async function renderToHTMLOrFlight(
ReactDOMServer: require('react-dom/server.edge'),
element: (
<>
- {Array.from(serverInsertedHTMLCallbacks).map(
- (callback, index) => (
-
- {callback()}
-
- )
- )}
{polyfillsFlushed
? null
: polyfills?.map((polyfill) => {
@@ -1573,6 +1555,7 @@ export async function renderToHTMLOrFlight(
/>
)
})}
+ {renderServerInsertedHTML()}
{errorMetaTags}
>
),
diff --git a/packages/next/src/server/app-render/server-inserted-html.tsx b/packages/next/src/server/app-render/server-inserted-html.tsx
new file mode 100644
index 0000000000000..f044c24feaba3
--- /dev/null
+++ b/packages/next/src/server/app-render/server-inserted-html.tsx
@@ -0,0 +1,29 @@
+// Provider for the `useServerInsertedHTML` API to register callbacks to insert
+// elements into the HTML stream.
+
+import React from 'react'
+import { ServerInsertedHTMLContext } from '../../shared/lib/server-inserted-html'
+
+export function createServerInsertedHTML() {
+ const serverInsertedHTMLCallbacks: (() => React.ReactNode)[] = []
+ const addInsertedHtml = (handler: () => React.ReactNode) => {
+ serverInsertedHTMLCallbacks.push(handler)
+ }
+
+ return {
+ ServerInsertedHTMLProvider({ children }: { children: JSX.Element }) {
+ return (
+
+ {children}
+
+ )
+ },
+ renderServerInsertedHTML() {
+ return serverInsertedHTMLCallbacks.map((callback, index) => (
+
+ {callback()}
+
+ ))
+ },
+ }
+}
diff --git a/packages/next/src/server/stream-utils/node-web-streams-helper.ts b/packages/next/src/server/stream-utils/node-web-streams-helper.ts
index 6b230596a4d9a..d6e2bd41be64d 100644
--- a/packages/next/src/server/stream-utils/node-web-streams-helper.ts
+++ b/packages/next/src/server/stream-utils/node-web-streams-helper.ts
@@ -58,17 +58,14 @@ export function readableStreamTee(
const writer2 = transformStream2.writable.getWriter()
const reader = readable.getReader()
- function read() {
- reader.read().then(({ done, value }) => {
- if (done) {
- writer.close()
- writer2.close()
- return
- }
- writer.write(value)
- writer2.write(value)
- read()
- })
+ async function read() {
+ const { done, value } = await reader.read()
+ if (done) {
+ await Promise.all([writer.close(), writer2.close()])
+ return
+ }
+ await Promise.all([writer.write(value), writer2.write(value)])
+ await read()
}
read()
@@ -91,15 +88,14 @@ export function chainStreams(
}
export function streamFromArray(strings: string[]): ReadableStream {
- // Note: we use a TransformStream here instead of instantiating a ReadableStream
- // because the built-in ReadableStream polyfill runs strings through TextEncoder.
- const { readable, writable } = new TransformStream()
-
- const writer = writable.getWriter()
- strings.forEach((str) => writer.write(encodeText(str)))
- writer.close()
-
- return readable
+ return new ReadableStream({
+ start(controller) {
+ for (const str of strings) {
+ controller.enqueue(encodeText(str))
+ }
+ controller.close()
+ },
+ })
}
export async function streamToString(
@@ -121,19 +117,19 @@ export async function streamToString(
}
}
-export function createBufferedTransformStream(
- transform: (v: string) => string | Promise = (v) => v
-): TransformStream {
- let bufferedString = ''
+export function createBufferedTransformStream(): TransformStream<
+ Uint8Array,
+ Uint8Array
+> {
+ let bufferedBytes: Uint8Array = new Uint8Array()
let pendingFlush: Promise | null = null
const flushBuffer = (controller: TransformStreamDefaultController) => {
if (!pendingFlush) {
pendingFlush = new Promise((resolve) => {
setTimeout(async () => {
- const buffered = await transform(bufferedString)
- controller.enqueue(encodeText(buffered))
- bufferedString = ''
+ controller.enqueue(bufferedBytes)
+ bufferedBytes = new Uint8Array()
pendingFlush = null
resolve()
}, 0)
@@ -142,11 +138,14 @@ export function createBufferedTransformStream(
return pendingFlush
}
- const textDecoder = new TextDecoder()
-
return new TransformStream({
transform(chunk, controller) {
- bufferedString += decodeText(chunk, textDecoder)
+ const newBufferedBytes = new Uint8Array(
+ bufferedBytes.length + chunk.byteLength
+ )
+ newBufferedBytes.set(bufferedBytes)
+ newBufferedBytes.set(chunk, bufferedBytes.length)
+ bufferedBytes = newBufferedBytes
flushBuffer(controller)
},
@@ -164,7 +163,6 @@ export function createInsertedHTMLStream(
return new TransformStream({
async transform(chunk, controller) {
const insertedHTMLChunk = encodeText(await getServerInsertedHTML())
-
controller.enqueue(insertedHTMLChunk)
controller.enqueue(chunk)
},
@@ -237,7 +235,7 @@ function createHeadInsertionTransformStream(
// Suffix after main body content - scripts before