Boost page speed with “mindful loading” of third party scripts
Be even lazier than lazy loading: A smarter way to load external scripts and widgets.
We’ve all been there, building the most fabulously fast site ever, when someone asks…
“Can you just add this little script to the page?”
“Hmm, ok, what does the script do?”
“Oh it’s just for analytics | tracking | product reviews | user-comments | social media | a-b testing | some other thing that the Director of X says must go in before release.”
So you add the little script. But all of a sudden…
As you suspected, that little script is not so little. And it fetches more scripts. And they fiddle with your perfectly crafted DOM. And they all seem to be huge and slow too. And they’re killing your beautiful Page Speed score. What the hell are they doing?!
No problem. You start thinking and googling. You read somewhere that scripts can block the page load, so…
you try the usual quick wins that you googled last time…
- Move the script to the bottom of the page or
- Use async:
<script async src="//thirdparty.com/widget.js">
or - Use defer:
<script defer src="//thirdparty.com/widget.js">
…but that’s not enough,
so you try something super clever and pat yourself on the back for your phenomenal cunning:
<link rel="preconnect" href="//thirdparty.com">
<link rel="preconnect" href="//anotherThirdparty.com">
All these things are worth doing and make a difference, that you can see and measure when you run a Lighthouse report or WebPageTest, but they’re only small improvements.
What if there’s an even better way?
Let’s rethink. When do we actually *need* that script?
Typically the answer is either…
(a) We need it immediately on page load
Ok, a gazillion clever people have posted solutions to optimise this. Including the little techniques above. You’ve just got to do what you can to reduce the impact of that script.
(The most effective is to push back and discuss whether you really do need it on page load. If you get lucky you may find you don’t need it until later.)
or…
(b) We need it later
- when the user scrolls down the page or
- when the user clicks or swipes something.
Oooh, now we see an opportunity.
Postponing the script in the Browser
Let’s look at some solutions, in order of impact. Each of these involves taking a step back and re-thinking the norm a little more each time.
We’ll just summarise the approaches for now, and go into detail later in this article because there’ll be even more we can do to improve the illusion of speed.
Solution 1: Late load
The simplest solution. After page load, wait a few seconds before adding the script to the page. Ok, ok, this feels horribly hacky and unscientific and will surely make some people a little sick in the mouth.
I’m not recommending it but including it here for completeness. It can be an acceptable compromise in some cases. Just use it with care and thought when you have no other options. It is really only helpful for giving the browser some breathing space while the more important assets are fetched, but there’s no certain way to know how long to wait.
Note: This is really only suitable for scripts that must be on the page but are not needed right away. One use case might be a shopping cart total from some self-important third party that thinks sending megabytes to everyone’s mobile is ok. Is it really worth degrading the speed of the whole page just to show how many things are in your cart?
Solution 2: Lazy load
You’ve done this with images right? Heck it’s so normal for images that image lazyloading has gone native.
So why don’t we do this for external scripts and widgets too? Only add them to the DOM after the user scrolls down the page.
You could borrow from the image lazy-loading world and write a little IntersectionObserver helper to inject our script tag instead.
In case you’re new to this malarkey, lazyloading is a technique that avoids fetching an image until the user scrolls down to it. The image is only fetched when it is scrolled into the viewport.
Solution 3: Load on demand
Perhaps most users don’t interact with the widget, so why waste their time loading it needlessly?
Wait for the user to tap something before you add the script to the page. Good examples of this are widgets to leave reviews or comments. Most people don’t, so wait until they choose to.
This is my favourite. The overall reduction in network use is super satisfying. More on this in the next section because it works best when part of your UI is rendered server-side.
Use the server too
Now combine the above thinking with a server-side solution. Take your pick of whatever is fashionable today:
- Prerender / Static Site Generation (SSG)
- Server Side Render (SSR)
Now what if the server could render the UI widget instead?
Let’s use an example. Our script loads a widget to show product star-ratings in the page. That means every visitor’s browser is fetching and running this script.
…and perhaps when you mouse-over or tap it, you see detailed ratings and reviews.
This screenshot happens to be for Bazaarvoice Ratings and Reviews but the approach works for many others.
At this point I should point out that not all third party scripts are slow. They all have cost however, and there are things we can do reduce their impact.
Option 1: Bespoke markup & styles
Render those visual elements yourself on the server, then use one of the techniques above to replace it with the real widget.
The result is a perfectly formed UI that only becomes fully interactive when the user chooses to interact.
In the case of our Star-Ratings widget, we would need to:
- Call the ratings API server-side or during the build process.
- Render our own little implementation of the widget markup and style.
- Write a sprinkle of JS to add the widget-script to the page only when needed.
After writing a few of these solutions you will see patterns emerging. You will find that you can re-use your code to awaken more than one script. But watch out for “Other considerations” below.
Option 2: Run the widget script server-side
Try running your widget script server side to pre-render itself. With the right Virtual DOM you may find it works. In which case you can put your feet up. Just be wary of what it adds to the DOM because it may add more script tags or bloated islands of JSON, or render in the wrong place. In which case you’re probably no better off.
Other [equally vital] considerations:
Avoid loading the script repeatedly
This happens where you need to include the third party more than once. The solution rather depends on the specific widget.
For example a YouTube video is added as an iframe. You might choose to add it when the user scrolls down the page. You simply add an iframe each a video is needed. It’s ok to add it each time.
Product Review Stars on the other hand, may rely on one script, but it needs to be triggered once per product. For this you need to keep track of whether the script has already been added to the page. The first time review-stars are activated we will add the script to the page. Subsequent times will not. They each need to be informed when the script has loaded, so they can run an initialisation method.
Testing whether the script is already on the page
This tends to fall into 2 types of test: One to look for the script and another to see if the script is loaded and ready.
- Is the script on the page? Use something like:
if (document.querySelector('script[src="https://path/to/the.js"'))
2. Is the script loaded and ready to use? Use something like the following script. The most universally reusable solution is to use an interval timer, but if you can think of a less dirty way then go for it.
// This helper returns a Promise that will resolve when the script is loaded and ready:async function awaitScriptLoad(isScriptReady) { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { if (isScriptReady()) { // Tidy up & tell the caller it's safe to run the script: clearInterval(intervalId); clearTimeout(timeoutId); resolve(); } }, 250); // Recommended: Define a timeout too: const timeoutId = setTimeout(() => { clearInterval(intervalId); reject(); }, 30000); });}
// This will be your own helper to detect whether script is ready:
// This example tests for the object created by Video.js but some third parties load more scripts that add methods. You may need to detect them too.function isScriptLoadedAndReady() { return 'videojs' in window;}
// Wait for the Promise to resolve:
// Sometimes you may also need to catch a failure and handle it.await awaitScriptLoad(isScriptLoadedAndReady);
// Now do something with the script.
Accessibility
Whatever you develop must be accessible.
Sometimes when you use the technique of server-side rendering your own markup, you can produce a solution that is more accessible than then the third party, at least until it is triggered and replaces your markup.
No-JS
Don’t forget about the user without JS. For everyone this happens during page load, so whatever you produce should look right on first render.
Where possible, provide a <noscript> fallback for your amazing solution.
- Server-side render whatever you can. Prepare the UI as much as possible before it reaches the browser.
- Add third party scripts to the page as late as possible. Maybe never
- Bonus feature: By putting off third party widgets you also exclude their markup until it is absolutely needed. Sometimes that means improved page accessibility for more people. The Lighthouse screenshot above is from a page that fetches 2 fonts and at least four third parties directly, including Lottie animations and videos. All of the third parties make additional requests for more resources but they’ve been postponed until they’re needed. The below-100% scores in “Best Practices” and “SEO” are from a third party cookie banner 🙄, so try as you might, you may never get top scores across the board unless you fix the third parties too!