Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defer scripts with Next JS

I am using Next JS on a project and I am going through some optimizations using Google's Page Speed Insights tool. One of the main negative contributers to my sites performance is 3rd party scripts mainly being Google Tag script (which also loads my Google Analytics) and the Google maps API script. So far I have tried several things to offset the loading of these two libraries but I still get the following messages from the tool:

Remove unused JavaScript

https://maps.googleapis.com/maps/api/js?key=asdafsafasafasfasf&libraries=places&language=en&region=US
(maps.googleapis.com)

https://www.googletagmanager.com/gtag/js?id=UA-123123-2
(www.googletagmanager.com)

I need to have these scripts loaded everywhere on the site so I tried to delay the loading of the scripts until the everything else on the page was finished loading. Here is what I initially had:

_document.js

import Document, { Head, Main, NextScript, Html } from "next/document";

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html lang="en">
        <Head>
          <script
            defer
            src={`https://www.googletagmanager.com/gtag/js?id=UA-123123-2`}
          />
          <script
            defer
            src="https://maps.googleapis.com/maps/api/js?key=asdafsafasafasfasf&libraries=places&language=en&region=US"
          />
          <script
            dangerouslySetInnerHTML={{
              __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'UA-123123-2');
          `,
            }}
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

As you can see I just load in the scripts with the defer prop like you normally would. With this method google page speed insights still give me the same message however. I then tried a different approach using an experimental component in Next JS called ScriptLoader which aims to allow people to defer scripts. Here is a link to that component:

https://github.com/vercel/next.js/discussions/18172

Here is my implementation of it:

_app.js

import App from "next/app";
import ScriptLoader from "next/dist/client/experimental-script.js";

export default class NextApp extends App {
  render() {
    return (<>
      <ScriptLoader
        strategy="defer"
        src="https://www.googletagmanager.com/gtag/js?id=UA-123123-2"
      />
      <ScriptLoader
        strategy="defer"
        src="https://maps.googleapis.com/maps/api/js?key=asdafsafasafasfasf&libraries=places&language=en&region=US"
      />
    </>)
  }
}

It still however gives me the same error message. I am not sure what to try now! Has anyone else come across this issue and found a workaround?

Thanks

like image 232
red house 87 Avatar asked Mar 02 '21 18:03

red house 87


2 Answers

Google Analytics / Gtag

From my own experience, I have to unfortunately tell you that deferring loading of analytics is not a good idea and often leads to unexpected errors when a function is called which does not exist (yet). So best load this in the very beginning even when it sucks that Google penalizes you for using their own scripts. So I think your initial approach with loading gtag via a custom _document.js file is fine.

Google Maps

Thank god it's different for Google Maps. There is an npm package called Google Maps JavaScript API Loader. This makes it possible - or easier - to load Maps dynamically. In addition to this, you can leverage the dynamic import functionality of JavaScript. You could create a custom loading function which only fires after some condition is fulfilled. It could look like this:

const myCustomLoadFunction = async () => {
  try {
    const { Loader } = await import("@googlemaps/js-api-loader");
    const loader = new Loader({
      apiKey: "YOUR_API_KEY",
      version: "weekly",
      // ...additionalOptions,
    });

    const mapOptions = {
      center: { lat: 0, lng: 0 },
      zoom: 4,
    };

    await loader.load();
    new google.maps.Map(document.getElementById("map"), mapOptions);
  } catch (e) {
    // do something
  }
};

You could place this in a useEffect hook or even call it when a user hovers somewhere or clicks on something and so delay the import even longer.

like image 56
Gh05d Avatar answered Oct 07 '22 18:10

Gh05d


I'm dealing with the same issue recently with a more complex GTM config. I can share my approaches.

First of all, In Lighthouse, the "Remove Unused JavaScript" does NOT mean the entire file is not used. It really just tell you that there is a large portion of unused code in a given file. If it's some 3rd party file, we might not be able to do anything about it.

Also, each JavaScript file, including analytics will have an impact on the performance. Ideally, only pay the price when the cost can be justified (essential analytics, app monitoring service, and so on ... ).

Goal

The goal is not to solve the "Remove Unused JavaScript". The real goal should be reducing the impact of JavaScript by deferring loading them so other more important page content can be load as fast as possible.

Async Tag

GTM itself doesn't have a big performance footprint. It really depends on what you have inside your GTM container. I think having GTM loaded with async tag is fine.

Google Maps API can be loaded asynchronously by GTM. We can decide when to load it more precisely.

GTM Loading

For scripts you don't need to deliver immediately, such as Google Map API, You probably don't want to load them on Container Loaded since it will take valuable resource which can be used to load real page content. You should consider moving it to Window Loaded. At that point, the page has been fully loaded and the impact on Google Web Core Vita should be minimum.

For analytics, ideally, they should be loaded as early as possible to capture data more precisely. However, this can change depends on the business requirement. For my organization, we move some analytics to Window Loaded phase. The reason is that we want to have better page loading performance and we don't really care as much about the users who bounce before the page is even loaded.

If a further delay is desire, we can create an additional trigger which fires after some ms of timeout. For more details, you can refer to this blog post

A Potential Solution

In summary, this is an approach I would go with. It is similar to what my team use for our projects.

  • Only load the GTM in Next.js with the async attribute.
  • Move Google Analytic and Google Map API to GTM
  • Delay the loading time as much as possible while it still makes sense to the business/project/user.

I hope it makes sense and is helpful.

References

GTM - Page View Trigger

like image 27
Andrew Zheng Avatar answered Oct 07 '22 17:10

Andrew Zheng