Skip to content

Improving script loading strategy #24939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 13, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
@@ -1260,6 +1260,7 @@ export default async function getBaseWebpackConfig(
pageEnv: config.experimental.pageEnv,
excludeDefaultMomentLocales: config.future.excludeDefaultMomentLocales,
assetPrefix: config.assetPrefix,
disableOptimizedLoading: config.experimental.disableOptimizedLoading,
target,
reactProductionProfiling,
webpack: !!config.webpack,
3 changes: 3 additions & 0 deletions packages/next/export/index.ts
Original file line number Diff line number Diff line change
@@ -369,6 +369,7 @@ export default async function exportApp(
defaultLocale: i18n?.defaultLocale,
domainLocales: i18n?.domains,
trailingSlash: nextConfig.trailingSlash,
disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading,
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
@@ -541,6 +542,8 @@ export default async function exportApp(
optimizeFonts: nextConfig.optimizeFonts,
optimizeImages: nextConfig.experimental.optimizeImages,
optimizeCss: nextConfig.experimental.optimizeCss,
disableOptimizedLoading:
nextConfig.experimental.disableOptimizedLoading,
parentSpanId: pageExportSpan.id,
})

5 changes: 5 additions & 0 deletions packages/next/export/worker.ts
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ interface ExportPageInput {
optimizeFonts: boolean
optimizeImages?: boolean
optimizeCss: any
disableOptimizedLoading: any
parentSpanId: any
}

@@ -70,6 +71,7 @@ interface RenderOpts {
ampSkipValidation?: boolean
optimizeFonts?: boolean
optimizeImages?: boolean
disableOptimizedLoading?: boolean
optimizeCss?: any
fontManifest?: FontManifest
locales?: string[]
@@ -98,6 +100,7 @@ export default async function exportPage({
optimizeFonts,
optimizeImages,
optimizeCss,
disableOptimizedLoading,
}: ExportPageInput): Promise<ExportPageResults> {
const exportPageSpan = trace('export-page-worker', parentSpanId)

@@ -284,6 +287,7 @@ export default async function exportPage({
optimizeImages,
/// @ts-ignore
optimizeCss,
disableOptimizedLoading,
distDir,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
@@ -357,6 +361,7 @@ export default async function exportPage({
optimizeFonts,
optimizeImages,
optimizeCss,
disableOptimizedLoading,
fontManifest: optimizeFonts
? requireFontManifest(distDir, serverless)
: null,
1 change: 1 addition & 0 deletions packages/next/next-server/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -193,6 +193,7 @@ export type DocumentProps = DocumentInitialProps & {
devOnlyCacheBusterQueryString: string
scriptLoader: { afterInteractive?: string[]; beforeInteractive?: any[] }
locale?: string
disableOptimizedLoading?: boolean
}

/**
2 changes: 2 additions & 0 deletions packages/next/next-server/server/config-shared.ts
Original file line number Diff line number Diff line change
@@ -61,6 +61,7 @@ export type NextConfig = { [key: string]: any } & {
eslint?: boolean
reactRoot: boolean
enableBlurryPlaceholder: boolean
disableOptimizedLoading: boolean
}
}

@@ -118,6 +119,7 @@ export const defaultConfig: NextConfig = {
eslint: false,
reactRoot: Number(process.env.NEXT_PRIVATE_REACT_ROOT) > 0,
enableBlurryPlaceholder: false,
disableOptimizedLoading: true,
},
future: {
strictPostcssConfiguration: false,
3 changes: 3 additions & 0 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
@@ -157,6 +157,7 @@ export default class Server {
images: string
fontManifest: FontManifest
optimizeImages: boolean
disableOptimizedLoading: boolean
optimizeCss: any
locale?: string
locales?: string[]
@@ -217,6 +218,8 @@ export default class Server {
: null,
optimizeImages: !!this.nextConfig.experimental.optimizeImages,
optimizeCss: this.nextConfig.experimental.optimizeCss,
disableOptimizedLoading: this.nextConfig.experimental
.disableOptimizedLoading,
domainLocales: this.nextConfig.i18n?.domains,
distDir: this.distDir,
}
3 changes: 3 additions & 0 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
@@ -190,6 +190,7 @@ export type RenderOptsPartial = {
locales?: string[]
defaultLocale?: string
domainLocales?: DomainLocales
disableOptimizedLoading?: boolean
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
@@ -234,6 +235,7 @@ function renderDocument(
defaultLocale,
domainLocales,
isPreview,
disableOptimizedLoading,
}: RenderOpts & {
props: any
docComponentsRendered: DocumentProps['docComponentsRendered']
@@ -305,6 +307,7 @@ function renderDocument(
devOnlyCacheBusterQueryString,
scriptLoader,
locale,
disableOptimizedLoading,
...docProps,
})}
</AmpStateContext.Provider>
252 changes: 156 additions & 96 deletions packages/next/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -51,6 +51,115 @@ function getDocumentFiles(
}
}

function getPolyfillScripts(context: DocumentProps, props: OriginProps) {
// polyfills.js has to be rendered as nomodule without async
// It also has to be the first script to load
const {
assetPrefix,
buildManifest,
devOnlyCacheBusterQueryString,
disableOptimizedLoading,
} = context

return buildManifest.polyfillFiles
.filter(
(polyfill) => polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
)
.map((polyfill) => (
<script
key={polyfill}
defer={!disableOptimizedLoading}
nonce={props.nonce}
crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
noModule={true}
src={`${assetPrefix}/_next/${polyfill}${devOnlyCacheBusterQueryString}`}
/>
))
}

function getPreNextScripts(context: DocumentProps, props: OriginProps) {
const { scriptLoader, disableOptimizedLoading } = context

return (scriptLoader.beforeInteractive || []).map(
(file: ScriptLoaderProps) => {
const { strategy, ...scriptProps } = file
return (
<script
{...scriptProps}
defer={!disableOptimizedLoading}
nonce={props.nonce}
crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
/>
)
}
)
}

function getDynamicChunks(
context: DocumentProps,
props: OriginProps,
files: DocumentFiles
) {
const {
dynamicImports,
assetPrefix,
isDevelopment,
devOnlyCacheBusterQueryString,
disableOptimizedLoading,
} = context

return dynamicImports.map((file) => {
if (!file.endsWith('.js') || files.allFiles.includes(file)) return null

return (
<script
async={!isDevelopment && disableOptimizedLoading}
defer={!disableOptimizedLoading}
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${devOnlyCacheBusterQueryString}`}
nonce={props.nonce}
crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
/>
)
})
}

function getScripts(
context: DocumentProps,
props: OriginProps,
files: DocumentFiles
) {
const {
assetPrefix,
buildManifest,
isDevelopment,
devOnlyCacheBusterQueryString,
disableOptimizedLoading,
} = context

const normalScripts = files.allFiles.filter((file) => file.endsWith('.js'))
const lowPriorityScripts = buildManifest.lowPriorityFiles?.filter((file) =>
file.endsWith('.js')
)

return [...normalScripts, ...lowPriorityScripts].map((file) => {
return (
<script
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${devOnlyCacheBusterQueryString}`}
nonce={props.nonce}
async={!isDevelopment && disableOptimizedLoading}
defer={!disableOptimizedLoading}
crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
/>
)
})
}

/**
* `Document` component handles the initial `document` markup and renders only on the server side.
* Commonly used for implementing server side rendering for `css-in-js` libraries.
@@ -285,6 +394,22 @@ export class Head extends Component<
]
}

getDynamicChunks(files: DocumentFiles) {
return getDynamicChunks(this.context, this.props, files)
}

getPreNextScripts() {
return getPreNextScripts(this.context, this.props)
}

getScripts(files: DocumentFiles) {
return getScripts(this.context, this.props, files)
}

getPolyfillScripts() {
return getPolyfillScripts(this.context, this.props)
}

handleDocumentScriptLoaderItems(children: React.ReactNode): ReactNode[] {
const { scriptLoader } = this.context
const scriptLoaderItems: ScriptLoaderProps[] = []
@@ -347,9 +472,12 @@ export class Head extends Component<
headTags,
unstable_runtimeJS,
unstable_JsPreload,
disableOptimizedLoading,
} = this.context

const disableRuntimeJS = unstable_runtimeJS === false
const disableJsPreload = unstable_JsPreload === false
const disableJsPreload =
unstable_JsPreload === false || !disableOptimizedLoading

this.context.docComponentsRendered.Head = true

@@ -582,6 +710,18 @@ export class Head extends Component<
{!disableRuntimeJS &&
!disableJsPreload &&
this.getPreloadMainLinks(files)}
{!disableOptimizedLoading &&
!disableRuntimeJS &&
this.getPolyfillScripts()}
{!disableOptimizedLoading &&
!disableRuntimeJS &&
this.getPreNextScripts()}
{!disableOptimizedLoading &&
!disableRuntimeJS &&
this.getDynamicChunks(files)}
{!disableOptimizedLoading &&
!disableRuntimeJS &&
this.getScripts(files)}
{process.env.__NEXT_OPTIMIZE_CSS && this.getCssLinks(files)}
{process.env.__NEXT_OPTIMIZE_CSS && (
<noscript data-n-css={this.props.nonce ?? ''} />
@@ -627,106 +767,19 @@ export class NextScript extends Component<OriginProps> {
'!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();'

getDynamicChunks(files: DocumentFiles) {
const {
dynamicImports,
assetPrefix,
isDevelopment,
devOnlyCacheBusterQueryString,
} = this.context

return dynamicImports.map((file) => {
if (!file.endsWith('.js') || files.allFiles.includes(file)) return null

return (
<script
async={!isDevelopment}
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${devOnlyCacheBusterQueryString}`}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
})
return getDynamicChunks(this.context, this.props, files)
}

getPreNextScripts() {
const { scriptLoader } = this.context

return (scriptLoader.beforeInteractive || []).map(
(file: ScriptLoaderProps) => {
const { strategy, ...props } = file
return (
<script
{...props}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
}
)
return getPreNextScripts(this.context, this.props)
}

getScripts(files: DocumentFiles) {
const {
assetPrefix,
buildManifest,
isDevelopment,
devOnlyCacheBusterQueryString,
} = this.context

const normalScripts = files.allFiles.filter((file) => file.endsWith('.js'))
const lowPriorityScripts = buildManifest.lowPriorityFiles?.filter((file) =>
file.endsWith('.js')
)

return [...normalScripts, ...lowPriorityScripts].map((file) => {
return (
<script
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${devOnlyCacheBusterQueryString}`}
nonce={this.props.nonce}
async={!isDevelopment}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
})
return getScripts(this.context, this.props, files)
}

getPolyfillScripts() {
// polyfills.js has to be rendered as nomodule without async
// It also has to be the first script to load
const {
assetPrefix,
buildManifest,
devOnlyCacheBusterQueryString,
} = this.context

return buildManifest.polyfillFiles
.filter(
(polyfill) =>
polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
)
.map((polyfill) => (
<script
key={polyfill}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
noModule={true}
src={`${assetPrefix}/_next/${polyfill}${devOnlyCacheBusterQueryString}`}
/>
))
return getPolyfillScripts(this.context, this.props)
}

static getInlineScriptSource(documentProps: Readonly<DocumentProps>): string {
@@ -752,6 +805,7 @@ export class NextScript extends Component<OriginProps> {
unstable_runtimeJS,
docComponentsRendered,
devOnlyCacheBusterQueryString,
disableOptimizedLoading,
} = this.context
const disableRuntimeJS = unstable_runtimeJS === false

@@ -841,10 +895,16 @@ export class NextScript extends Component<OriginProps> {
}}
/>
)}
{!disableRuntimeJS && this.getPolyfillScripts()}
{!disableRuntimeJS && this.getPreNextScripts()}
{disableRuntimeJS ? null : this.getDynamicChunks(files)}
{disableRuntimeJS ? null : this.getScripts(files)}
{disableOptimizedLoading &&
!disableRuntimeJS &&
this.getPolyfillScripts()}
{disableOptimizedLoading &&
!disableRuntimeJS &&
this.getPreNextScripts()}
{disableOptimizedLoading &&
!disableRuntimeJS &&
this.getDynamicChunks(files)}
{disableOptimizedLoading && !disableRuntimeJS && this.getScripts(files)}
</>
)
}
3 changes: 3 additions & 0 deletions test/integration/optimized-loading/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
experimental: { disableOptimizedLoading: false },
}
10 changes: 10 additions & 0 deletions test/integration/optimized-loading/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default () => (
<div>
<h1>Hello World!</h1>
<br />
<br />
<Link href="/page1">Without font</Link>
</div>
)
9 changes: 9 additions & 0 deletions test/integration/optimized-loading/pages/page1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default () => <h1>Hello World!</h1>

export const getServerSideProps = () => {
return {
props: {
hello: 'world',
},
}
}
75 changes: 75 additions & 0 deletions test/integration/optimized-loading/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-env jest */

import { join } from 'path'
import cheerio from 'cheerio'
import {
nextServer,
nextBuild,
startApp,
stopApp,
renderViaHTTP,
findPort,
launchApp,
killApp,
} from 'next-test-utils'

const appDir = join(__dirname, '../')
let server
let app
jest.setTimeout(1000 * 60 * 5)

const context = {}

function runTests(url) {
it('should render the page', async () => {
const html = await renderViaHTTP(context.appPort, url)
expect(html).toMatch(/Hello World/)
})

it('should not have JS preload links', async () => {
const html = await renderViaHTTP(context.appPort, url)
const $ = cheerio.load(html)
expect($('link[rel=preload]').length).toBe(0)
})

it('should load scripts with defer in head', async () => {
const html = await renderViaHTTP(context.appPort, url)
const $ = cheerio.load(html)
expect($('script[async]').length).toBe(0)
expect($('head script[defer]').length).toBeGreaterThan(0)
})
}

describe('Optimized loading', () => {
describe('production mode', () => {
beforeAll(async () => {
await nextBuild(appDir)
app = nextServer({
dir: join(__dirname, '../'),
dev: false,
quiet: true,
})

server = await startApp(app)
context.appPort = server.address().port
})
afterAll(() => stopApp(server))

runTests('/')
runTests('/page1')
})

describe('dev mode', () => {
let app

beforeAll(async () => {
context.appPort = await findPort()
app = await launchApp(join(__dirname, '../'), context.appPort)
})

afterAll(() => killApp(app))

runTests('/')
runTests('/page1')
})
})