Experimental script loader changes (#22038)
Making experimental script work in _document.js - Fixes for server to client transition Adding additional test for _document.js
This commit is contained in:
parent
103422ce70
commit
a107dcb732
11 changed files with 189 additions and 70 deletions
|
@ -1031,6 +1031,9 @@ export default async function getBaseWebpackConfig(
|
|||
'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify(
|
||||
config.experimental.optimizeCss && !dev
|
||||
),
|
||||
'process.env.__NEXT_SCRIPT_LOADER': JSON.stringify(
|
||||
!!config.experimental.scriptLoader
|
||||
),
|
||||
'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify(
|
||||
config.experimental.scrollRestoration
|
||||
),
|
||||
|
|
|
@ -7,7 +7,7 @@ import { requestIdleCallback } from './request-idle-callback'
|
|||
const ScriptCache = new Map()
|
||||
const LoadCache = new Set()
|
||||
|
||||
interface Props extends ScriptHTMLAttributes<HTMLScriptElement> {
|
||||
export interface Props extends ScriptHTMLAttributes<HTMLScriptElement> {
|
||||
strategy?: 'defer' | 'lazy' | 'dangerouslyBlockRendering' | 'eager'
|
||||
id?: string
|
||||
onLoad?: () => void
|
||||
|
@ -16,13 +16,22 @@ interface Props extends ScriptHTMLAttributes<HTMLScriptElement> {
|
|||
preload?: boolean
|
||||
}
|
||||
|
||||
const ignoreProps = [
|
||||
'onLoad',
|
||||
'dangerouslySetInnerHTML',
|
||||
'children',
|
||||
'onError',
|
||||
'strategy',
|
||||
'preload',
|
||||
]
|
||||
|
||||
const loadScript = (props: Props): void => {
|
||||
const {
|
||||
src = '',
|
||||
src,
|
||||
id,
|
||||
onLoad = () => {},
|
||||
dangerouslySetInnerHTML,
|
||||
children = '',
|
||||
id,
|
||||
onError,
|
||||
} = props
|
||||
|
||||
|
@ -72,7 +81,7 @@ const loadScript = (props: Props): void => {
|
|||
}
|
||||
|
||||
for (const [k, value] of Object.entries(props)) {
|
||||
if (value === undefined) {
|
||||
if (value === undefined || ignoreProps.includes(k)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -83,7 +92,32 @@ const loadScript = (props: Props): void => {
|
|||
document.body.appendChild(el)
|
||||
}
|
||||
|
||||
export default function Script(props: Props): JSX.Element | null {
|
||||
function handleClientScriptLoad(props: Props) {
|
||||
const { strategy = 'defer' } = props
|
||||
if (strategy === 'defer') {
|
||||
loadScript(props)
|
||||
} else if (strategy === 'lazy') {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function loadLazyScript(props: Props) {
|
||||
if (document.readyState === 'complete') {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function initScriptLoader(scriptLoaderItems: Props[]) {
|
||||
scriptLoaderItems.forEach(handleClientScriptLoad)
|
||||
}
|
||||
|
||||
function Script(props: Props): JSX.Element | null {
|
||||
const {
|
||||
src = '',
|
||||
onLoad = () => {},
|
||||
|
@ -102,11 +136,13 @@ export default function Script(props: Props): JSX.Element | null {
|
|||
if (strategy === 'defer') {
|
||||
loadScript(props)
|
||||
} else if (strategy === 'lazy') {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
})
|
||||
loadLazyScript(props)
|
||||
}
|
||||
}, [strategy, props])
|
||||
}, [props, strategy])
|
||||
|
||||
if (!process.env.__NEXT_SCRIPT_LOADER) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (strategy === 'dangerouslyBlockRendering') {
|
||||
const syncProps: Props = { ...restProps }
|
||||
|
@ -157,3 +193,5 @@ export default function Script(props: Props): JSX.Element | null {
|
|||
|
||||
return null
|
||||
}
|
||||
|
||||
export default Script
|
||||
|
|
|
@ -139,6 +139,11 @@ if (process.env.__NEXT_I18N_SUPPORT) {
|
|||
}
|
||||
}
|
||||
|
||||
if (process.env.__NEXT_SCRIPT_LOADER && data.scriptLoader) {
|
||||
const { initScriptLoader } = require('./experimental-script')
|
||||
initScriptLoader(data.scriptLoader)
|
||||
}
|
||||
|
||||
type RegisterFn = (input: [string, () => void]) => void
|
||||
|
||||
const pageLoader: PageLoader = new PageLoader(buildId, prefix)
|
||||
|
|
|
@ -103,6 +103,7 @@ export type NEXT_DATA = {
|
|||
locales?: string[]
|
||||
defaultLocale?: string
|
||||
domainLocales?: DomainLocales
|
||||
scriptLoader?: any[]
|
||||
isPreview?: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
} from '../next-server/server/get-page-files'
|
||||
import { cleanAmpPath } from '../next-server/server/utils'
|
||||
import { htmlEscapeJsonString } from '../server/htmlescape'
|
||||
import Script, {
|
||||
Props as ScriptLoaderProps,
|
||||
} from '../client/experimental-script'
|
||||
|
||||
export { DocumentContext, DocumentInitialProps, DocumentProps }
|
||||
|
||||
|
@ -313,6 +316,34 @@ export class Head extends Component<
|
|||
]
|
||||
}
|
||||
|
||||
handleDocumentScriptLoaderItems(children: React.ReactNode): ReactNode[] {
|
||||
const { scriptLoader } = this.context
|
||||
const scriptLoaderItems: ScriptLoaderProps[] = []
|
||||
const filteredChildren: ReactNode[] = []
|
||||
|
||||
React.Children.forEach(children, (child: any) => {
|
||||
if (child.type === Script) {
|
||||
if (child.props.strategy === 'eager') {
|
||||
scriptLoader.eager = (scriptLoader.eager || []).concat([
|
||||
{
|
||||
...child.props,
|
||||
},
|
||||
])
|
||||
return
|
||||
} else if (['lazy', 'defer'].includes(child.props.strategy)) {
|
||||
scriptLoaderItems.push(child.props)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
filteredChildren.push(child)
|
||||
})
|
||||
|
||||
this.context.__NEXT_DATA__.scriptLoader = scriptLoaderItems
|
||||
|
||||
return filteredChildren
|
||||
}
|
||||
|
||||
makeStylesheetInert(node: ReactNode): ReactNode[] {
|
||||
return React.Children.map(node, (c: any) => {
|
||||
if (
|
||||
|
@ -402,6 +433,10 @@ export class Head extends Component<
|
|||
children = this.makeStylesheetInert(children)
|
||||
}
|
||||
|
||||
if (process.env.__NEXT_SCRIPT_LOADER) {
|
||||
children = this.handleDocumentScriptLoaderItems(children)
|
||||
}
|
||||
|
||||
let hasAmphtmlRel = false
|
||||
let hasCanonicalRel = false
|
||||
|
||||
|
@ -649,10 +684,11 @@ export class NextScript extends Component<OriginProps> {
|
|||
getPreNextScripts() {
|
||||
const { scriptLoader } = this.context
|
||||
|
||||
return (scriptLoader.eager || []).map((file: string) => {
|
||||
return (scriptLoader.eager || []).map((file: ScriptLoaderProps) => {
|
||||
const { strategy, ...props } = file
|
||||
return (
|
||||
<script
|
||||
{...file}
|
||||
{...props}
|
||||
nonce={this.props.nonce}
|
||||
crossOrigin={
|
||||
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react'
|
||||
/// @ts-ignore
|
||||
import Document, { Main, NextScript, Head } from 'next/document'
|
||||
import Script from 'next/experimental-script'
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
constructor(props) {
|
||||
|
@ -19,6 +20,26 @@ export default class MyDocument extends Document {
|
|||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Voces"
|
||||
/>
|
||||
<Script
|
||||
id="documentDefer"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=documentDefer"
|
||||
strategy="defer"
|
||||
></Script>
|
||||
<Script
|
||||
id="documentLazy"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=documentLazy"
|
||||
strategy="lazy"
|
||||
></Script>
|
||||
<Script
|
||||
id="documentBlock"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=documentBlock"
|
||||
strategy="dangerouslyBlockRendering"
|
||||
></Script>
|
||||
<Script
|
||||
id="documentEager"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=documentEager"
|
||||
strategy="eager"
|
||||
></Script>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
|
|
@ -4,8 +4,8 @@ const Page = () => {
|
|||
return (
|
||||
<div class="container">
|
||||
<Script
|
||||
id="script"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=defer"
|
||||
id="scriptDefer"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptDefer"
|
||||
preload
|
||||
></Script>
|
||||
<div>index</div>
|
||||
|
|
|
@ -4,8 +4,8 @@ const Page = () => {
|
|||
return (
|
||||
<div class="container">
|
||||
<Script
|
||||
id="script"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=eager"
|
||||
id="scriptEager"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptEager"
|
||||
strategy="eager"
|
||||
></Script>
|
||||
<div>page1</div>
|
||||
|
|
|
@ -4,7 +4,7 @@ const Page = () => {
|
|||
return (
|
||||
<div class="container">
|
||||
<Script
|
||||
id="script"
|
||||
id="scriptBlock"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=dangerouslyBlockRendering"
|
||||
strategy="dangerouslyBlockRendering"
|
||||
></Script>
|
||||
|
|
|
@ -11,8 +11,8 @@ const Page = () => {
|
|||
})`}
|
||||
</Script>
|
||||
<Script
|
||||
id="script"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=lazy"
|
||||
id="scriptLazy"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptLazy"
|
||||
strategy="lazy"
|
||||
></Script>
|
||||
<div>page3</div>
|
||||
|
|
|
@ -40,26 +40,28 @@ describe('Script Loader', () => {
|
|||
browser = await webdriver(appPort, '/')
|
||||
await waitFor(1000)
|
||||
|
||||
const script = await browser.elementById('script')
|
||||
const src = await script.getAttribute('src')
|
||||
const scriptPreload = await browser.elementsByCss(
|
||||
`link[rel=preload][href="${src}"]`
|
||||
)
|
||||
const endScripts = await browser.elementsByCss(
|
||||
'#script ~ script[src^="/_next/static/"]'
|
||||
)
|
||||
const endPreloads = await browser.elementsByCss(
|
||||
`link[rel=preload][href="${src}"] ~ link[rel=preload][href^="/_next/static/"]`
|
||||
)
|
||||
async function test(id) {
|
||||
const script = await browser.elementById(id)
|
||||
const src = await script.getAttribute('src')
|
||||
const endScripts = await browser.elementsByCss(
|
||||
`#${id} ~ script[src^="/_next/static/"]`
|
||||
)
|
||||
const endPreloads = await browser.elementsByCss(
|
||||
`link[rel=preload][href="${src}"] ~ link[rel=preload][href^="/_next/static/"]`
|
||||
)
|
||||
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Renders preload
|
||||
expect(scriptPreload.length).toBeGreaterThan(0)
|
||||
// Script is inserted at the end
|
||||
expect(endScripts.length).toBe(0)
|
||||
//Preload is defined at the end
|
||||
expect(endPreloads.length).toBe(0)
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Script is inserted at the end
|
||||
expect(endScripts.length).toBe(0)
|
||||
//Preload is defined at the end
|
||||
expect(endPreloads.length).toBe(0)
|
||||
}
|
||||
|
||||
// Defer script in page
|
||||
await test('scriptDefer')
|
||||
// Defer script in _document
|
||||
await test('documentDefer')
|
||||
} finally {
|
||||
if (browser) await browser.close()
|
||||
}
|
||||
|
@ -73,15 +75,22 @@ describe('Script Loader', () => {
|
|||
await browser.waitForElementByCss('#onload-div')
|
||||
await waitFor(1000)
|
||||
|
||||
const script = await browser.elementById('script')
|
||||
const endScripts = await browser.elementsByCss(
|
||||
'#script ~ script[src^="/_next/static/"]'
|
||||
)
|
||||
async function test(id) {
|
||||
const script = await browser.elementById(id)
|
||||
const endScripts = await browser.elementsByCss(
|
||||
`#${id} ~ script[src^="/_next/static/"]`
|
||||
)
|
||||
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Script is inserted at the end
|
||||
expect(endScripts.length).toBe(0)
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Script is inserted at the end
|
||||
expect(endScripts.length).toBe(0)
|
||||
}
|
||||
|
||||
// Lazy script in page
|
||||
await test('scriptLazy')
|
||||
// Lazy script in _document
|
||||
await test('documentLazy')
|
||||
} finally {
|
||||
if (browser) await browser.close()
|
||||
}
|
||||
|
@ -91,33 +100,38 @@ describe('Script Loader', () => {
|
|||
const html = await renderViaHTTP(appPort, '/page1')
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const script = $('#script')
|
||||
const src = script.attr('src')
|
||||
function test(id) {
|
||||
const script = $(`#${id}`)
|
||||
const src = script.attr('src')
|
||||
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Preload is inserted at the beginning
|
||||
expect(
|
||||
$(
|
||||
`link[rel=preload][href="${src}"] ~ link[rel=preload][href^="/_next/static/"]`
|
||||
).length &&
|
||||
!$(
|
||||
`link[rel=preload][href^="/_next/static/chunks/main"] ~ link[rel=preload][href="${src}"]`
|
||||
// Renders script tag
|
||||
expect(script).toBeDefined()
|
||||
// Preload is inserted at the beginning
|
||||
expect(
|
||||
$(
|
||||
`link[rel=preload][href="${src}"] ~ link[rel=preload][href^="/_next/static/"]`
|
||||
).length &&
|
||||
!$(
|
||||
`link[rel=preload][href^="/_next/static/chunks/main"] ~ link[rel=preload][href="${src}"]`
|
||||
).length
|
||||
).toBeTruthy()
|
||||
|
||||
// Preload is inserted after fonts and CSS
|
||||
expect(
|
||||
$(
|
||||
`link[rel=stylesheet][href^="/_next/static/css"] ~ link[rel=preload][href="${src}"]`
|
||||
).length
|
||||
).toBeTruthy()
|
||||
).toBeGreaterThan(0)
|
||||
|
||||
// Preload is inserted after fonts and CSS
|
||||
expect(
|
||||
$(
|
||||
`link[rel=stylesheet][href^="/_next/static/css"] ~ link[rel=preload][href="${src}"]`
|
||||
).length
|
||||
).toBeGreaterThan(0)
|
||||
// Script is inserted before NextScripts
|
||||
expect(
|
||||
$(`#__NEXT_DATA__ ~ #${id} ~ script[src^="/_next/static/chunks/main"]`)
|
||||
.length
|
||||
).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
// Script is inserted before NextScripts
|
||||
expect(
|
||||
$('#__NEXT_DATA__ ~ #script ~ script[src^="/_next/static/chunks/main"]')
|
||||
.length
|
||||
).toBeGreaterThan(0)
|
||||
test('scriptEager')
|
||||
test('documentEager')
|
||||
})
|
||||
|
||||
it('priority dangerouslyBlockRendering', async () => {
|
||||
|
@ -125,10 +139,11 @@ describe('Script Loader', () => {
|
|||
const $ = cheerio.load(html)
|
||||
|
||||
// Script is inserted in place
|
||||
expect($('.container #script').length).toBeGreaterThan(0)
|
||||
expect($('.container #scriptBlock').length).toBeGreaterThan(0)
|
||||
expect($('head #documentBlock').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('onloads fire correctly', async () => {
|
||||
it('onload fires correctly', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/page4')
|
||||
|
|
Loading…
Reference in a new issue