Script Loading, performance and Next Script
Tags: javascript, nextjs, performance
Published: 2022-10-16 18:57:59

## 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](https://web.dev/vitals/)):


[![LCP web vital](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hsefrx8m0qw6zn91wc7q.jpg)
](https://web.dev/vitals/)

[![FID web vital](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ggltp6pkd86s1h6xzjib.jpg)
](https://web.dev/vitals/)

[![CLS web vital](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6ok89liyah5685ic1mi.jpg)
](https://web.dev/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](https://addyosmani.com/blog/script-priorities/):

1. `<script>` in `<head>`: highly critical scripts such as those would affect DOM structure of the entire page
2. `<link rel=preload>` + `<script async>` hack or
`<script type=module async>`: medium/high priority scripts such as that generate critical content
3. `<script async>`: often used to indicate non-critical scripts
4. `<script defer>`: low priority scripts
5. `<script>` at the end of `<body>`: lowest priority  scripts that may be used occasionally  
6. `<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](https://nextjs.org/docs/basic-features/script
). Three really, `beforeInteractive`, `afterInteractive`, `lazyOnLoad`. There is an experimental `worker` strategy that utilizes [Partytown](https://partytown.builder.io/). 

`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
](https://github.com/vercel/next.js/blob/canary/packages/next/pages/_document.tsx#L318-L332)

`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
](https://github.com/vercel/next.js/blob/canary/packages/next/client/script.tsx#L226-L236)



```
 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](https://github.com/vercel/next.js/blob/canary/packages/next/client/script.tsx#L226-L236)

## 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](https://developer.mozilla.org/en-US/docs/Web/API/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:


![LazyOnLoad script network waterfall](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87gs7i13l917zoh8sx5v.png)


## 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](https://www.webpagetest.org/) and [PageSpeed Insights](https://pagespeed.web.dev/). Unfortunately, the results are not conclusive. 

The set up of the two static html pages are:

[Static html 1]( https://nextjs-playground-cyan-chi.vercel.app/static/test.html): GA script in the head, using `async` (the way [nytimes](https://www.nytimes.com/2022/10/14/science/sarcastic-fringehead-mouth-display.html) does), twitter script at the bottom of body

[Static html 2]( https://nextjs-playground-cyan-chi.vercel.app/static/test2.html): GA script in the middle of `body`, twitter script at the bottom of body

[Next Script Page](https://nextjs-playground-cyan-chi.vercel.app/scripts-demo/performace/next-script): use `afterInteractive` for both the twitter script and ga script.


Pagespeed test results ([static1](https://pagespeed.web.dev/report?url=https%3A%2F%2Fnextjs-playground-cyan-chi.vercel.app%2Fstatic%2Ftest.html), [static2](https://pagespeed.web.dev/report?url=https%3A%2F%2Fnextjs-playground-cyan-chi.vercel.app%2Fstatic%2Ftest1.html), [Next Script](https://pagespeed.web.dev/report?url=https%3A%2F%2Fnextjs-playground-cyan-chi.vercel.app%2Fscripts-demo%2Fperformace%2Fnext-script)):

Pagespeed insights indicates that Next Script fares better than both the static html pages.

**Static 1 PSI score**
![Static 1 PSI score](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z30s3ejeane71d477u2p.png)

**Static 2 PSI score**
![Static 2 PSI score](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v771oaezqd278jttya8q.png)

**Next Script PSI score**
![Next Script PSI score](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mpnx34m1f36z23rpnfen.png)

Webpage test results ([static1](https://www.webpagetest.org/result/221016_AiDcW0_78T/), [static2](https://www.webpagetest.org/result/221016_BiDcH8_78G/), [Next Script](https://www.webpagetest.org/result/221016_BiDc3B_78D/))

However, webpage test gives the results that indicate the  opposite:

**Static 1 Webpage test metrics**
![Webpage test metrics for static 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uzzami7u8tvkwj3sw101.png)

**Static 2 Webpage test metrics**

![Webpage test metrics for static 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5o1a6qtfeo2q4wz8sbah.png)

**Next Script Webpage test metrics**

![Webpage test metrics for Next Script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ua1llm3z37dk6q0n6no3.png)

## 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](https://nextjs-playground-cyan-chi.vercel.app/) and [this github repository](https://github.com/xd-hearst/nextjs-playground).