Third-party scripts impact performance, which is why it's important to audit them regularly and use efficient techniques for loading them. This codelab shows you how to optimize the loading of third-party resources. It covers the following techniques:
Deferring script loading
Lazy loading non-critical resources
Preconnecting to required origins
The included sample app features a simple web page with three features coming from third-party sources:
A video embed
A data-visualization library for rendering a line graph
A social media sharing widget
You'll start by measuring the performance of the app and then apply each technique to improve different aspects of app performance.
Measure performance
First open the sample app in the fullscreen view:
- Click Remix to Edit to make the project editable.
- To preview the site, press View App. Then press Fullscreen .
Run a Lighthouse performance audit on the page to establish baseline performance:
- Press `Control+Shift+J` (or `Command+Option+J` on Mac) to open DevTools.
- Click the Lighthouse tab.
- Click Mobile.
- Select the Performance checkbox. (You can clear the rest of the checkboxes in the Audits section.)
- Click Simulated Fast 3G, 4x CPU Slowdown.
- Select the Clear Storage checkbox.
- Click Run audits.
When you run an audit on your machine, the exact results may vary, but you should notice that the First Contentful Paint (FCP) time is pretty high, and that Lighthouse suggests two opportunities to investigate: Eliminate render-blocking resources and Preconnect to required origins. (Even if the metrics are all in the green, optimizations will still yield improvements.)
Defer third-party JavaScript
The Eliminate render-blocking resources audit identified that you can save some time by deferring a script coming from d3js.org:
D3.js is a JavaScript library for creating data visualizations. The script.js
file in the sample app uses D3 utility functions to create the SVG line chart and append it to the page. The order of operations here matters: script.js
has to run after the document is parsed and the D3 library has loaded, which is why it's included right before the closing </body>
tag in index.html
.
However, the D3 script is included in the page's <head>
, which blocks the parsing of the rest document:
Two magic attributes can unblock the parser when added to the script tag:
async
ensures that scripts download in the background and execute at the first opportunity after they finish downloading.defer
ensures that scripts download in the background and execute after parsing is completely finished.
Since this chart is not really critical to the overall page and will most likely be below the fold, use defer
to make sure there's no parser blocking.
Step 1: Load the script asynchronously with the defer
attribute
On line 17 in index.html
, add the defer
attribute to the <script>
element:
<script src="https://d3js.org/d3.v3.min.js" defer></script>
Step 2: Ensure the correct order of operations
Now that D3 is deferred, script.js
will run before D3 is ready, resulting in an error.
Scripts with the defer
attribute execute in the order in which they were specified. To ensure script.js
gets executed after D3 is ready, add defer
to it and move it up to the <head>
of the document, right after the D3 <script>
element. Now it no longer blocks the parser, and the download starts sooner.
<script src="https://d3js.org/d3.v3.min.js" defer></script>
<script src="./script.js" defer></script>
Lazy-load third-party resources
All resources that are below the fold are good candidates for lazy loading.
The sample app has a YouTube video embedded in an iframe. To check out how many requests the page makes and which come from the embedded YouTube iframe:
- To preview the site, press View App. Then press Fullscreen .
- Press `Control+Shift+J` (or `Command+Option+J` on Mac) to open DevTools.
- Click the Network tab.
- Select the Disable cache checkbox.
- Select Fast 3G in the Throttling dropdown menu.
- Reload the page.
The Network panel reveals that the page made a total of 28 requests and transferred almost 1 MB of compressed resources.
To identify the requests that the YouTube iframe
made, look for the video ID 6lfaiXM6waw
in the Initiator column. To group together all the requests by domain:
In the Network panel, right-click a column title.
In the dropdown menu, select the Domains column.
To sort the requests by domain, click the Domains column title.
The new sorting reveals that there are additional requests to Google domains. In total, the YouTube iframe makes 14 requests for scripts, stylesheets, images, and fonts. But unless users actually scroll down to play the video, they don't really need all those assets.
By waiting to lazy-load the video until a user scrolls down to that section of the page, you cut the number of requests the page initially makes. This approach saves users' data and speeds up the initial load.
One way to implement lazy loading is by using the Intersection Observer, a browser API that notifies you when an element enters or exits the browser's viewport.
Step 1: Prevent video from loading initially
To lazy-load the video iframe, you must first prevent it from loading in the usual way. Do that by replacing the src
attribute with the data-src
attribute to specify the video URL:
<iframe width="560" height="315" data-src="https://www.youtube.com/embed/lS9D6w1GzGY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
data-src
is a data attribute, which allows you to store extra information on standard HTML elements. A data attribute can be named anything, as long as it starts with "data-".
An iframe without a src
simply won't load.
Step 2: Use Intersection Observer to lazy-load the video
To load the video when a user scrolls to it you need to know when that happens. That's where the Intersection Observer API steps in. The Intersection Observer API lets you register a callback function that's executed whenever an element you want to track enters or exits the viewport.
To get started, create a new file and name it lazy-load.js
:
- Click New File and give it a name.
- Click Add This File.
Add the script tag to your document head:
<script src="/lazy-load.js" defer></script>
In lazy-load.js
, create a new IntersectionObserver
and pass it a callback function to run:
// create a new Intersection Observer
let observer = new IntersectionObserver(callback);
Now give observer
a target element to watch (the video iframe in this case) by passing it as an argument in the observe
method:
// the element that you want to watch
const element = document.querySelector('iframe');
// register the element with the observe method
observer.observe(element);
callback
receives a list of IntersectionObserverEntry
objects and the IntersectionObserver
object itself. Each entry contains a target
element and properties that describe its dimensions, position, the time it entered the viewport, and more. One of the properties of IntersectionObserverEntry
is isIntersecting
—a boolean value that equals true
when the element enters the viewport.
In this example, the target
is the iframe
. isIntersecting
equals true
when target
enters the viewport. To see this in action, replace callback
with the following function:
let observer = new IntersectionObserver(callback);
let observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
console.log(entry.target);
console.log(entry.isIntersecting);
});
});
- To preview the site, press View App. Then press Fullscreen .
- Press `Control+Shift+J` (or `Command+Option+J` on Mac) to open DevTools.
- Click the Console tab.
Try scrolling up and down. You should see the value of isIntersecting
change and the target element logged to the console.
To load the video when the user scrolls to its position, use isIntersecting
as a condition to run a loadElement
function, which gets the value from the iframe
element's data-src
and sets it as the iframe
element's src
attribute. That replacement triggers the loading of the video. Then, once the video is loaded, call the unobserve
method on the observer
to stop watching the target element:
let observer = new IntersectionObserver(function (entries, observer) {
entries.forEach(entry => {
console.log(entry.target);
console.log(entry.isIntersecting);
});
});
if (entry.isIntersecting) {
// do this when the element enters the viewport
loadElement(entry.target);
// stop watching
observer.unobserve(entry.target);
}
});
});
function loadElement(element) {
const src = element.getAttribute('data-src');
element.src = src;
}
Step 3: Reevaluate performance
To see how the size and number of resources changed, open the DevTools Network panel and reload the page again. The Network panel reveals that the page made 14 requests and only 260 KB. That's a meaningful improvement!
Now scroll down the page and keep an eye on the Network panel. When you get to the video, you should see the page trigger additional requests.
Preconnect to required origins
You've deferred non-critical JavaScript and lazy-loaded the YouTube requests, so now it's time to optimize the remaining third-party content.
Adding the rel=preconnect
attribute to a link tells the browser to establish a connection to a domain before the request for that resource is made. This attribute is best used on origins that provide resources you are certain the page needs.
The Lighthouse audit you ran in the first step suggested in Preconnect to required origins that you can save around 400 ms by establishing early connections to staticxx.facebook.com and youtube.com:
Since the YouTube video is now lazy-loaded, that leaves only staticxx.facebook.com, the source of the social media sharing widget. Establishing an early connection to this domain is as simple as adding a <link>
tag to the document's <head>
:
<link rel="preconnect" href="https://staticxx.facebook.com">
Reevaluate performance
Here's the state of the page after optimization. Follow the steps from the Measure performance section of the codelab to run another Lighthouse audit.