Optimize loading third-parties
tl;dr: Third-party resources can slow down sites and can be a challenge to optimize. You can follow certain best practices to load or delay different types of third-parties efficiently. You can also use framework-level components such as the Next.js Script component, which provides a template for framing the “when” and “how” for loading third-party scripts. Alternatively, experimental ideas like Partytown may be of interest.
It would be hard to find a modern site that operates in silos. Most sites coexist and depend on several other sources on the web for data, functions, content, and much more. Any resource located on another domain and consumed by your site is a third-party (3P) resource for your site. Typical third-party resources that are included on sites include
- Embeds for maps, videos, social media, and chat services
- Advertisements
- Analytics components and tag managers
- A/B testing and personalization scripts
- Utility libraries that provide ready-to-use helper functions such as those used for data visualization or animations.
- reCAPTCHA or CAPTCHA for bot detection.
You can use third-parties to integrate other features that add value to your content or to reduce some drudgery involved in building a site from scratch. As per the Web Almanac report for 2021, more than 94% of the pages on the web use third-parties - images and JavaScript forming the most significant contributors to third-party content. Below is a helpful breakdown of third-party requests by content type and category:
While third-party resources can enrich your site with valuable features, they can also slow it down if:
- They cause additional round trips to the third-party domain for every required resource.
- They make heavy usage of JavaScript (impacting download and execution time)or are bulky in size because of unoptimized images/videos.
- Individual site owners cannot influence the implementation, and their behavior can be unpredictable.
- They can block the rendering of other critical resources on the page and affect Core Web Vitals (CWV).
Despite these issues, third-parties may be essential to your business. If you cannot do away with 3P’s, the next best thing is to optimize them to reduce performance impact - and that is what we will cover in this section.
We have included some strategies and best practices that apply to different types of third-party scripts. The Next.js Script component incorporates many of these best practices, and you can learn about it in the second half of this post. Let’s first see how you would find out if a third-party script is harming the performance of a page.
Assessing the performance impact of 3P resources
You can use a combination of techniques to find how third-party code is affecting your site.
-
The following Lighthouse audits help identify slow third-party scripts that affect CWV.
- Reduce the impact of third-party code for scripts that block the main thread.
- Reduce JavaScript execution time for scripts that take long to execute
- Avoid enormous network payloads for large scripts
- Use the WebPageTest (WPT) waterfall chart to identify third-party blocking scripts or WPT side-by-side comparison to measure the impact of 3rd party tags.
- Sites like Bundlephobia help to assess the cost of adding available npm packages to your bundles. You can also find size and dependencies included in any package using npm package search.
With that background on identifying problematic third-party code, let’s explore ways to optimize it.
Optimization strategies
Since third-party code is not under your control, you cannot optimize the libraries directly. This leaves you with two options.
-
Replace or remove: If the value provided by the third-party script is not proportional to its performance cost, consider removing it. You can also evaluate other alternatives that are lightweight but provide similar functionality. In this case study, we discuss how we improved the performance of a movies app by switching packages with lighter alternatives and similar features.
-
Optimize the loading sequence: The loading process involves loading several first-party and third-party resources in the browser. To design an optimal loading strategy, you would need to consider the browser assigned priority for different resources, their position on the page, and the value of each resource for the web page. We have proposed an optimal loading sequence for a React/Next.js app. We will now see how this applies to various third-party resources and the steps we can take to load them optimally.
Load 3P scripts efficiently
Following are the time-tested best practices that can reduce the performance impact of third-party resources when used correctly.
Use async or defer to prevent scripts from blocking other content.
Applicable to: Non-critical scripts (tag managers, analytics)
JavaScript download and execution is synchronous by default and can block the HTML parser and DOM construction on the main thread. Use of async or defer attributes in the
<script>
element tells the browser to download scripts asynchronously. You can use these to download any script that is not necessary for the critical rendering path (e.g., the main UI component)
defer
: the script is fetched in parallel as the parser executes, and script execution is delayed till the parsing is complete. Defer should be the default choice for delaying execution until after DOM construction.async
: the script is fetched in parallel while parsing but executed as soon as it is available when it blocks the parser. For module scripts with dependencies, the script and all its dependencies are executed in the defer queue. Useasync
for scripts that need to run earlier in the loading process. For example, you may want to execute specific analytics scripts early without missing any early page-load data.
<script src="https://example.com/deferthis.js" defer></script>
<script src="https://example.com/asyncthis.js" async></script>
Credit: developers.google.com
One caveat worth mentioning here is that async and defer lower the browser assigned priority of the resources causing it to load significantly later. A new feature for priority hints can help to work around this problem.
Establish early connections to required origins using resource hints
Applicable to: Critical scripts, fonts, CSS, images from third-party CDNs
Connecting to third-party origins can be slow due to the DNS lookups, redirects, and multiple round trips that may be required for each third-party server. Resource hints dns-prefetch and preconnect help cut down the time needed for this setup by initiating the connections early in the life cycle.
Including a dns-prefetch resource hint corresponding to a domain will perform the DNS lookup early, thus reducing the latency associated with dns lookups. You can pair this with preconnect for the most critical resources. The preconnect initiates a connection with the third-party domain by performing TCP round trips and handling TLS negotiations in addition to the DNS lookup.
<head>
<link rel="preconnect" href="http://example.com" />
<link rel="dns-prefetch" href="http://example.com" />
</head>
Our post on the ideal loading sequence, provides a list of third-party resources where preconnect should be used.
Benefits of using resource hints are evident in this case study where Andy Davies discusses how using preconnect helped reduce the loading time for the main product image by initiating an early connection to the third-party image CDN.
“The real-world metrics showed a 400ms improvement at the median and greater than 1s improvement at the 95th percentile.”
Similarly, you may use resource hints to optimize loading time for critical third-parties like bot detection (reCaptcha) and consent management.
Lazy load below-the-fold 3P resources
Applicable to: Embeds such as YouTube, Maps, Advertisements and Social media
Third-party embeds like those used for social media feeds, advertisements, YouTube videos, and maps can slow down web pages. However, all such embeds may not be visible to users on page load and may be lazy-loaded as the user scrolls down to them. You can use different lazy-loading methods depending on desired browser support.
- The loading attribute can be used with images and iframes commonly used to load third-party embeds like those for YouTube or Google Maps.
- A custom implementation using the IntersectionObserver API allows you to detect when the observed element enters or exits the browser’s viewport.
- Lazy-sizes - a popular JavaScript library that implements lazy-loading for you.
A variation of lazy-loading embeds uses a static or dynamic facade displayed to users on page loads. Instead of the map embed, you can use a static image of the actual embed to show a specific area on the map embed. Alternatively, you could use a facade that looks like the embed but loads only when the user clicks or interacts with it. Some methods for implementing facades for popular embeds include the Map Static API for maps, Tweetpik for Twitter embeds, lite-youtube-embed for YouTube, React-live-chat-loader for chat widgets. A comprehensive discussion on these techniques is available here.
A few caveats concerning lazy-loading and facades
- The behavior for the YouTube facade is slightly different on iOS and Safari on macOS 11+. Tapping/clicking the first time loads the actual video embed. Users will have to tap again to play the video.
- Lazy loading can lead to layout shifts and affect the user experience if the size of the embed is not specified. To prevent layout shifts, you should specify the size for all lazy-loaded embeds or their container elements.
Self-host 3P scripts to prevent round trips
Applicable to: JavaScript files, fonts
Although preconnect or dns-prefetch allows you to initiate connections to third-party origins early, the connections are still required. Also, with third-party origins, you need to rely on their caching strategy, which may not be optimal.
Self-hosting a copy of the scripts on the same origin offers you more control over the loading and caching process used for the scripts. Self-hosting reduces the time required for DNS lookup and allows you to improve the caching strategy for the scripts using HTTP caching. You can also use HTTP/2 server push to push scripts that you know the user will need. A great example of how self-hosting third-party scripts is Casper.com which improved the start render time for its home page by 1.7 seconds by self-hosting third-party scripts provided by Optimizely.
With self-hosted copies of third-party scripts, you have to ensure that you update your copy regularly based on changes to the original. Without updates, the script may become outdated, missing important fixes or changes corresponding to dependencies. Self-hosting on a server instead of a CDN will also prevent you from taking advantage of edge-caching mechanisms employed by CDNs.
Use service workers to cache scripts where possible
Applicable to: JavaScript files, fonts
Self-hosting may not be an option for scripts that change frequently. You can use service workers to improve caching for such third-party scripts while also utilizing CDN edge caching. This technique gives you better control over the frequency of re-fetches over the network. This technique can be combined with preconnects to further reduce the network cost of the fetch operation. You can also load resources such that requests for non-essential third-party scripts are deferred till the page reaches a key user moment.
Follow the ideal loading sequence
Consider the above guidance for different types of third-parties and their value to the page. Based on the intended use for each resource, you can follow the ideal resource loading sequence to interlace first-party and third-party resources optimally for faster page loads.
Best practices by script type
Some scripts are easier to optimize than others. A discussion with web performance experts on optimizing different third-parties, some of the typical constraints observed, and their wish list for loading third-parties led us to some interesting conclusions. The general consensus was that most users do not interact with a site until a certain threshold of content is visible. Following was the guidance specific to different script types.
Non-critical JavaScript
Most third-parties like chat widgets or analytics scripts are not
critical to the user experience and can be delayed. Using the defer
script attribute is the most common method to delay the loading and execution of these scripts.
Advertising or analytics teams may worry about the impact of deferring scripts on the visibility and advertising revenue of the app. The Telegraph case study is often cited in this context, where deferring all scripts did not skew any analytics or advertising metrics. Instead, the First Ad Loaded metric improved by an average of 4 seconds. Some developers have also designed solutions to delay the load of third-parties till after the page becomes interactive.
Bot detection/ReCaptcha
Since you would want to prevent bots from accessing web forms, developers would usually load these scripts as early as possible. However, ReCaptcha has a sizable JS payload and main thread footprint, so there is motivation to defer loading it until required. Few methods to optimize this script are
- Load it only on a few pages with form inputs from the user that may get spammed by a bot.
- Lazy load the script when the user interacts with form elements, for example, on form focus.
- Use resource hints to establish early connections when you need the script to execute on page load.
Google Tag Manager (GTM)
Large sites often provide Google Tag Manager access to marketing teams or agencies. This allows them to add new marketing tags to all pages on the site for better tracking. Performance is not a primary concern for the marketing team, and all of them may not know that inconsiderately adding tags can slow down the site. Optimization of GTM scripts is more about controlling who accesses GTM and monitoring the changes that they make.
You can start by ensuring that the site owners own the account rather than an external agency. This allows you to define granular access permissions for who can add, edit and publish tags. Better collaboration between development and marketing departments can be set up to audit new tags and remove unused tags.
Your site may not need GTM on all pages. (for example, there is no reason for the marketing team to track events on the checkout page of an e-commerce site). Pages should be audited individually so that unnecessary GTM inclusions can be removed. Sites that use cookie banners can also opt not to load GTM if the user declines cookies. Finally, if you must load GTM on a page, you can defer the scripts to fire after loading the main content.
Another optimization that applies to older third-party script tags is related to document.write(). Injecting scripts using document.write() is unsafe and may result in warnings or errors based on the browser and type of script. Some third-party scripts still use this method. GTM provides a configuration in its Custom HTML tag creation interface called Support document.write(). If this is enabled, Google Tag Manager temporarily replaces the default document.write() function with a secure version of its own.
A/B Testing and Personalization
Sites conduct A/B tests to check which version of the webpage performs better. One of the two variants of the page is loaded for different users in the identified sample of users. A/B tests can significantly affect the performance of pages on which they are run, with each test adding as much as 1 sec to the loading time. Currently, many A/B tests are sourced externally through third-parties, and developers have little control over the JavaScript code executed to alter the UI for these tests.
Site personalization is a related concept which involves running scripts to provide a tailored experience for different users based on known data. These scripts are again heavy and difficult to optimize. Like A/B testing scripts, personalization scripts also need to run early as the rendered UI depends on the script’s output. Developing a custom server-based solution for A/B testing and personalization is the ideal method to optimize A/B testing.
However, it may not always be feasible.
To optimize third-party A/B testing scripts, you can limit the number of users who receive the script. The script identifies which version to display based on heuristics and enables the correct version for the user. This can slow down the page for all users. Google’s Optimize allows the configuration of rules for targeting users. Many of these rules can be evaluated on Google servers, so that performance impact is low for users who are not targeted.
YouTube and map embeds
These embeds are heavy, and developers must explore lazy-loading or click-to-load patterns to load the embeds to optimize them. Use of solutions like lite-youtube-embed is encouraged while noting that double-tap/click is required in iOS/macOS-Safari to play the video using this facade.
Social media embeds
Some social media embeds provide an option to lazy-load their scripts (e.g., data-lazy in Facebook embeds). You can explore this to improve performance. Another alternative is to use image facades created manually or using tools like tweetpik.
Out-of-the-box optimization
To optimize third-parties, development teams should understand the nuances of resource hints, lazy loading, HTTP caching, and service workers and then implement these in their solutions. Some frameworks and libraries have encapsulated these best practices in a way that developers can easily use.
Partytown created by Builder.io is an experimental library that helps run resource-intensive scripts on a web worker instead of the main thread. Their philosophy is that the main thread should be dedicated to your code, and any scripts that are not required by the critical path can be sandboxed and isolated to a web worker. Partytown allows you to configure access to the main thread APIs such as cookies, localStorage, userAgent, etc. API calls may also be logged with arguments to get a better insight into what the scripts do.
JavaScript proxies and a service worker handle communication between the web worker and the main thread. Partytown scripts must be self-hosted on the same server as the HTML documents. It may be used with React or Next.js apps or even without any framework. Each third-party script that can execute in a web server should set the type attribute of its opening script tag to text/partytown as follows.
<script type="text/partytown">// Third-party analytics scripts</script>
The library also provides a React Partytown component that you can directly include in your React or Next.js projects. It can be included in the document <head>
as shown below for a Next.js document.
import { Partytown } from '@builder.io/partytown/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<Partytown />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Partytown also includes React components for common analytics libraries such as Google Tag Manager. The following example shows how you can add this to your React/Next.js projects.
import { Partytown, GoogleTagManager, GoogleTagManagerNoScript } from '@builder.io/partytown/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<GoogleTagManager containerId={'GTM-XXXXX'} />
<Partytown />
</Head>
<body>
<GoogleTagManagerNoScript containerId={'GTM-XXXXX'} />
<Main />
<NextScript />
</body>
</Html>
);
}
Next.js itself provides out-of-the-box optimization for third-party scripts through its Script component. Let’s see how this allows us to improve loading performance for different third-parties.
Next.js Script
component
Next.js 11 was released in mid-2021 with components based on the Conformance methodology introduced by Google’s Aurora team. Conformance is a system that provides well-designed solutions and rules to support optimal loading and Core Web Vitals. Conformance codifies best practices into rulesets that developers can easily implement. Strong defaults and actionable rules form the basis of this system. They make it easy for developers to do the right thing and prevent anti-patterns from creeping in.
The Next.js Script component uses conformance by providing a customizable template that improves loading performance. The Script component encapsulates the <script>
tag and
allows you to set the loading priority for third-party scripts using the strategy attribute. The strategy attribute can take three values.
- beforeInteractive: Use this for critical scripts that the browser should execute before the page becomes interactive. (e.g., bot detection)
- afterInteractive: Use this for scripts that the browser can run after the page is interactive. (e.g., tag managers) This is the default strategy applied and is equivalent to loading the script with
defer
- lazyOnload: Use this for scripts that can be lazily loaded when the browser is idle.
Setting the strategy helps Next.js automatically apply the optimizations and best practices to load the script while ensuring the best loading sequence. You can use the script tag with the strategy attribute, as shown below. Unlike the native HTML script tag, you must not place the next/script tag in the next/head
component or pages/document.js
.
Before:
import Head from "next/head";
export default function Home() {
return (
<>
<Head>
<script async src="https://example.com/samplescript.js" />
</Head>
</>
);
}
After:
// pages/index.js
// default strategy afterinteractive will apply when strategy not specified.
import Script from 'next/script'
<br>
export default function Home() {
return (
<>
<Script src="https://example.com/samplescript.js" />
</>
)
}
The Script component allows you to address many of the use cases discussed earlier. You can use it to load third-party scripts for analytics, social media, utility libraries, and others. The following examples demonstrate how you can apply the above strategies to different types of third-party scripts.
Load polyfills early
In situations where you want specific polyfills that apply to core content to be loaded early, you can use the beforeInteractive strategy to load the polyfill as shown in the following example from Next.js docs.
import Script from "next/script";
export default function Home() {
return (
<>
<Script
src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserverEntry%2CIntersectionObserver"
strategy="beforeInteractive"
/>
</>
);
}
Lazy-load social media embeds
Social media embeds, especially those not visible on page load, can be delayed or lazy-loaded as the user scrolls to them or during periods of inactivity. You can use the lazyonload
strategy as shown in the following snippet.
import Script from "next/script";
export default function Home() {
return (
<>
<Script
src="https://connect.facebook.net/en_US/sdk.js"
strategy="lazyOnload"
/>
</>
);
}
Execute code conditionally on load
There may be some code that needs to execute after a specific third-party has been loaded. This can be specified in the onload attribute of the script component. For example, the following snippet shows how to include code that will execute based on users’ consent.
<Script
src={url} // consent management
strategy="beforeInteractive"
onLoad={() => {
// If loaded successfully, then you can load other scripts in sequence
}}
/>
Using inline scripts within the script tag
Inline scripts that need to execute based on load of a third-party component may also be included in the Script component as shown here.
import Script from 'next/script'
<Script id="show-banner" strategy="lazyOnload">
{`document.getElementById('banner').removeClass('hidden')`}
</Script>
// or
<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').removeClass('hidden')`
}}
/>
Here the inline script is used to change the visibility of a third-party banner ad after it is lazy-loaded. Note that inline scripts may also be included using the dangerouslySetInnerHTML attribute.
Forward attributes to third-party scripts
You can set specific attribute values that can be used by the third-party script in the Script component. The following example shows how two such attributes are passed to an analytics script.
import Script from "next/script";
export default function Home() {
return (
<>
<Script
src="https://www.google-analytics.com/analytics.js"
id="analytics"
nonce="XUENAJFW"
data-test="analytics"
/>
</>
);
}
Load analytics scripts
There are different ways to include analytics on your site, using Google Analytics(GA) and Google Tag Manager(GTM). You can use the Script component to load gtag.js or analytics.js scripts optimally on your Next.js site. Depending on where you want to execute these scripts, you can load them in _app.js (applicable to all the pages) or on specific pages.
GTM may be enabled for all the pages on the site by including the script component inside _app.js as follows:
import Script from "next/script";
// + other imports
function MyApp({ Component, pageProps }) {
// Other app code
return (
<>
{/* Google Tag Manager - Global base code */}
<Script
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer', '${GTM_ID}');
`,
}}
/>
<Component {...pageProps} />
</>
);
}
export default MyApp;
Instead, if you want to load analytics.js on specific pages, you can include it on the page as shown.
import Script from "next/script";
//other imports
const Home = () => {
return (
<div class="container">
<Script id="google-analytics" strategy="afterInteractive">
{`
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
`}
</Script>
</div>
//Other UI related HTML
);
};
import Script from "next/script";
//other imports
const Home = () => {
return (
<div class="container">
<Script id="google-analytics" strategy="afterInteractive">
{`
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
`}
</Script>
</div>
//Other UI related HTML
);
};
export default Home;
Note that in both examples above, the analytics scripts are loaded with strategy = afterInteractive.
Conclusion
When composing your web pages combining resources from your servers with those from other corners of the web, you must monitor the interplay between these resources frequently. You could start by sequencing the resources correctly and following best practices. You can also rely on frameworks or solutions that have built-in these best practices into their design.
As the site grows, performance reporting and regular audits can help eliminate redundancies and optimize scripts that affect performance. Lastly, we can always hope that third-parties with commonly known performance issues will optimize code at their end or expose APIs that enable workarounds to address these issues.