DEV Community

Cover image for Script Loading, performance and Next Script
xxxd for This is Learning

Posted on • Updated on

Script Loading, performance and Next Script

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" />
Enter fullscreen mode Exit fullscreen mode

or some inline sprinkle,

<script>
 alert('Hello world')
</script>
Enter fullscreen mode Exit fullscreen mode

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):

LCP web vital

FID web vital

CLS web vital

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:

  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. 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}
        />
      )
    })
Enter fullscreen mode Exit fullscreen mode

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])

Enter fullscreen mode Exit fullscreen mode
  const { strategy = 'afterInteractive' } = props
  if (strategy === 'lazyOnload') {
    window.addEventListener('load', () => {
      requestIdleCallback(() => loadScript(props))
    })
  } else {
    loadScript(props)
  }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
            />
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }, []);
Enter fullscreen mode Exit fullscreen mode

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

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.

Static 1 PSI score
Static 1 PSI score

Static 2 PSI score
Static 2 PSI score

Next Script PSI score
Next Script PSI score

Webpage test results (static1, static2, Next Script)

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

Static 1 Webpage test metrics
Webpage test metrics for static 1

Static 2 Webpage test metrics

Webpage test metrics for static 2

Next Script Webpage test metrics

Webpage test metrics for Next Script

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)

Collapse
 
kayyali18 profile image
Ahmad Kayyali

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!

Collapse
 
xun_2cbcd4cac625126e profile image
xxxd

thanks, Ahmad

Collapse
 
lukeecart profile image
Luke Cartwright

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