Skip to content

Commit bb68419

Browse files
committed
Simplify the html insertion html stream handling for ppr
1 parent 461f0be commit bb68419

File tree

6 files changed

+123
-17
lines changed

6 files changed

+123
-17
lines changed

Diff for: packages/next/src/server/stream-utils/node-web-streams-helper.ts

+22-17
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ function createHeadInsertionTransformStream(
196196
insert: () => Promise<string>
197197
): TransformStream<Uint8Array, Uint8Array> {
198198
let inserted = false
199-
let freezing = false
200199

201200
// We need to track if this transform saw any bytes because if it didn't
202201
// we won't want to insert any server HTML at all
@@ -205,32 +204,35 @@ function createHeadInsertionTransformStream(
205204
return new TransformStream({
206205
async transform(chunk, controller) {
207206
hasBytes = true
208-
// While react is flushing chunks, we don't apply insertions
209-
if (freezing) {
210-
controller.enqueue(chunk)
211-
return
212-
}
213207

214208
const insertion = await insert()
215-
216209
if (inserted) {
217210
if (insertion) {
218211
const encodedInsertion = encoder.encode(insertion)
219212
controller.enqueue(encodedInsertion)
220213
}
221214
controller.enqueue(chunk)
222-
freezing = true
223215
} else {
224216
// TODO (@Ethan-Arrowood): Replace the generic `indexOfUint8Array` method with something finely tuned for the subset of things actually being checked for.
225217
const index = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.HEAD)
218+
// In fully static rendering or non PPR rendering cases:
219+
// `/head>` will always be found in the chunk in first chunk rendering.
226220
if (index !== -1) {
227221
if (insertion) {
228222
const encodedInsertion = encoder.encode(insertion)
223+
// Get the total count of the bytes in the chunk and the insertion
224+
// e.g.
225+
// chunk = <head><meta charset="utf-8"></head>
226+
// insertion = <script>...</script>
227+
// output = <head><meta charset="utf-8"> [ <script>...</script> ] </head>
229228
const insertedHeadContent = new Uint8Array(
230229
chunk.length + encodedInsertion.length
231230
)
231+
// Append the first part of the chunk, before the head tag
232232
insertedHeadContent.set(chunk.slice(0, index))
233+
// Append the server inserted content
233234
insertedHeadContent.set(encodedInsertion, index)
235+
// Append the rest of the chunk
234236
insertedHeadContent.set(
235237
chunk.slice(index),
236238
index + encodedInsertion.length
@@ -239,18 +241,21 @@ function createHeadInsertionTransformStream(
239241
} else {
240242
controller.enqueue(chunk)
241243
}
242-
freezing = true
244+
inserted = true
245+
} else {
246+
// This will happens in PPR rendering during next start, when the page is partially rendered.
247+
// When the page resumes, the head tag will be found in the middle of the chunk.
248+
// Where we just need to append the insertion and chunk to the current stream.
249+
// e.g.
250+
// PPR-static: <head>...</head><body> [ resume content ] </body>
251+
// PPR-resume: [ insertion ] [ rest content ]
252+
if (insertion) {
253+
controller.enqueue(encoder.encode(insertion))
254+
}
255+
controller.enqueue(chunk)
243256
inserted = true
244257
}
245258
}
246-
247-
if (!inserted) {
248-
controller.enqueue(chunk)
249-
} else {
250-
scheduleImmediate(() => {
251-
freezing = false
252-
})
253-
}
254259
},
255260
async flush(controller) {
256261
// Check before closing if there's anything remaining to insert.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
export default async function Root({
4+
children,
5+
}: {
6+
children: React.ReactNode
7+
}) {
8+
return (
9+
<html>
10+
<body>{children}</body>
11+
</html>
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useRef } from 'react'
4+
import { useServerInsertedHTML } from 'next/navigation'
5+
6+
export function InsertHtml({ id, data }: { id: string; data: string }) {
7+
const insertRef = useRef(false)
8+
useServerInsertedHTML(() => {
9+
// only insert the style tag once
10+
if (insertRef.current) {
11+
return
12+
}
13+
insertRef.current = true
14+
const value = (
15+
<style
16+
data-test-id={id}
17+
>{`[data-inserted-${data}] { content: ${data} }`}</style>
18+
)
19+
console.log(`testing-log-insertion:${data}`)
20+
return value
21+
})
22+
23+
return <div>Loaded: {data}</div>
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { Suspense } from 'react'
2+
import { headers } from 'next/headers'
3+
import { InsertHtml } from './client'
4+
5+
async function Dynamic() {
6+
await headers()
7+
8+
return (
9+
<div>
10+
<h3>dynamic</h3>
11+
<InsertHtml id={'inserted-html'} data={'dynamic-data'} />
12+
</div>
13+
)
14+
}
15+
16+
export default function Page() {
17+
return (
18+
<Suspense fallback={<div>Loading...</div>}>
19+
<Dynamic />
20+
</Suspense>
21+
)
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
experimental: {
6+
ppr: true,
7+
},
8+
}
9+
10+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('ppr-use-server-inserted-html', () => {
4+
const { next, isNextStart } = nextTestSetup({
5+
files: __dirname,
6+
})
7+
8+
if (isNextStart) {
9+
it('should mark the route as ppr rendered', async () => {
10+
const prerenderManifest = JSON.parse(
11+
await next.readFile('.next/prerender-manifest.json')
12+
)
13+
expect(prerenderManifest.routes['/partial-resume'].renderingMode).toBe(
14+
'PARTIALLY_STATIC'
15+
)
16+
})
17+
}
18+
19+
it('should not log insertion in build', async () => {
20+
const output = next.cliOutput
21+
expect(output).not.toContain('testing-log-insertion:')
22+
})
23+
24+
it('should insert the html insertion into html body', async () => {
25+
const $ = await next.render$('/partial-resume')
26+
const output = next.cliOutput
27+
expect(output).toContain('testing-log-insertion:dynamic-data')
28+
29+
expect($('head [data-test-id]').length).toBe(0)
30+
expect($('body [data-test-id]').length).toBe(1)
31+
})
32+
})

0 commit comments

Comments
 (0)