App Router Preinitialize all required scripts except one for bootstrap (#53705)
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 Co-authored-by: Steven <steven@ceriously.com>
This commit is contained in:
parent
d58fd68f0a
commit
79b7c1493b
4 changed files with 103 additions and 38 deletions
|
@ -78,6 +78,7 @@ import { appendMutableCookies } from '../web/spec-extension/adapters/request-coo
|
|||
import { ComponentsType } from '../../build/webpack/loaders/next-app-loader'
|
||||
import { ModuleReference } from '../../build/webpack/loaders/metadata/types'
|
||||
import { createServerInsertedHTML } from './server-inserted-html'
|
||||
import { getRequiredScripts } from './required-scripts'
|
||||
|
||||
export type GetDynamicParamFromSegment = (
|
||||
// [slug] / [[slug]] / [...slug]
|
||||
|
@ -1387,11 +1388,15 @@ export async function renderToHTMLOrFlight(
|
|||
* A new React Component that renders the provided React Component
|
||||
* using Flight which can then be rendered to HTML.
|
||||
*/
|
||||
const createServerComponentsRenderer = (loaderTreeToRender: LoaderTree) =>
|
||||
const createServerComponentsRenderer = (
|
||||
loaderTreeToRender: LoaderTree,
|
||||
preinitScripts: () => void
|
||||
) =>
|
||||
createServerComponentRenderer<{
|
||||
asNotFound: boolean
|
||||
}>(
|
||||
async (props) => {
|
||||
preinitScripts()
|
||||
// Create full component tree from root to leaf.
|
||||
const injectedCSS = new Set<string>()
|
||||
const injectedFontPreloadTags = new Set<string>()
|
||||
|
@ -1490,7 +1495,16 @@ export async function renderToHTMLOrFlight(
|
|||
integrity: subresourceIntegrityManifest?.[polyfill],
|
||||
}))
|
||||
|
||||
const ServerComponentsRenderer = createServerComponentsRenderer(tree)
|
||||
const [preinitScripts, bootstrapScript] = getRequiredScripts(
|
||||
buildManifest,
|
||||
assetPrefix,
|
||||
subresourceIntegrityManifest,
|
||||
getAssetQueryString(true)
|
||||
)
|
||||
const ServerComponentsRenderer = createServerComponentsRenderer(
|
||||
tree,
|
||||
preinitScripts
|
||||
)
|
||||
const content = (
|
||||
<HeadManagerContext.Provider
|
||||
value={{
|
||||
|
@ -1576,28 +1590,7 @@ export async function renderToHTMLOrFlight(
|
|||
onError: htmlRendererErrorHandler,
|
||||
nonce,
|
||||
// Include hydration scripts in the HTML
|
||||
bootstrapScripts: [
|
||||
...(subresourceIntegrityManifest
|
||||
? buildManifest.rootMainFiles.map((src) => ({
|
||||
src:
|
||||
`${assetPrefix}/_next/` +
|
||||
src +
|
||||
// Always include the timestamp query in development
|
||||
// as Safari caches them during the same session, no
|
||||
// matter what cache headers are set.
|
||||
getAssetQueryString(true),
|
||||
integrity: subresourceIntegrityManifest[src],
|
||||
}))
|
||||
: buildManifest.rootMainFiles.map(
|
||||
(src) =>
|
||||
`${assetPrefix}/_next/` +
|
||||
src +
|
||||
// Always include the timestamp query in development
|
||||
// as Safari caches them during the same session, no
|
||||
// matter what cache headers are set.
|
||||
getAssetQueryString(true)
|
||||
)),
|
||||
],
|
||||
bootstrapScripts: [bootstrapScript],
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1680,8 +1673,18 @@ export async function renderToHTMLOrFlight(
|
|||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const [errorPreinitScripts, errorBootstrapScript] =
|
||||
getRequiredScripts(
|
||||
buildManifest,
|
||||
assetPrefix,
|
||||
subresourceIntegrityManifest,
|
||||
getAssetQueryString(false)
|
||||
)
|
||||
|
||||
const ErrorPage = createServerComponentRenderer(
|
||||
async () => {
|
||||
errorPreinitScripts()
|
||||
const [MetadataTree, MetadataOutlet] = createMetadataComponents({
|
||||
tree, // still use original tree with not-found boundaries to extract metadata
|
||||
pathname,
|
||||
|
@ -1762,20 +1765,7 @@ export async function renderToHTMLOrFlight(
|
|||
streamOptions: {
|
||||
nonce,
|
||||
// Include hydration scripts in the HTML
|
||||
bootstrapScripts: subresourceIntegrityManifest
|
||||
? buildManifest.rootMainFiles.map((src) => ({
|
||||
src:
|
||||
`${assetPrefix}/_next/` +
|
||||
src +
|
||||
getAssetQueryString(false),
|
||||
integrity: subresourceIntegrityManifest[src],
|
||||
}))
|
||||
: buildManifest.rootMainFiles.map(
|
||||
(src) =>
|
||||
`${assetPrefix}/_next/` +
|
||||
src +
|
||||
getAssetQueryString(false)
|
||||
),
|
||||
bootstrapScripts: [errorBootstrapScript],
|
||||
},
|
||||
})
|
||||
|
||||
|
|
56
packages/next/src/server/app-render/required-scripts.tsx
Normal file
56
packages/next/src/server/app-render/required-scripts.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import type { BuildManifest } from '../get-page-files'
|
||||
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
export function getRequiredScripts(
|
||||
buildManifest: BuildManifest,
|
||||
assetPrefix: string,
|
||||
SRIManifest: undefined | Record<string, string>,
|
||||
qs: string
|
||||
): [() => void, string | { src: string; integrity: string }] {
|
||||
let preinitScripts: () => void
|
||||
let preinitScriptCommands: string[] = []
|
||||
let bootstrapScript: string | { src: string; integrity: string } = ''
|
||||
const files = buildManifest.rootMainFiles
|
||||
if (files.length === 0) {
|
||||
throw new Error(
|
||||
'Invariant: missing bootstrap script. This is a bug in Next.js'
|
||||
)
|
||||
}
|
||||
if (SRIManifest) {
|
||||
bootstrapScript = {
|
||||
src: `${assetPrefix}/_next/` + files[0] + qs,
|
||||
integrity: SRIManifest[files[0]],
|
||||
}
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
const src = `${assetPrefix}/_next/` + files[i] + qs
|
||||
const integrity = SRIManifest[files[i]]
|
||||
preinitScriptCommands.push(src, integrity)
|
||||
}
|
||||
preinitScripts = () => {
|
||||
// preinitScriptCommands is a double indexed array of src/integrity pairs
|
||||
for (let i = 0; i < preinitScriptCommands.length; i += 2) {
|
||||
ReactDOM.preinit(preinitScriptCommands[i], {
|
||||
as: 'script',
|
||||
integrity: preinitScriptCommands[i + 1],
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bootstrapScript = `${assetPrefix}/_next/` + files[0] + qs
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
const src = `${assetPrefix}/_next/` + files[i] + qs
|
||||
preinitScriptCommands.push(src)
|
||||
}
|
||||
preinitScripts = () => {
|
||||
// preinitScriptCommands is a singled indexed array of src values
|
||||
for (let i = 0; i < preinitScriptCommands.length; i++) {
|
||||
ReactDOM.preinit(preinitScriptCommands[i], {
|
||||
as: 'script',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [preinitScripts, bootstrapScript]
|
||||
}
|
8
test/e2e/app-dir/app/app/bootstrap/page.js
Normal file
8
test/e2e/app-dir/app/app/bootstrap/page.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default async function Page() {
|
||||
return (
|
||||
<div>
|
||||
This fixture is to assert where the bootstrap scripts and other required
|
||||
scripts emit during SSR
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1859,5 +1859,16 @@ createNextDescribe(
|
|||
expect(await browser.elementByCss('p').text()).toBe('item count 128000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('bootstrap scripts', () => {
|
||||
it('should only bootstrap with one script, prinitializing the rest', async () => {
|
||||
const html = await next.render('/bootstrap')
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
// We assume a minimum of 2 scripts, webpack runtime + main-app
|
||||
expect($('script[async]').length).toBeGreaterThan(1)
|
||||
expect($('body').find('script[async]').length).toBe(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue