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
Changes to the beforeInteractive strategy to make it work for streaming #31936
Changes from 11 commits
bd0e593
74c3b90
534cdc9
5f4b871
34b1dfd
fb0c0c1
4183720
375a603
60e0b6f
7830ead
162c6b2
b9af011
1f0586d
899b506
0e2437b
a0f97b4
0a38ef9
def82bc
5a0aaf8
caa62ca
b9d293f
1b3389b
bc9c136
d6967f9
6530d68
15ea355
d83e4c5
73c282a
9660ff9
242324d
7a591e5
b370a52
99dcc1d
b6cb974
450ab3b
316f368
11fdcad
6627015
19f9ae1
1bec0e6
c6e2f2f
d8d6a4a
01346c1
beeb03a
243b0ef
e00ed11
2bc903b
04253eb
0d8e9d5
e17be39
bf11680
265c654
d292f25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -66,26 +66,73 @@ With `next/script`, you decide when to load your third-party script by using the | |||||
|
||||||
There are three different loading strategies that can be used: | ||||||
|
||||||
- `beforeInteractive`: Load before the page is interactive | ||||||
- `beforeInteractive`: Load script when any page in the application is loaded and before it becomes interactive | ||||||
<!-- - `beforePageRender`: Load script for a single page before it becomes interactive --> | ||||||
- `afterInteractive`: (**default**): Load immediately after the page becomes interactive | ||||||
- `lazyOnload`: Load during idle time | ||||||
|
||||||
#### beforeInteractive | ||||||
|
||||||
Scripts that load with the `beforeInteractive` strategy are injected into the initial HTML from the server and run before self-bundled JavaScript is executed. This strategy should be used for any critical scripts that need to be fetched and executed before the page is interactive. | ||||||
Scripts that load with the `beforeInteractive` strategy are injected into the initial HTML from the server and run before self-bundled JavaScript is executed. This strategy should be used for any critical scripts that need to be fetched and executed before any page becomes interactive. This strategy only works inside **\_document.js** and is designed to load scripts that is needed by the entire site (i.e. the script will load when any page in the application has been loaded server-side). | ||||||
|
||||||
The reason `beforeInteractive` was designed this way, to work only inside `\_document.js` is to support streaming modes. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```jsx | ||||||
<Script | ||||||
src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" | ||||||
strategy="beforeInteractive" | ||||||
/> | ||||||
// In _document.js | ||||||
|
||||||
ijjk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
export default class MyDocument extends Document { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use a functional |
||||||
render() { | ||||||
return ( | ||||||
<Html> | ||||||
<Head /> | ||||||
<body> | ||||||
<Main /> | ||||||
<NextScript /> | ||||||
<Script | ||||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" | ||||||
strategy="beforeInteractive" | ||||||
></Script> | ||||||
</body> | ||||||
</Html> | ||||||
) | ||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
Examples of scripts that should be loaded as soon as possible with this strategy include: | ||||||
|
||||||
- Bot detectors | ||||||
- Cookie consent managers | ||||||
|
||||||
<!--- | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's delete this comment for now? (Maybe save on a Git branch) |
||||||
#### beforePageRender | ||||||
|
||||||
Scripts that load with the `beforePageRender` strategy are injected into the initial HTML from the server and run before self-bundled JavaScript is executed. This strategy is similar to `beforeInteractive` but is designed for scripts that are needed by a page and not the entire site (i.e. this script will load only when the page that uses it is loaded server-side) | ||||||
|
||||||
```jsx | ||||||
import Script from 'next/script' | ||||||
|
||||||
const Page = () => { | ||||||
return ( | ||||||
<div class="container"> | ||||||
<div>page1</div> | ||||||
</div> | ||||||
) | ||||||
} | ||||||
|
||||||
Page.scriptLoader = () => { | ||||||
return ( | ||||||
<Script | ||||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" | ||||||
strategy="beforePageRender" | ||||||
></Script> | ||||||
) | ||||||
} | ||||||
|
||||||
export default Page | ||||||
``` | ||||||
--> | ||||||
|
||||||
#### afterInteractive | ||||||
|
||||||
Scripts that use the `afterInteractive` strategy are injected client-side and will run after Next.js hydrates the page. This strategy should be used for scripts that do not need to load as soon as possible and can be fetched and executed immediately after the page is interactive. | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -549,8 +549,8 @@ | |||||||||
"path": "/errors/sharp-version-avif.md" | ||||||||||
}, | ||||||||||
{ | ||||||||||
"title": "script-in-document-page", | ||||||||||
"path": "/errors/no-script-in-document-page.md" | ||||||||||
"title": "before-interactive-script-outside-doument", | ||||||||||
"path": "/errors/no-before-interactive-script-outside-doument.md" | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
}, | ||||||||||
{ | ||||||||||
"title": "script-component-in-head-component", | ||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,34 @@ | ||||||
# beforeInteractive Script component outside \_document.js | ||||||
|
||||||
#### Why This Error Occurred | ||||||
|
||||||
You can't use the `next/script` component with the `beforeInteractive` strategy outside the `_document.js` page. That's because `beforeInteractive` strategy only works inside **\_document.js** and is designed to load scripts that is needed by the entire site (i.e. the script will load when any page in the application has been loaded server-side). | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
The strategy is designed this way to make it compatible with streaming modes. | ||||||
|
||||||
#### Possible Ways to Fix It | ||||||
|
||||||
If you want a global script, move the script inside `_document.js` page. | ||||||
|
||||||
```jsx | ||||||
// In _document.js | ||||||
|
||||||
ijjk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
export default class MyDocument extends Document { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, should we use a functional |
||||||
render() { | ||||||
return ( | ||||||
<Html> | ||||||
<Head /> | ||||||
<body> | ||||||
<Main /> | ||||||
<NextScript /> | ||||||
<Script | ||||||
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js" | ||||||
strategy="beforeInteractive" | ||||||
></Script> | ||||||
</body> | ||||||
</Html> | ||||||
) | ||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
- [next-script](https://nextjs.org/docs/basic-features/script#usage) |
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -13,13 +13,13 @@ module.exports = { | |||||
'link-passhref': require('./rules/link-passhref'), | ||||||
'no-document-import-in-page': require('./rules/no-document-import-in-page'), | ||||||
'no-head-import-in-document': require('./rules/no-head-import-in-document'), | ||||||
'no-script-in-document': require('./rules/no-script-in-document'), | ||||||
'no-script-component-in-head': require('./rules/no-script-component-in-head'), | ||||||
'no-server-import-in-page': require('./rules/no-server-import-in-page'), | ||||||
'no-typos': require('./rules/no-typos'), | ||||||
'no-duplicate-head': require('./rules/no-duplicate-head'), | ||||||
'inline-script-id': require('./rules/inline-script-id'), | ||||||
'next-script-for-ga': require('./rules/next-script-for-ga'), | ||||||
'no-before-interactive-script-outside-doument': require('./rules/no-before-interactive-script-outside-doument'), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}, | ||||||
configs: { | ||||||
recommended: { | ||||||
|
@@ -39,12 +39,12 @@ module.exports = { | |||||
'@next/next/next-script-for-ga': 1, | ||||||
'@next/next/no-document-import-in-page': 2, | ||||||
'@next/next/no-head-import-in-document': 2, | ||||||
'@next/next/no-script-in-document': 2, | ||||||
'@next/next/no-script-component-in-head': 2, | ||||||
'@next/next/no-server-import-in-page': 2, | ||||||
'@next/next/no-typos': 1, | ||||||
'@next/next/no-duplicate-head': 2, | ||||||
'@next/next/inline-script-id': 2, | ||||||
'@next/next/no-before-interactive-script-outside-doument': 2, | ||||||
}, | ||||||
}, | ||||||
'core-web-vitals': { | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,51 @@ | ||||||
const path = require('path') | ||||||
|
||||||
module.exports = { | ||||||
meta: { | ||||||
docs: { | ||||||
description: | ||||||
'Disallow using next/script beforeInteractive strategy outside the next/_document component', | ||||||
recommended: true, | ||||||
url: 'https://nextjs.org/docs/messages/no-before-interactive-script-outside-doument', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}, | ||||||
}, | ||||||
create: function (context) { | ||||||
let scriptImportName = null | ||||||
|
||||||
return { | ||||||
'ImportDeclaration[source.value="next/script"] > ImportDefaultSpecifier'( | ||||||
node | ||||||
) { | ||||||
scriptImportName = node.local.name | ||||||
}, | ||||||
JSXOpeningElement(node) { | ||||||
if (!scriptImportName) { | ||||||
return | ||||||
} | ||||||
|
||||||
if (node.name && node.name.name !== scriptImportName) { | ||||||
return | ||||||
} | ||||||
|
||||||
const strategy = node.attributes.find( | ||||||
(child) => child.name && child.name.name === 'strategy' | ||||||
) | ||||||
|
||||||
if (strategy.value && strategy.value.value !== 'beforeInteractive') { | ||||||
return | ||||||
} | ||||||
|
||||||
const document = context.getFilename().split('pages')[1] | ||||||
if (document && path.parse(document).name.startsWith('_document')) { | ||||||
return | ||||||
} | ||||||
|
||||||
context.report({ | ||||||
node, | ||||||
message: | ||||||
'next/script beforeInteractive strategy should only be used inside next/_document. See: https://nextjs.org/docs/messages/no-before-interactive-script-outside-doument', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}) | ||||||
}, | ||||||
} | ||||||
}, | ||||||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -547,6 +547,7 @@ export async function renderToHTML( | |
App.getInitialProps === (App as any).origGetInitialProps | ||
|
||
const hasPageGetInitialProps = !!(Component as any)?.getInitialProps | ||
const hasPageScripts = (Component as any).scriptLoader | ||
|
||
const pageIsDynamic = isDynamicRoute(pathname) | ||
|
||
|
@@ -735,6 +736,18 @@ export async function renderToHTML( | |
|
||
let head: JSX.Element[] = defaultHead(inAmpMode) | ||
|
||
let initialScripts: any = {} | ||
if (hasPageScripts) { | ||
initialScripts.beforeInteractive = [] | ||
.concat(hasPageScripts()) | ||
.filter( | ||
(script: any) => | ||
script.props.strategy === 'beforeInteractive' || | ||
script.props.strategy === 'beforePageRender' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove |
||
) | ||
.map((script: any) => script.props) | ||
} | ||
|
||
let scriptLoader: any = {} | ||
const nextExport = | ||
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback))) | ||
|
@@ -784,7 +797,7 @@ export async function renderToHTML( | |
updateScripts: (scripts) => { | ||
scriptLoader = scripts | ||
}, | ||
scripts: {}, | ||
scripts: initialScripts, | ||
mountedInstances: new Set(), | ||
}} | ||
> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: is => are