Skip to content

Commit 3b5bec9

Browse files
committed
Currently all scripts that are required for every page are loaded as part of the bootstrap scripts API in React. Unfortunately this loads them all as sync scripts and thus requires preloading which increases their priority higher than they might otherwise be causing things like images to load later than desired, blocking paint. We can improve this by only using one script for bootstrapping and having the rest pre-initialized. This only works because all of these scripts are webpack runtime or chunks and can be loaded in any order asynchronously.
With this change we should see improvements in LCP and other metrics as preloads for images are favored over loading scripts
1 parent f0dab3a commit 3b5bec9

File tree

4 files changed

+100
-38
lines changed

4 files changed

+100
-38
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { appendMutableCookies } from '../web/spec-extension/adapters/request-coo
7878
import { ComponentsType } from '../../build/webpack/loaders/next-app-loader'
7979
import { ModuleReference } from '../../build/webpack/loaders/metadata/types'
8080
import { createServerInsertedHTML } from './server-inserted-html'
81+
import { getRequiredScripts } from './required-scripts'
8182

8283
export type GetDynamicParamFromSegment = (
8384
// [slug] / [[slug]] / [...slug]
@@ -1387,11 +1388,15 @@ export async function renderToHTMLOrFlight(
13871388
* A new React Component that renders the provided React Component
13881389
* using Flight which can then be rendered to HTML.
13891390
*/
1390-
const createServerComponentsRenderer = (loaderTreeToRender: LoaderTree) =>
1391+
const createServerComponentsRenderer = (
1392+
loaderTreeToRender: LoaderTree,
1393+
preinitScripts: () => void
1394+
) =>
13911395
createServerComponentRenderer<{
13921396
asNotFound: boolean
13931397
}>(
13941398
async (props) => {
1399+
preinitScripts()
13951400
// Create full component tree from root to leaf.
13961401
const injectedCSS = new Set<string>()
13971402
const injectedFontPreloadTags = new Set<string>()
@@ -1490,7 +1495,16 @@ export async function renderToHTMLOrFlight(
14901495
integrity: subresourceIntegrityManifest?.[polyfill],
14911496
}))
14921497

1493-
const ServerComponentsRenderer = createServerComponentsRenderer(tree)
1498+
const [preinitScripts, bootstrapScript] = getRequiredScripts(
1499+
buildManifest,
1500+
assetPrefix,
1501+
subresourceIntegrityManifest,
1502+
getAssetQueryString(true)
1503+
)
1504+
const ServerComponentsRenderer = createServerComponentsRenderer(
1505+
tree,
1506+
preinitScripts
1507+
)
14941508
const content = (
14951509
<HeadManagerContext.Provider
14961510
value={{
@@ -1576,28 +1590,7 @@ export async function renderToHTMLOrFlight(
15761590
onError: htmlRendererErrorHandler,
15771591
nonce,
15781592
// Include hydration scripts in the HTML
1579-
bootstrapScripts: [
1580-
...(subresourceIntegrityManifest
1581-
? buildManifest.rootMainFiles.map((src) => ({
1582-
src:
1583-
`${assetPrefix}/_next/` +
1584-
src +
1585-
// Always include the timestamp query in development
1586-
// as Safari caches them during the same session, no
1587-
// matter what cache headers are set.
1588-
getAssetQueryString(true),
1589-
integrity: subresourceIntegrityManifest[src],
1590-
}))
1591-
: buildManifest.rootMainFiles.map(
1592-
(src) =>
1593-
`${assetPrefix}/_next/` +
1594-
src +
1595-
// Always include the timestamp query in development
1596-
// as Safari caches them during the same session, no
1597-
// matter what cache headers are set.
1598-
getAssetQueryString(true)
1599-
)),
1600-
],
1593+
bootstrapScripts: [bootstrapScript],
16011594
},
16021595
})
16031596

@@ -1680,8 +1673,18 @@ export async function renderToHTMLOrFlight(
16801673
)}
16811674
</>
16821675
)
1676+
1677+
const [errorPreinitScripts, errorBootstrapScript] =
1678+
getRequiredScripts(
1679+
buildManifest,
1680+
assetPrefix,
1681+
subresourceIntegrityManifest,
1682+
getAssetQueryString(false)
1683+
)
1684+
16831685
const ErrorPage = createServerComponentRenderer(
16841686
async () => {
1687+
errorPreinitScripts()
16851688
const [MetadataTree, MetadataOutlet] = createMetadataComponents({
16861689
tree, // still use original tree with not-found boundaries to extract metadata
16871690
pathname,
@@ -1762,20 +1765,7 @@ export async function renderToHTMLOrFlight(
17621765
streamOptions: {
17631766
nonce,
17641767
// Include hydration scripts in the HTML
1765-
bootstrapScripts: subresourceIntegrityManifest
1766-
? buildManifest.rootMainFiles.map((src) => ({
1767-
src:
1768-
`${assetPrefix}/_next/` +
1769-
src +
1770-
getAssetQueryString(false),
1771-
integrity: subresourceIntegrityManifest[src],
1772-
}))
1773-
: buildManifest.rootMainFiles.map(
1774-
(src) =>
1775-
`${assetPrefix}/_next/` +
1776-
src +
1777-
getAssetQueryString(false)
1778-
),
1768+
bootstrapScripts: [errorBootstrapScript],
17791769
},
17801770
})
17811771

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { BuildManifest } from '../get-page-files'
2+
3+
import ReactDOM from 'react-dom'
4+
5+
export function getRequiredScripts(
6+
buildManifest: BuildManifest,
7+
assetPrefix: string,
8+
SRIManifest: undefined | Record<string, string>,
9+
qs: string
10+
): [() => void, string | { src: string; integrity: string }] {
11+
let preinitScripts: () => void
12+
let preinitScriptCommands: string[] = []
13+
let bootstrapScript: string | { src: string; integrity: string } = ''
14+
const files = buildManifest.rootMainFiles
15+
if (files.length === 0) {
16+
throw new Error(
17+
'Invariant: missing bootstrap script. This is a bug in Next.js'
18+
)
19+
}
20+
if (SRIManifest) {
21+
bootstrapScript = {
22+
src: `${assetPrefix}/_next/` + files[0] + qs,
23+
integrity: SRIManifest[files[0]],
24+
}
25+
for (let i = 1; i < files.length; i++) {
26+
const src = `${assetPrefix}/_next/` + files[i] + qs
27+
const integrity = SRIManifest[files[i]]
28+
preinitScriptCommands.push(src, integrity)
29+
}
30+
preinitScripts = () => {
31+
// preinitScriptCommands is a double indexed array of src/integrity pairs
32+
for (let i = 0; i < preinitScriptCommands.length; i += 2) {
33+
ReactDOM.preinit(preinitScriptCommands[i], {
34+
as: 'script',
35+
integrity: preinitScriptCommands[i + 1],
36+
})
37+
}
38+
}
39+
} else {
40+
bootstrapScript = `${assetPrefix}/_next/` + files[0] + qs
41+
for (let i = 1; i < files.length; i++) {
42+
const src = `${assetPrefix}/_next/` + files[i] + qs
43+
preinitScriptCommands.push(src)
44+
}
45+
preinitScripts = () => {
46+
// preinitScriptCommands is a singled indexed array of src values
47+
for (let i = 0; i < preinitScriptCommands.length; i++) {
48+
ReactDOM.preinit(preinitScriptCommands[i], {
49+
as: 'script',
50+
})
51+
}
52+
}
53+
}
54+
55+
return [preinitScripts, bootstrapScript]
56+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default async function Page() {
2+
return (
3+
<div id="anchor">hello, bootstrap scripts should appear after this div</div>
4+
)
5+
}

test/e2e/app-dir/app/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,5 +1859,16 @@ createNextDescribe(
18591859
expect(await browser.elementByCss('p').text()).toBe('item count 128000')
18601860
})
18611861
})
1862+
1863+
describe('bootstrap scripts', () => {
1864+
it('should only bootstrap with one script, prinitializing the rest', async () => {
1865+
const html = await next.render('/bootstrap')
1866+
const $ = cheerio.load(html)
1867+
1868+
// We assume a minimum of 2 scripts, webpack runtime + main-app
1869+
expect($('script[async]').length).toBeGreaterThan(1)
1870+
expect($('body').find('script[async]').length).toBe(1)
1871+
})
1872+
})
18621873
}
18631874
)

0 commit comments

Comments
 (0)