In the beginning ...
In the beginning of web, there were no scripts; then there were minuscules of JavaScript aiming to give the web a bit of life, user interactions and dynamic flourishes, and what not; then JavaScript is everywhere, front and center, first and foremost.
The beginning of JavaScript was simple as all beginnings are. We simple stick a piece of reference in the head
of a page html, as so:
<script src="mysecretsource.js" />
or some inline sprinkle,
<script>
alert('Hello world')
</script>
Then JavaScript gets very, very complicated. And the business of JavaScript loading and injection gets very messy and confusing very quickly.
Current Affair of JavaScript Loading
In the name of performance, in the fight to capture and retain users' attention in the age of scanty and fleeting attention, how-when-where-and-whether we load a piece of JavaScript becomes a battle of survival and thrival for some web applications. Because, let's face it, if your web page does not load and start functioning with three seconds, your users are likely click away, with that, whatever hopes you may have to sell.
Performance becomes so critical that the lord of the web, Google, created a slew of performance tuning and monitoring tools. There are Google lighthouse, Webpage test, PageSpeed insights. Of the many metrics Google measures and uses to penalize some web sites for being bad (slow and janky) and elevate others for being good (fast and smooth), three of them are the most vital (so called web vitals):
The business of how JavaScript we load directly impacts all three of the web vitals. It is so critical that Google chrome has come up with a matrix of script loading and executing priorities:
-
<script>
in<head>
: highly critical scripts such as those would affect DOM structure of the entire page -
<link rel=preload>
+<script async>
hack or<script type=module async>
: medium/high priority scripts such as that generate critical content -
<script async>
: often used to indicate non-critical scripts -
<script defer>
: low priority scripts -
<script>
at the end of<body>
: lowest priority scripts that may be used occasionally -
<link rel=prefetch>
+<script>
: for scripts likely to to be helpful with a next-page navigation
Here comes Next.js
If you have been doing web development for a while, surely you have been washed over by the tides and fundamental shifts over SPA (single page application), MPA (Multiple page Application), SSR (Server side rendering), CSR (Client side rendering), SSR / CSR hybrid and this and that.
Over the many competing frameworks and philosophies, Next.Js have come out triumphant.
Next.js is an open-source web development framework created by Vercel enabling React-based web applications with server-side rendering and generating static websites. React documentation mentions Next.js among "Recommended Toolchains" advising it to developers as a solution when "Building a server-rendered website with Node.js".
Next.js has also been endorsed by Google and Vercel has been collaborating closely with Google.
Surely the headache of deciding how and when to load a piece of JavaScript has also reached Next.js and Google itself, so much so that Next.js have come up with a set of script loading strategies, which also has had Google's blessing.
The four strategies of Next Script
There are four strategies in using Next Script. Three really, beforeInteractive
, afterInteractive
, lazyOnLoad
. There is an experimental worker
strategy that utilizes Partytown.
beforeInteractive
: injects scripts in the head
with defer
;
const beforeInteractiveScripts = (scriptLoader.beforeInteractive || [])
.filter((script) => script.src)
.map((file: ScriptProps, index: number) => {
const { strategy, ...scriptProps } = file
return (
<script
{...scriptProps}
key={scriptProps.src || index}
defer={scriptProps.defer ?? !disableOptimizedLoading}
nonce={props.nonce}
data-nscript="beforeInteractive"
crossOrigin={props.crossOrigin || crossOrigin}
/>
)
})
Next.js Source Code for beforeInteractive
script handling
afterInteractive
: adds the script to the bottom of <body>
;
lazyOnload
: similar to afterInteractive
, however it uses requestIdleCallback
to wait until the main thread becomes idle
Next.js Source Code for afterInteractive
/lazyOnLoad
script handling
useEffect(() => {
// other code
if (strategy === 'afterInteractive') {
loadScript(props)
} else if (strategy === 'lazyOnload') {
loadLazyScript(props)
}
}
}, [props, strategy])
const { strategy = 'afterInteractive' } = props
if (strategy === 'lazyOnload') {
window.addEventListener('load', () => {
requestIdleCallback(() => loadScript(props))
})
} else {
loadScript(props)
}
Next.js Source use requestIdleCallback
to lazy load script
Next Script in practice
Using Next Script is easy, deciding which strategy to use is hard. Harder than just sticking the script somewhere anyway.
beforeInteractive
beforeInteractive
should only be used when a script is absolutely critical to the web application as a whole. For example, device setting detection, framework at run time.
Next.js states that:
This strategy only works inside
_document.js
and is designed to load scripts that are needed by the entire site
If for any reasons, you insist on using beforeInteractive
in a component, it will still work. It is just that the ordering and timing of this script might not be predictable.
Sample Code:
<Html>
<Head />
<body>
<Main />
<NextScript />
<Script
src="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/js/bootstrap.min.js"
strategy="beforeInteractive"
></Script>
</body>
</Html>
Checking the html output, you can see the script being loaded in the head
, before other Next.js first-party script.
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/js/bootstrap.min.js" defer="" data-nscript="beforeInteractive"></script>
afterInteractive
Next Script by default uses afterInteractive
strategy. You can add the script in any component.
Sample code:
<Script src="https://www.google-analytics.com/analytics.js" />
Checking the html output, you can see the script being inserted at the bottom of the body
.
<script src="https://www.google-analytics.com/analytics.js" data-nscript="afterInteractive"></script>
Third party scripts such as ads, google analytics should be good candidates for this strategy.
lazyOnLoad
lazyOnLoad
works the same way as scripts using afterInteractive
. However it will only be loaded during idle time. Next.js uses the window.requestIdleCallback method to check if browser is idle.
Third party scripts that have the lowest priorities should be loaded lazyOnLoad
strategy. For example, third party script for comments (if you do not care comments).
Sample code
<Script
src="https://www.google-analytics.com/analytics.js"
strategy="lazyOnload"
/>
Now if you check the html output, you can see it is also gets added at the bottom of page, like the script using afterInteractive
strategy.
If we fake some busy browser processing, we can clearly see how the layzOnLoad
strategy works.
For example, on component mount, we add the following code:
useEffect(() => {
for (let i = 0; i <= 10000; i++) {
if (i === 10000) console.log(i);
}
}, []);
Then we added an onLoad
event handler in the script, we can see the onLoad
event wont fire until the above dummy code finishing processing.
This can also be seen in the network waterfall capture:
Measuring the web vitals
So is Next Script better than that you pick and choose one of the priorities and inject scripts on your own? I feel it is hardly conclusive for the following reasons:
1) In the end, Next Script uses the native script
, only with a bit syntactic sugar and some cache handling. The same mistakes made with the native script
might also be made with Next Script by picking the wrong strategy, and vice versa.
2) I have created one dummy next.js pages and two static html pages. I have run tests on those pages through both webpage tests and PageSpeed Insights. Unfortunately, the results are not conclusive.
The set up of the two static html pages are:
Static html 1: GA script in the head, using async
(the way nytimes does), twitter script at the bottom of body
Static html 2: GA script in the middle of body
, twitter script at the bottom of body
Next Script Page: use afterInteractive
for both the twitter script and ga script.
Pagespeed test results (static1, static2, Next Script):
Pagespeed insights indicates that Next Script fares better than both the static html pages.
Webpage test results (static1, static2, Next Script)
However, webpage test gives the results that indicate the opposite:
Static 2 Webpage test metrics
Next Script Webpage test metrics
Conclusion
JavaScript is complicated, loading of JavaScript is complicated, though the complexities is merited given it has powered the vast and intricate web. Next Script is a nice addition to rein in the complexity and made developers' job a little easier.
Source Code
All code and some explanation can be found in my next.js playground and this github repository.
Top comments (3)
Chunks up the differences in Vanilla script loading vs Next.js while maintaining integrity and no bias.
Quick explanation of the differences means this is a bookmarked page for whenver I work with scripts
Great read!
thanks, Ahmad
Thank you for sharing this knowledge. I've noticed a weird thiing where the scripts are added but not executed between pages. So when I move from one page to another an event handler is not longer running.
Any guidance on what to do?
One suggestion I found was adding
?rerun=${Math.round(Math.random() * 100)
on the end of the script url but that seems hacky and variables are double set