Home
Published on

Loading 3rd-Party JavaScript (Like a Boss)

Tags
Author
  • Wilbert's Profile Pic
    Name
    Wilbert
    Twitter
    @wilbert_abreu
    Job Title
    Senior Engineer @ American Express 🇩🇴

3rd-party scripts are a common culprit when diagnosing page performance issues across the web.

Whether it's analytics scripts/"pixels", chatbot scripts, etc.., chances are if you are an engineer, marketer, product person, or entrepreneur you've had to inject these scripts on your website.

What's the issue?

Some engineers might say the problem is the presence of the 3rd-party scripts themselves. If you hear this, you have the green light to laugh at them out loud and walk away.

The unfortunate reality is unless you are maybe Facebook, Amazon, Neflix or Google, you don't have the resources or time to build everthing you use in-house.

For everyone else, we leverage "3rd-party tools" (tools/scripts that are built by other companies), to enhance or add some functionaility to our pages.

The performance issues arise in how these scripts are added onto our pages. This is a prime no-no example:

The Don't Do This Example: Inject in head, no async or defer

<head>
  <script src="https://pixelNeverDoThis.com"></script>
</head>

Doing this means you are essentially telling the browser, "This is a high priority script, complete parsing this script now and forget about everything else".

This blocks DOM parsing and rendering, delaying actual high priority resources, like your JS client chunks or images, from loading and negatively impacts almost every web metric you can think of.

Just tell me the right way!

Ok, hold on, I will, but first here's a convoluted meme to confuse you:

If you understood the above, congrats you're an Internet expert, here's your crown 👑.

For everyone else, lets start with the best approach:

<script>
  (() => {
    const injectScript = ({ src }) => {
      const scriptEl = document.createElement('script')
      scriptEl.setAttribute('type', 'text/javascript')
      scriptEl.setAttribute('async', '')
      scriptEl.src = src

      document.body.appendChild(scriptEl)
    }

    // To Inject a Single Script
    window.addEventListner('load', () => {
      injectScript({ src: "https://pixel1.com")
    }) 

    // To Inject Multiple Scripts
    // Example: 3rd-Party Scripts Array
    const scriptsArray = ["https://script1.com", "https://script2.com"]
    window.addEventListner('load', () => {
      scriptsArray.forEach(src => injectScript({ src }))
    }) 
  })()
</script>

If React is your cup of tea, as it is for me, you can also create an useExternalScripts hook like this:

import injectScript from '@@util/injectScript'

const useExternalScripts = ({ scriptsArray = []}) => {
  React.useEffect(() => {
    window.addEventListner('load', () => {
      scriptsArray.forEach(src => injectScript({ src }))
    }) 
  }, [])
}

Why is this the best approach?

The MDN docs (an Engineer's best friend), defines the "load" event, as a browser event that "is fired when the whole page has loaded, including all dependent resources such as stylesheets and images." Other than stylesheets and images, the main visual indicator the "load" event affects is the loading spinner in the browsers tab bar. In Chrome, it looks like this:

In other words, after the "load" event is the perfect time to start loading all your lesser priority resources (3rd-party scripts) that should not impact performance since the page has loaded already!

What kind of 3rd-party scripts should I use this for?

This approach is specifically for lower "priority" 3rd party scripts (think non-visual, not needed for page rendering in the viewport). If it's required immediately for visual page rendering in some way, whether it be React or otherwise, then it should be higher priority and not use this approach.

The rules of thumb are:

  • If it's absolutely needed for every one of your pages to render (or you can see it visually in the viewport on all pages), load it via a package manager.

  • If you only need it for a specific page or a subset (again visually in the viewport), install it via a package manager, but lazyload it.

  • If you only need it for a specific page or a subset (but it's not seen visually in the initial viewport), install it via a package manager, but lazyload it on scroll/click/another action.

  • If a package is not available for whatever reason (which is pretty common) and it's not seen visually in the viewport, then use this approach.


Thanks for reading everyone!

Follow me here.