Fast

index


layout: collection override:tags: [] date: 2018-11-05 pathName: fast

Adaptive serving based on network quality

Loading a website can be a very different experience depending on the network conditions. Everything is usually smooth when you are on a fast network, but when you're on the go with a limited data plan and spotty connection, or stuck with a laptop on slow coffee-shop Wi-Fi, it's a different story.

One way to deal with this is by adapting which assets you're serving to users based on the quality of their connection. This is now possible with the Network Information API which enables web applications to access information about the user's network.

{% BrowserCompat 'api.NetworkInformation' %}

Usage

There are many ways you can use this network information to improve the user experience:

Many applications are already doing something similar. For example, YouTube, Netflix and most other video (or video calling) services automatically adjust the resolution during streaming. When Gmail is loading, it provides users with a link to "load basic HTML (for slow connections)".

{% Img src="image/admin/zSTd1IMrb3UJfefdeRAt.png", alt="A link to load basic HTML version of Gmail when users are on slow connections", width="528", height="236" %}

How it works

The navigator.connection object contains information about a client's connection. Its properties are explained in the table bellow.

Property Explanation
downlink The bandwidth estimate in megabits per second.
effectiveType The effective type of the connection, with possible values 'slow-2g', '2g', '3g', or '4g' (covers 4g and higher). Determined based on the combination of round-trip time and downlink speed. For example, fast downlink combined with high latency will have lower effectiveType due to latency.
onchange An event handler that fires when connection information changes.
rtt The estimated round-trip latency of the connection in milliseconds.
saveData A boolean that defines whether the user has requested a reduced data usage mode.

Here's what this looks like when you run it in the browser's console:

{% Img src="image/admin/ayW8uxhh3S6I4MTipr0m.jpg", alt="Chrome DevTools console displaying the values of navigator.connection object's properties", width="800", height="523" %}

The effectiveType values are also available via Client Hints and allow you to communicate the browser's connection type to servers.

{% Aside %} You can access Network Information API inside Service Workers to adapt to situations when users are offline. {% endAside %}

The onchange event listener enables you to dynamically adapt to changes in network quality. If you deferred uploads or downloads because of poor network conditions, you can rely on the event listener to restart the transfer when it detects better network conditions. You can also use it to notify users when the network quality changes. For example, if they lost their Wi-Fi signal and were dropped to a cellular network this can prevent accidental data transfers (and charges 💸).

Use the onchange event listener as you would any other event listener:

navigator.connection.addEventListener('change', doSomethingOnChange);

{% Aside %} Network information API is supported in Chromium-based browsers since version 62. {% endAside %}

Conclusion

The potential benefits of the Network Information API are big, especially for users on slow networks and applications that require a lot of bandwidth. Best of all, it can be used as a progressive enhancement technique.

Apply instant loading with the PRPL pattern

PRPL is an acronym that describes a pattern used to make web pages load and become interactive, faster:

In this guide, learn how each of these techniques fit together but still can be used independently to achieve performance results.

Audit your page with Lighthouse

Run Lighthouse to identify opportunities for improvement aligned with the PRPL techniques:

{% Instruction 'devtools-lighthouse', 'ol' %}

  1. Select the Performance and Progressive Web App checkboxes.
  2. Click Run Audits to generate a report.

For more information, see Discover performance opportunities with Lighthouse.

Preload critical resources

Lighthouse shows the following failed audit if a certain resource is parsed and fetched late:

{% Img src="image/admin/tgcMfl3HJLmdoERFn7Ji.png", alt="Lighthouse: Preload key requests audit", width="745", height="97" %}

Preload is a declarative fetch request that tells the browser to request a resource as soon as possible. Preload critical resources by adding a <link> tag with rel="preload" to the head of your HTML document:

The browser sets a more appropriate priority level for the resource in order to try to download it sooner while not delaying the window.onload event.

For more information about preloading critical resources, refer to the Preload critical assets guide.

Render the initial route as soon as possible

Lighthouse provides a warning if there are resources that delay First Paint, the moment when your site renders pixels to the screen:

{% Img src="image/admin/gvj0jlCYbMdpLNtHu0Ji.png", alt="Lighthouse: Eliminate render-blocking resources audit", width="800", height="111" %}

To improve First Paint, Lighthouse recommends inlining critical JavaScript and deferring the rest using async, as well as inlining critical CSS used above-the-fold. This improves performance by eliminating round-trips to the server to fetch render-blocking assets. However, inline code is harder to maintain from a development perspective and cannot be cached separately by the browser.

Another approach to improve First Paint is to server-side render the initial HTML of your page. This displays content immediately to the user while scripts are still being fetched, parsed, and executed. However, this can increase the payload of the HTML file significantly, which can harm Time to Interactive, or the time it takes for your application to become interactive and can respond to user input.

There is no single correct solution to reduce the First Paint in your application, and you should only consider inlining styles and server-side rendering if the benefits outweigh the tradeoffs for your application. You can learn more about both of these concepts with the following resources.

{% Img src="image/admin/xv1f7ZLKeBZD83Wcw6pd.png", alt="Requests/responses with service worker", width="800", height="1224" %}

Pre-cache assets

By acting as a proxy, service workers can fetch assets directly from the cache rather than the server on repeat visits. This not only allows users to use your application when they are offline, but also results in faster page load times on repeat visits.

Use a third-party library to simplify the process of generating a service worker unless you have more complex caching requirements than what a library can provide. For example, Workbox provides a collection of tools that allow you to create and maintain a service worker to cache assets. For more information on service workers and offline reliability, refer to the service worker guide in the reliability learning path.

Lazy load

Lighthouse displays a failed audit if you send too much data over the network.

{% Img src="image/admin/Ml4hOCqfD4kGWfuKYVTN.png", alt="Lighthouse: Has enormous network payloads audit", width="800", height="99" %}

This includes all asset types, but large JavaScript payloads are especially costly due to the time it takes the browser to parse and compile them. Lighthouse also provides a warning for this when appropriate.

{% Img src="image/admin/aKDCV8qv3nuTVFt0Txyj.png", alt="Lighthouse: JavaScript boot-up time audit", width="797", height="100" %}

To send a smaller JavaScript payload that contains only the code needed when a user initially loads your application, split the entire bundle and lazy load chunks on demand.

Once you've managed to split your bundle, preload the chunks that are more important (see the Preload critical assets guide). Preloading ensures more important resources are fetched and downloaded sooner by the browser.

Aside from splitting and loading different JavaScript chunks on demand, Lighthouse also provides an audit for lazy loading non-critical images.

{% Img src="image/admin/sEgLhoYadRCtKFCYVM1d.png", alt="Lighthouse: Defer offscreen images audit", width="800", height="90" %}

If you load many images on your web page, defer all that are below the fold, or outside the device viewport, when a page is loaded (see Use lazysizes to lazyload images).

Next Steps

Now that you understand some of the basic concepts behind the PRPL pattern, continue to the next guide in this section to learn more. It's important to remember that not all of the techniques need to be applied together. Any efforts made with any of the following will provide noticeable performance improvements.

You can read up more about PRPL patterns.

Avoid invisible text during font loading

Fonts are often large files that take awhile to load. To deal with this, some browsers hide text until the font loads (the "flash of invisible text"). If you're optimizing for performance, you'll want to avoid the "flash of invisible text" and show content to users immediately using a system font (the "flash of unstyled text").

Display text immediately

This guide outlines two ways to achieve this: the first approach is very simple but does not have universal browser support; the second approach is more work but has full browser support. The best choice for you is the one that you'll actually implement and maintain.

Option #1: Use font-display

Before After
@font-face { font-family: Helvetica; } @font-face { font-family: Helvetica; font-display: swap; }

font-display is an API for specifying font display strategy. swap tells the browser that text using this font should be displayed immediately using a system font. Once the custom font is ready, the system font is swapped out.

{% BrowserCompat 'css.at-rules.font-face.font-display' %}

If a browser does not support font-display, the browser continues to follow its default behavior for loading fonts.

These are the default font-loading behaviors for common browsers:

Browser Default behavior if font is not ready…
Edge Uses a system font until font is ready. Swaps out font.
Chrome Will hide text for up to 3 seconds. If text is still not ready, uses a system font until font is ready. Swaps out font.
Firefox Will hide text for up to 3 seconds. If text is still not ready, uses a system font until font is ready. Swaps out font.
Safari Hides text until font is ready.

Option #2: Wait to use custom fonts until they are loaded

With a bit more work, the same behavior can be implemented to work across all browsers.

There are three parts to this approach:

Here are the changes you can expect to make to implement this:

{% Aside 'codelab' %} Use Font Face Observer to display text immediately. {% endAside %}

Verify

Run Lighthouse to verify the site is using font-display: swap to display text:

{% Instruction 'audit-performance', 'ol' %}

Confirm that the Ensure text remains visible during webfont load audit is passing.

Browser-level image lazy loading for the web

Browser-level support for lazy loading images is now supported on the web! This video shows a demo of the feature:

You can use the loading attribute to lazy-load images without the need to write custom lazy loading code or use a separate JavaScript library. Let's dive into the details.

Browser compatibility

{% BrowserCompat 'html.elements.img.loading' %}

Browsers that do not support the loading attribute simply ignore it without side effects.

Why browser-level lazy loading?

According to the HTTP Archive, images are the most requested asset type for most websites and usually take up more bandwidth than any other resource. At the 90th percentile, sites send over 5 MB of images on desktop and mobile. That's a lot of cat pictures.

Previously, there were two ways to defer the loading of off-screen images:

Either option can let developers include lazy loading functionality, and many developers have built third-party libraries to provide abstractions that are even easier to use. With lazy loading supported directly by the browser, however, there's no need for an external library. Browser-level lazymloading also ensures that deferred loading of images still works even if JavaScript is disabled on the client.

The loading attribute

Chrome loads images at different priorities depending on where they're located with respect to the device viewport. Images below the viewport are loaded with a lower priority, but they're still fetched as the page loads.

You can use the loading attribute to completely defer the loading of offscreen images that are reached by scrolling:

<img src="image.png" loading="lazy" alt="…" width="200" height="200">

Here are the supported values for the loading attribute:

{% Aside 'caution' %} Images that are highly likely to be in-viewport, and in particular LCP images, should not be lazy-loaded. {% endAside %}

Relationship between the loading attribute and fetch priority

The eager value is simply an instruction to load the image as usual, without delaying the load further if it is off-screen. It does not imply that the image is loaded any quicker than another image without the loading="eager" attribute.

Browsers prioritize resources based on various heuristics, and the loading attribute just states when the image resource is queued, not how it is prioritized in that queue. eager just implies the usual eager queueing browsers use by default.

If you want to increase the fetch priority of an important image (for example the LCP image), then Fetch Priority should be used with fetchpriority="high".

Note that an image with loading="lazy" and fetchpriority="high" will still be delayed while it is off-screen, and then fetched with a high priority when it is nearly within the viewport. It would likely be fetched with a high priority in this case anyway, so this combination should not really be needed nor used.

Distance-from-viewport thresholds

All images that are above the fold—that is, immediately viewable without scrolling—load normally. Those that are far below the device viewport are only fetched when the user scrolls near them.

Chromium's implementation of lazy loading tries to ensure that offscreen images are loaded early enough so that they have finished loading once the user scrolls near to them. By fetching nearby images well before they become visible in the viewport, we maximize the chance they are already loaded by the time they become visible.

Compared to JavaScript lazy loading libraries, the thresholds for fetching images that scroll into view may be considered conservative.

{% Aside %} Experiments conducted using Chrome on Android suggest that on 4G, 97.5% of below-the-fold images that are lazy-loaded were fully loaded within 10ms of becoming visible. Even on slow 2G networks, 92.6% of below-the-fold images were fully loaded within 10ms. This means browser-level lazy loading offers a stable experience regarding the visibility of elements that are scrolled into view. {% endAside %}

The distance threshold is not fixed and varies depending on several factors:

You can find the default values for the different effective connection types in the Chromium source. These numbers, and even the approach of fetching only when a certain distance from the viewport is reached, may change in the future as the Chrome team improves heuristics to determine when to begin loading.

{% Aside %} You can experiment with these different thresholds by throttling the network in DevTools. {% endAside %}

Improved data-savings and distance-from-viewport thresholds

As of July 2020, Chrome has made significant improvements to align the image lazy loading distance-from-viewport thresholds to better meet developer expectations.

On fast connections (4G), we reduced Chrome's distance-from-viewport thresholds from 3000px to 1250px and on slower connections (3G or lower), changed the threshold from 4000px to 2500px. This change achieves two things:

You can find a comparison between the old vs. new distance-from-viewport thresholds for one of our demos on a fast connection (4G) below:

Old thresholds. vs new thresholds:

{% Img src="image/admin/xSZMqpbioBRwRTnenK8f.png", alt="The new and improved thresholds for image lazy loading, reducing the distance-from-viewport thresholds for fast connections from 3000px down to 1250px", width="800", height="460" %}

and the new thresholds vs. LazySizes (a popular JS lazy loading library):

{% Img src="image/admin/oHMFvflk9aesT7r0iJbx.png", alt="The new distance-from-viewport thresholds in Chrome loading 90KB of images compared to LazySizes loading in 70KB under the same network conditions", width="800", height="355" %}

We are committed to working with the web standards community to explore better alignment in how distance-from-viewport thresholds are approached across different browsers.

Images should include dimension attributes

While the browser loads an image, it does not immediately know the image's dimensions, unless these are explicitly specified. To enable the browser to reserve sufficient space on a page for images, it is recommended that all <img> tags include both width and height attributes. Without dimensions specified, layout shifts can occur, which are more noticeable on pages that take some time to load.

<img src="image.png" loading="lazy" alt="…" width="200" height="200">

Alternatively, specify their values directly in an inline style:

<img src="image.png" loading="lazy" alt="…" style="height:200px; width:200px;">

The best practice of setting dimensions applies to <img> tags regardless of whether or not they are being loaded lazily. With lazy loading, this can become more relevant. Setting width and height on images in modern browsers also allows browsers to infer their intrinsic size.

In most scenarios images still lazy-load if dimensions are not included, but there are a few edge cases you should be aware of. Without width and height specified, image dimensions are 0×0 pixels at first. If you have a gallery of such images, the browser may conclude that all of them fit inside the viewport at the start, as each takes up practically no space and no image is pushed offscreen. In this case the browser determines that all of them are visible to the user and decides to load everything.

Also, specifying image dimensions decreases the chances of layout shifts happening. If you are unable to include dimensions for your images, lazy loading them can be a trade-off between saving network resources and potentially being more at risk of layout shift.

While lazy loading in Chromium is implemented in a way such that images are likely to be loaded once they are visible, there is still a small chance that they might not be loaded yet. In this case, missing width and height attributes on such images increase their impact on Cumulative Layout Shift.

{% Aside %} Take a look at this demo to see how the loading attribute works with 100 pictures. {% endAside %}

Images that are defined using the <picture> element can also be lazy-loaded:

<picture>
  <source media="(min-width: 800px)" srcset="large.jpg 1x, larger.jpg 2x">
  <img src="photo.jpg" loading="lazy">
</picture>

Although a browser will decide which image to load from any of the <source> elements, the loading attribute only needs to be included to the fallback <img> element.

Avoid lazy loading images that are in the first visible viewport

You should avoid setting loading=lazy for any images that are in the first visible viewport. This is particularly relevant for LCP images. See the article The performance effects of too much lazy-loading for more information.

Generally, any images within the viewport should be loaded eagerly using the browser's defaults. You do not need to specify loading=eager for this to be the case for in-viewport images.

<!-- visible in the viewport -->
<img src="product-1.jpg" alt="..." width="200" height="200">
<img src="product-2.jpg" alt="..." width="200" height="200">
<img src="product-3.jpg" alt="..." width="200" height="200">

<!-- offscreen images -->
<img src="product-4.jpg" loading="lazy" alt="..." width="200" height="200">
<img src="product-5.jpg" loading="lazy" alt="..." width="200" height="200">
<img src="product-6.jpg" loading="lazy" alt="..." width="200" height="200">

Graceful degradation

Browsers that do not support the loading attribute will ignore its presence. While these browsers will of course not get the benefits of lazy loading, including the attribute has no negative impact on them.

FAQ

Are there plans to automatically lazy-load images in Chrome?

Previously, Chromium automatically lazy-loaded any images that were well suited to being deferred if Lite mode was enabled on Chrome for Android and the loading attribute was either not provided or set as loading="auto". However, Lite mode has been deprecated (as was the non-standard loading="auto") and there are currently no plans to provide automatically lazy-load of images in Chrome.

Can I change how close an image needs to be before a load is triggered?

These values are hardcoded and can't be changed through the API. However, they may change in the future as browsers experiment with different threshold distances and variables.

Can CSS background images take advantage of the loading attribute?

No, it can currently only be used with <img> tags.

Is there a downside to lazy loading images that are within the device viewport?

It is safer to avoid putting loading=lazy on above-the-fold images, as Chrome won't preload loading=lazy images in the preload scanner and will also delay fetching such images until all layout is complete. See Avoid lazy-loading images that are in the first visible viewport for more information.

How does the loading attribute work with images that are in the viewport but not immediately visible (for example: behind a carousel, or hidden by CSS for certain screen sizes)?

Using loading="lazy" may prevent them being loaded when they are not visible but within the calculated-distance. For example, Chrome, Safari and Firefox do not load images using display: none; styling—either on the image element or on a parent element. However, other techniques to hide images—such as using opacity:0 styling—will still result in the images being loaded. Always test your implementation thoroughly to ensure it's acting as intended.

What if I'm already using a third-party library or a script to lazy-load images?

With full support of native lazy loading now available in modern browsers, you may wish to reconsider if you still need a third-party library or script to lazy-load images.

One reason to continue to use a third-party library along with loading="lazy" is to provide a polyfill for browsers that do not support the attribute, or to have more control over when lazy loading is triggered.

How do I handle browsers that don't support lazy loading?

Create a polyfill or use a third-party library to lazy-load images on your site. The loading property can be used to detect if the feature is supported in the browser:

if ('loading' in HTMLImageElement.prototype) {
  // supported in browser
} else {
  // fetch polyfill/third-party library
}

For example, lazysizes is a popular JavaScript lazy loading library. You can detect support for the loading attribute to load lazysizes as a fallback library only when loading isn't supported. This works as follows:

<!-- Let's load this in-viewport image normally -->
<img src="hero.jpg" alt="…">

<!-- Let's lazy-load the rest of these images -->
<img data-src="unicorn.jpg" alt="…" loading="lazy" class="lazyload">
<img data-src="cats.jpg" alt="…" loading="lazy" class="lazyload">
<img data-src="dogs.jpg" alt="…" loading="lazy" class="lazyload">

<script>
  if ('loading' in HTMLImageElement.prototype) {
    const images = document.querySelectorAll('img[loading="lazy"]');
    images.forEach(img => {
      img.src = img.dataset.src;
    });
  } else {
    // Dynamically import the LazySizes library
    const script = document.createElement('script');
    script.src =
      'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.1.2/lazysizes.min.js';
    document.body.appendChild(script);
  }
</script>

Here's a demo of this pattern. Try it out in an older browser to see the fallback in action.

{% Aside %} The lazysizes library also provides a loading plugin that uses browser-level lazy loading when available but falls back to the library's custom functionality when needed. {% endAside %}

Is lazy loading for iframes also supported in browsers?

{% BrowserCompat 'html.elements.iframe.loading' %}

<iframe loading=lazy> has also been standardized and is already implemented in Chromium and Safari. This allows you to lazy-load iframes using the loading attribute. See this dedicated article about iframe lazy-loading for more information.

How does browser-level lazy loading affect advertisements on a web page?

All ads displayed to the user in the form of an image or iframe lazy-load just like any other image or iframe.

How are images handled when a web page is printed?

All images and iframes are immediately loaded if the page is printed. See issue #875403 for details.

Does Lighthouse recognize browser-level lazy loading?

Lighthouse 6.0 and above factor in approaches for offscreen image lazy loading that may use different thresholds, allowing them to pass the Defer offscreen images audit.

Conclusion

Baking in support for lazy loading images can make it significantly easier for you to improve the performance of your web pages.

Are you noticing any unusual behavior with this feature enabled in Chrome? File a bug!

Choose the right image format

The very first question you should ask yourself is whether an image is, in fact, required to achieve the effect you are after. Good design is simple and will also always yield the best performance. If you can eliminate an image resource, which often requires a large number of bytes relative to HTML, CSS, JavaScript and other assets on the page, then that is always the best optimization strategy. That said, a well-placed image can also communicate more information than a thousand words, so it is up to you to find that balance.

Next, you should consider if there is an alternative technology that could deliver the desired results, but in a more efficient manner:

If you ever find yourself encoding text in an image asset, stop and reconsider. Great typography is critical to good design, branding, and readability, but text-in-images delivers a poor user experience: the text is not selectable, not searchable, not zoomable, not accessible, and not friendly for high-DPI devices. The use of web fonts requires its own set of optimizations, but it addresses all of these concerns and is always a better choice for displaying text.

Choose the right image format

If you are sure an image is the correct option, you should carefully select the right kind of image for the job.

{% Img src="image/admin/dJuB2DQcbhtwD5VdPVlR.png", alt="Zoomed-in vector and raster images", width="585", height="313" %}
Zoomed-in vector image (L) raster image (R)

Each format has its own set of pros and cons. Vector formats are ideally suited for images that consist of simple geometric shapes such as logos, text, or icons. They deliver sharp results at every resolution and zoom setting, which makes them an ideal format for high-resolution screens and assets that need to be displayed at varying sizes.

However, vector formats fall short when the scene is complicated (for example, a photo): the amount of SVG markup to describe all the shapes can be prohibitively high and the output may still not look "photorealistic". When that's the case, that's when you should be using a raster image format such as PNG, JPEG, WebP, or AVIF.

Raster images do not have the same nice properties of being resolution or zoom independent —when you scale up a raster image you'll see jagged and blurry graphics. As a result, you may need to save multiple versions of a raster image at various resolutions to deliver the optimal experience to your users.

Implications of high-resolution screens

There are two different kinds of pixels: CSS pixels and device pixels. A single CSS pixel may correspond directly to a single device pixel, or may be backed by multiple device pixels. What's the point? Well, the more device pixels there are, the finer the detail of the displayed content on the screen.

{% Img src="image/admin/oQV7qJ9fUMkYsKlUMrL4.png", alt="Three images showing the difference between CSS pixels and device pixels.", width="470", height="205" %}
The difference between CSS pixels and device pixels.

High DPI (HiDPI) screens produce beautiful results, but there is one obvious tradeoff: image assets require more detail to take advantage of the higher device pixel counts. The good news is, vector images are ideally suited for this task, as they can be rendered at any resolution with sharp results— you might incur a higher processing cost to render the finer detail, but the underlying asset is the same and is resolution independent.

On the other hand, raster images pose a much larger challenge because they encode image data on a per-pixel basis. Hence, the larger the number of pixels, the larger the filesize of a raster image. As an example, let's consider the difference between a photo asset displayed at 100x100 (CSS) pixels:

Screen resolution Total pixels Uncompressed filesize (4 bytes per pixel)
1x 100 x 100 = 10,000 40,000 bytes
2x 100 x 100 x 4 = 40,000 160,000 bytes
3x 100 x 100 x 9 = 90,000 360,000 bytes

When we double the resolution of the physical screen, the total number of pixels increases by a factor of four: double the number of horizontal pixels, times double the number of vertical pixels. Hence, a "2x" screen not just doubles, but quadruples the number of required pixels!

So, what does this mean in practice? High-resolution screens enable you to deliver beautiful images, which can be a great product feature. However, high-resolution screens also require high-resolution images, therefore:

Features of different raster image formats

In addition to different lossy and lossless compression algorithms, different image formats support different features such as animation and transparency (alpha) channels. As a result, the choice of the "right format" for a particular image is a combination of desired visual results and functional requirements.

Format Transparency Animation Browser
PNG Yes No All
JPEG No No All
WebP Yes Yes All modern browsers. See Can I use?
AVIF Yes Yes No. See Can I use?

There are two universally supported raster image formats: PNG and JPEG. In addition to these formats, modern browsers support the newer format WebP, while only some support the newer AVIF format. Both of the newer formats offer better overall compression and more features. So, which format should you use?

WebP and AVIF will generally provide better compression than older formats, and should be used where possible. You can use WebP or AVIF images along with a JPEG or PNG image as a fallback. See Use WebP images for more details.

In terms of older image formats, consider the following:

  1. Do you need animation? Use <video> elements.
    • What about GIF? GIF limits the color palette to at most 256 colors, and creates significantly larger file sizes than <video> elements. See Replace animated GIFs with video.
  2. Do you need to preserve fine detail with highest resolution? Use PNG or lossless WebP.
    • PNG does not apply any lossy compression algorithms beyond the choice of the size of the color palette. As a result, it will produce the highest quality image, but at a cost of significantly higher filesize than other formats. Use judiciously.
    • WebP has a lossless encoding mode that may be more efficient than PNG.
    • If the image asset contains imagery composed of geometric shapes, consider converting it to a vector (SVG) format!
    • If the image asset contains text, stop and reconsider. Text in images is not selectable, searchable, or "zoomable". If you need to convey a custom look (for branding or other reasons), use a web font instead.
  3. Are you optimizing a photo, screenshot, or a similar image asset? Use JPEG, lossy WebP, or lossy AVIF.
    • JPEG uses a combination of lossy and lossless optimization to reduce filesize of the image asset. Try several JPEG quality levels to find the best quality versus filesize tradeoff for your asset.
    • Lossy WebP or lossy AVIF may be acceptable JPEG alternatives, but be aware that WebP's lossy mode in particular discards some chroma information to achieve smaller images. This means that select colors may not be the same as an equivalent JPEG.

Finally, note that if you are using a WebView to render content in your platform-specific application, then you have full control of the client and can use WebP exclusively! Facebook and many others use WebP to deliver all of their images within their applications— the savings are definitely worth it.

Impact on Largest Contentful Paint (LCP)

Images may be LCP candidates. That means the size of an image affects its load time. When an image is an LCP candidate, efficiently encoding that image is crucial to improving LCP.

You should strive to apply the advice given in this article so that the perceptual performance of a page is as fast as it can possibly be for all users. LCP is part of perceptual performance, as it measures how fast the largest (and therefore most perceivable) element on the page displays.

Using the Chrome UX Report to look at performance in the field

The Chrome UX Report (informally known as CrUX) is a public dataset of real user experience data on millions of websites. Unlike lab data, CrUX data actually comes from opted-in users in the field. It measures the Core Web Vitals metrics, including Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS), as well as diagnostic metrics like Time to First Byte (TTFB) and First Contentful Paint (FCP).

The dataset also contains qualitative dimensions about the user experience, for example, the device and connection types, which enables drilling down into user experiences grouped by similar technologies. See the CrUX documentation for the full list of metrics.

Using this data, developers are able to understand the wide distribution of real world user experiences between websites, segments of the web, or the web as a whole. This is a big deal! The Chrome UX Report dataset is the first of its kind to enable web developers to compare their real user performance against the competition and industry.

How to use it

There are four primary ways to extract insights from the Chrome UX Report, ranging in complexity. For quick and easy analysis of website performance, the CrUX Dashboard and PageSpeed Insights are the recommended tools. BigQuery trades some of the simplicity of the analysis for the power of customization and more granular insights. And the API enables the integration of high-level data with other applications, and the History API provides historical data for trend analysis.

CrUX Dashboard

The CrUX Dashboard is a customizable data visualization tool of websites' historical performance built on Looker Studio. The data is sourced from the BigQuery dataset and all of the SQL queries are handled for you under the hood. The dashboard shows the distribution of user experiences, as captured by key performance metrics, and how it changes over time. It also shows how the distributions of qualitative metrics like device type and effective connection type change over time. Try the Looker Studio Dashboard guide.

PageSpeed Insights

PageSpeed Insights (PSI) shows the most recent performance distributions broken down by desktop and mobile users. Performance data is available for individual web pages (in addition to entire origins) and is aggregated for the most recent 28 days of data (as opposed to the previous calendar month on BigQuery). Using this tool is as easy as entering a URL or origin in the search box on the web interface, and the field performance data is displayed alongside prescriptive suggestions to optimize the page. Try the PageSpeed Insights guide.

CrUX on BigQuery

The CrUX database on BigQuery, part of the Google Cloud Platform (GCP) with a web and command line interface, hosts the raw data that aggregates key UX performance metrics for top origins on the web. New tables are periodically added to the database covering the previous calendar month. Developers can handcraft queries to mine the dataset for specific insights. BigQuery requires knowledge of SQL and a GCP project with billing enabled to run the queries. This is an especially useful tool for power users who require low-level access to the data to create custom reports, benchmarks, and reports about the state of the web. Try the BigQuery guide.

CrUX API

The CrUX API is a free and RESTful interface for looking up origin or URL-level user experience data. The data is updated daily and aggregates the previous 28 days of data, similar to PageSpeed Insights. You can use this API to build your own applications on top of the real-user experience data in CrUX. Try the CrUX API guide.

CrUX History API

The CrUX History API is a free and RESTful interface, similar to the daily API, for looking up origin or URL-level user experience data. The data is updated weekly and aggregates the previous 28 days of data, over a period of 6 months. You can use this API to build your own applications on top of the real-user experience data in CrUX. Try the CrUX History API guide.

How to get help

If you need any kind of support, there are a few channels to reach someone who can help. The CrUX Google Group is a public forum for users of the dataset to ask questions and share analyses. There is also a CrUX tag for Stack Overflow if you need programming help with SQL or API access. And finally, @ChromeUXReport is the Twitter account you can follow to ask questions and listen for product announcements.

See it in action

To get more acquainted with the available data, walk through step-by-step guides for using Looker Studio Dashboard, PageSpeed Insights, Bigquery, and both the daily and history API:

Adapt video to image serving based on network quality

{% include 'content/devtools-headsup.njk' %}

In this codelab, you'll learn how to adapt your content based on the network quality. This page's background video will load only when users are on a fast network. On slower networks, an image will load instead.

The Network Information API enables you to access information about the user's connection quality. You will use its effectiveType property to decide when to serve a video and when to serve an image. effectiveType can be 'slow-2g', '2g', '3g', or '4g'.

{% BrowserCompat 'api.NetworkInformation' %}

Step 1: Check connection type

The index.html file contains a <video> tag to display the background video (line 22). The code in script.js loads the video by setting the video tag's src attribute. (The video loading code is described in more detail in Step 2.)

To load the video conditionally, first check if the Network Information API is available; if it is, check the connection type.

  1. In script.js, add an if statement that tests whether the navigator.connection object exists and whether it has the effectiveType property.
  2. Add an if statement to check the effectiveType of the network.
if (navigator.connection && !!navigator.connection.effectiveType) {
  if (navigator.connection.effectiveType === '4g') {
    // Only load video on the fastest connections.
  } else {
    // In any other case load the image.
  }
}

Wrap the existing video loading code in an else statement, so that video will still load in browsers that don't support the Network Information API.

if (navigator.connection && !!navigator.connection.effectiveType) {
  if (navigator.connection.effectiveType === '4g') {
    // video loading code
  } else {
    // image loading code
  }
} else {
  const video = document.getElementById('coverVideo');
  const videoSource = video.getAttribute('data-src');
  video.setAttribute('src', videoSource);

  video.setAttribute('style', 'height: 100%; width: 100%; display:inline');
}

Step 2: Load video

If the effectiveType is '4g', use the video loading code from the beginning of the codelab.

if (navigator.connection.effectiveType === '4g') {
  const video = document.getElementById('coverVideo');
  const videoSource = video.getAttribute('data-src');
  video.setAttribute('src', videoSource);
  video.setAttribute('style', 'height: 100%; width: 100%; display:inline');
} else {
  // image loading code
}

Here's how the video loading code works: the <video> tag doesn't download or display anything at first because its src attribute is not set. The video URL to load is specified using the data-src attribute.

<video id="coverVideo" autoplay loop muted data-src="https://cdn.glitch.com/b6491350-b058-4eb6-aa6c-55c93122073e%2FMatrix%2C%20Console%2C%20Hacking%2C%20Code.mp4?1551464245607"></video>

Data attributes allow you to store extra information on standard HTML elements. A data element can be named anything, as long as it starts with "data-".

To actually display the video on the page, you need to get the value from data-src and set it as the video element's src attribute.

First, get the DOM element that contains the asset:

const video = document.getElementById('coverVideo');

Then get the resource location from the data-src attribute:

const videoSource = video.getAttribute('data-src');

And finally set that as the src attribute of the video element:

video.setAttribute('src', videoSource);

The last line takes care of CSS positioning:

video.setAttribute('style', 'height: 100%; width: 100%; display:inline');

Step 3: Load image

To conditionally load an image on slower networks, use the same strategy as for the video.

Add an image element to index.html (right after the video element), and use the data-src attribute instead of the src attribute.

<img id="coverImage" data-src="https://cdn.glitch.com/36529535-5976-40f8-979b-40c898b86bd0%2F36529535-5976-40f8-979b-40c898b86bd0_1_Sn80dgiwpMjBVrqjfiDbnA.jpg?1553003835358" />

In script.js, add code to set the image's src attribute depending on the effectiveType of the network.

if (navigator.connection.effectiveType === '4g') {
  const video = document.getElementById('coverVideo');
  const videoSource = video.getAttribute('data-src');
  video.setAttribute('src', videoSource);

  video.setAttribute('style', 'height: 100%; width: 100%; display:inline');
} else {
  const image = document.getElementById('coverImage');
  const imageSource = image.getAttribute('data-src');
  image.setAttribute('src', imageSource);

  image.setAttribute('style', 'height: 100%; width: 100%; display:inline');
}

Try it out

To test it yourself:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %}

  1. Click the Throttling drop-down, which is set to No throttling by default. Select Fast 3G.

{% Img src="image/admin/um0BRQOmsqGgZzWeLDHY.png", alt="DevTools Network tab with Fast 3G throttling option highlighted", width="723", height="198" %}

Now reload the page with Fast 3G still enabled. The app loads an image in the background instead of the video:

{% Img src="image/admin/wtQs6oCU8c5q23SQOnHo.png", alt="Matrix-like image background with 'NETWORK INFORMATION' text overlay", width="800", height="456" %}

Extra Credit: Respond to changes

Remember how this API has an onchange event listener? You can use it for many things: dynamically adapting content such as video quality; restarting deferred data transfers when a change to a high-bandwidth network type is detected; or notifying users when the network quality changes.

Here's a simple example of how to use this listener. Add it to script.js. This code will call the displayNetworkInfo function whenever the network information changes.

navigator.connection.addEventListener('change', displayNetworkInfo);

There's already an empty <h2> element on the index.html page. Now define the displayNetworkInfo function so that it displays the network information in the <h2> element and invoke the function.

function displayNetworkInfo() {
  document.getElementById('connection').innerHTML = navigator.connection.effectiveType;
}

displayNetworkInfo();

Here's the final state of the app on Glitch.

{% Img src="image/admin/0cTTeeAMgl5lD1PKRjy2.png", alt="Matrix-like video background with 'NETWORK INFORMATION 4g' text overlay", width="800", height="447" %}

To test it again:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %}

  1. Click the Throttling drop-down, which is set to No throttling by default. Select Fast 3G. {% Instruction 'reload-app', 'ol' %}

The app will update the network information to 3g:

{% Img src="image/admin/WRp4ceBiuDQZoWmuaZx4.png", alt="Matrix-like video background with 'NETWORK INFORMATION 3g' text overlay", width="800", height="447" %}

Art direction

{% Instruction 'preview' %}

What's going on here?

Art direction shows different images on different display sizes.

A responsive image loads different sizes of the same image. Art direction takes this a step further and loads completely different images depending on the display.

Use art direction to improve visual presentation. For example, the different image croppings in this demo ensure that the point of interest (the flower) is always shown in detail, regardless of the display. Art direction's benefits are purely aesthetic; it provides no performance benefit over responsive images.

View the code

How the code works

Art direction uses the <picture>, <source>, and <img> tags.

picture

The <picture> tag provides a wrapper for zero or more <source> tags and one <image> tag.

source

The <source> tag specifies a media resource.

The browser uses the first <source> tag with a media query that returns true. If none of the media queries match, the browser falls back to loading the image specified by the <img>. tag.

{% Aside 'gotchas' %}

img

The <img> tag makes this code work on browsers that don't support the <picture> tag.

If a browser does not support the <picture> tag, it loads the image specified by the <img> tag.

{% Aside 'gotchas' %}

Avoid flash of invisible text

This code lab shows you how to display text immediately using Font Face Observer.

Add Font Face Observer

Font Face Observer is a script that detects when a font loads. The fontfaceobserver.js file has already been saved to the project directory, so you don't need to add it separately. However, you do need to add a script tag for it.

{% Instruction 'remix' %}

  <div class="text">Some text.</div>
  <script src="fontfaceobserver.js" type="text/javascript"></script>
</body>

Use Font Face Observer

Create Observers

Create an observer for each font family that it is used on the page.

  <script src="fontfaceobserver.js" type="text/javascript"></script>
  <script type="text/javascript">
    const pacificoObserver = new FontFaceObserver('Pacifico');
    const robotoObserver = new FontFaceObserver('Roboto');
  </script>
<body>

If you're ever unsure what font face observers you need to create, just look for the font-family declarations in your CSS. Pass the font-family name of these declarations to FontFaceObserver(). There is no need to create a font observer for fallback fonts.

For example, if your CSS was:

font-family: "Times New Roman", Times, serif;

you would add FontFaceObserver('Times New Roman'). Times and serif are fallback fonts, so you would not need to declare FontFaceObservers for them.

Detect font load

The code for detecting a font load looks like this:

robotoObserver.load().then(function(){
  console.log("Hooray! Font loaded.");
});

robotoObserver.load() is a promise that resolves when the font loads.

The demo site uses two different fonts, so you need to use Promise.all() to wait until both fonts have loaded.

Promise.all([
  pacificoObserver.load(),
  robotoObserver.load()
]).then(function(){
  /* Do things */
});

✔️Check-in

Your script should now look like this:

<script type="text/javascript">
const pacificoObserver = new FontFaceObserver('Pacifico');
const robotoObserver = new FontFaceObserver('Roboto');

Promise.all([
  pacificoObserver.load(),
  robotoObserver.load()
]).then(function(){
  /* Do things */
});
</script>

Apply fonts-loaded class

document.documentElement.classList.add("fonts-loaded");

This adds the fonts-loaded class to the document's root element (the <html> tag) once both fonts have loaded.

✔️Check-in

Your completed script should look like this:

<script type="text/javascript">
  const pacificoObserver = new FontFaceObserver('Pacifico');
  const robotoObserver = new FontFaceObserver('Roboto');

  Promise.all([
    pacificoObserver.load(),
    robotoObserver.load()
  ]).then(function(){
    document.documentElement.className += " fonts-loaded";
  });
</script>

Update CSS

Your page should be styled to use a system font initially and custom fonts once the fonts-loaded class has been applied.

.header {
html.fonts-loaded .header {
  font-family: 'Pacifico', cursive;
}

.text
html.fonts-loaded .text {
  font-family: 'Roboto', sans-serif;
}

Verify

{% Instruction 'preview' %}

If the page looks like this, then you've successfully implemented Font Face Observer and gotten rid of the "Flash of Invisible Text."

{% Img src="image/admin/st2aEbPzhPIwyJiobkgO.png", alt="A heading in a cursive font.", width="572", height="246" %}

Reduce JavaScript payloads with code splitting

{% include 'content/devtools-headsup.njk' %}

Most web pages and applications are made up of many different parts. Instead of sending all the JavaScript that makes up the application as soon as the first page is loaded, splitting the JavaScript into multiple chunks improves page performance.

This codelab shows how to use code splitting to improve the performance of a simple application that sorts three numbers.

{% Img src="image/admin/1lb0XbGP6M4eShkmYaQW.png", alt="A browser window shows an application titled Magic Sorter with three fields for inputting numbers and a sort button.", width="800", height="504" %}

Measure

{% Aside %} Since webpack is used in this application, any changes made to the code will trigger a new build which can take a few seconds. Once it completes, you should see your changes reflected in the application. {% endAside %}

Like always, it's important to first measure how well a website performs before attempting to add any optimizations.

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %} {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/pyzLwutSzSx2qztQMXTM.png", alt="Network panel showing 71.2 KB JavaScript bundle.", width="800", height="153" %}

71.2 KB worth of JavaScript just to sort a few numbers in a simple application. What gives?

In the source code (src/index.js), the lodash library is imported and used in this application. Lodash provides many useful utility functions, but only a single method from the package is being used here. Installing and importing entire third-party dependencies where only a small portion of it is being utilized is a common mistake.

Optimize

There are a few ways the bundle size can be trimmed:

  1. Write a custom sorting method instead of importing a third-party library
  2. Use the built in Array.prototype.sort() method to sort numerically
  3. Only import the sortBy method from lodash and not the entire library
  4. Download the code for sorting only when the user clicks the button

Options 1 and 2 are perfectly appropriate methods to reduce the bundle size (and would probably make the most sense for a real application). However, those are not used in this tutorial for the sake of teaching 😈.

{% Aside %} The concept of removing unused code is explored in further detail in a separate guide. {% endAside %}

Both options 3 and 4 help improve the performance of this application. The next few sections of this codelab cover these steps. Like any coding tutorial, always try to write the code yourself instead of copy and pasting.

Only import what you need

A few files need to be modified to only import the single method from lodash. To begin with, replace this dependency in package.json:

"lodash": "^4.7.0",

with this:

"lodash.sortby": "^4.7.0",

Now in src/index.js, import this specific module:

import "./style.css";
import _ from "lodash";
import sortBy from "lodash.sortby";

And update how the values are sorted::

form.addEventListener("submit", e => {
  e.preventDefault();
  const values = [input1.valueAsNumber, input2.valueAsNumber, input3.valueAsNumber];
  const sortedValues = _.sortBy(values);
  const sortedValues = sortBy(values);

  results.innerHTML = `
    <h2>
      ${sortedValues}
    </h2>
  `
});

Reload the application, open DevTools, and take a look at the Network panel once again.

{% Img src="image/admin/J8c1PhqMDOJKjoenzMCN.png", alt="Network panel showing 15.2 KB JavaScript bundle.", width="800", height="148" %}

For this application, the bundle size was reduced by over 4X with very little work, but there's still more room for improvement.

Code splitting

webpack is one of the most popular open-source module bundlers used today. In short, it bundles all JavaScript modules (as well as other assets) that make up a web application into static files that can be read by the browser.

The single bundle used in this application can be split into two separate chunks:

With the use of dynamic imports, a secondary chunk can be lazy loaded, or loaded on demand. In this application, the code that makes up the chunk can be loaded only when the user presses the button.

Begin by removing the top-level import for the sort method in src/index.js:

import sortBy from "lodash.sortby";

And import it within the event listener that fires when the button is pressed:

form.addEventListener("submit", e => {
  e.preventDefault();
  import('lodash.sortby')
    .then(module => module.default)
    .then(sortInput())
    .catch(err => { alert(err) });
});

The import() feature is part of a proposal (currently at stage 3 of the TC39 process) to include the capability to dynamically import a module. webpack has already included support for this and follows the same syntax laid out by the proposal.

{% Aside %} Read more about how dynamic imports work in this Web Updates article. {% endAside %}

import() returns a promise and when it resolves, the selected module is provided which is split out into a separate chunk. After the module is returned, module.default is used to reference the default export provided by lodash. The promise is chained with another .then that calls a sortInput method to sort the three input values. At the end of the promise chain, .catch() is used to handle cases where the promise is rejected due to an error.

{% Aside 'caution' %} In a production application, always handle dynamic import errors appropriately. A simple alert message similar to what is used here may not provide the best user experience to let the user know something has failed. {% endAside %}

{% Aside %} You may see a linting error that says:

Parsing error: 'import' and 'export' may only appear at the top level.

This is due to the fact that the dynamic import syntax is still in the proposal stage and has not been finalized. Although webpack already supports it, the settings for ESLint (a JavaScript linting utility) used by Glitch has not been updated to include this syntax yet, but it still works! {% endAside %}

The last thing that needs to be done is to write the sortInput method at the end of the file. This needs to be a function that returns a function that takes in the imported method from lodash.sortBy. The nested function can then sort the three input values and update the DOM.

const sortInput = () => {
  return (sortBy) => {
    const values = [
      input1.valueAsNumber,
      input2.valueAsNumber,
      input3.valueAsNumber
    ];
    const sortedValues = sortBy(values);

    results.innerHTML = `
      <h2>
        ${sortedValues}
      </h2>
    `
  };
}

Monitor

Reload the application one last time and keep a close eye on the Network panel again. Only a small initial bundle is downloaded as soon as the app loads.

{% Img src="image/admin/f1QZcSozkaA1rj52YWGV.png", alt="Network panel showing 2.7 KB JavaScript bundle.", width="800", height="151" %}

After the button is pressed to sort the input numbers, the chunk that contains the sorting code gets fetched and executed.

{% Img src="image/admin/LPNj3JpAmzsGppwJl5fs.png", alt="Network panel showing 2.7 KB JavaScript bundle followed by a 13.9 KB JavaScript bundle.", width="800", height="211" %}

Notice how the numbers still get sorted!

Conclusion

Code splitting and lazy loading can be extremely useful techniques to trim down the initial bundle size of your application, and this can directly result in much faster page load times. However, there are some important things that need to be considered before including this optimization in your application.

Lazy loading UI

When lazy loading specific modules of code, it's important to consider how the experience would be for users with weaker network connections. Splitting and loading a very large chunk of code when a user submits an action can make it seem like the application may have stopped working, so consider showing a loading indicator of some sort.

Lazy loading third-party node modules

It is not always the best approach to lazy load third-party dependencies in your application and it depends on where you use them. Usually, third party dependencies are split into a separate vendor bundle that can be cached since they don't update as often. Read more about how the SplitChunksPlugin can help you do this.

Lazy loading with a JavaScript framework

Although it is useful to understand how dynamic imports work, always use the method recommended by your framework/library to lazy load specific modules.

Preloading and prefetching

Where possible, take advantage of browser hints such as <link rel="preload"> or <link rel="prefetch"> to try and load critical modules even sooner. webpack supports both hints through the use of magic comments in import statements. This is explained in more detail in the Preload critical chunks guide.

Lazy loading more than code

Images can make up a significant part of an application. Lazy loading those that are below the fold, or outside the device viewport, can speed up a website. Read more about this in the Lazysizes guide.

Use density descriptors

{% Instruction 'preview' %}

You can use the device emulator for this. If you're looking for specific display densities, here are some devices to try:

1x density Blackberry Playbook, many external monitors
2x density iPad or IPhone 5/6
3x density Galaxy S5 or iPhone X

How does it work?

The concept of density descriptors may be unfamiliar to most folks. To better understand them, it helps to have a bit of background on how the browser works with pixels.

What are pixels

Let's start at the very beginning by defining what a pixel is. This sounds simple, but "pixel" can actually have many meanings:

Device pixel (a.k.a. "physical pixel")
The smallest dot of color that can be displayed on a device.
Logical pixel
Information that specifies the color at a particular location on a grid. This type of pixel has no inherent physical size.
CSS pixel
The CSS spec defines a pixel as a unit of physical measurement. 1 pixel = 1/96th of an inch.

Pixel Density

Pixel density (also referred to as "screen density" or "display density") measures the density of device pixels in a given physical area. This is commonly measured using pixels per inch (ppi).

For many years, 96 ppi was a very common display density (hence CSS defining a pixel as 1/96th of an inch). Starting in the 1980s it was the default resolution of Windows. In addition, it was the resolution of CRT monitors.

This began to change as LED monitors became common in the 2000s. In particular, Apple made a big splash in 2010 when it introduced Retina displays. These displays had a minimum resolution of 192 ppi, which was twice the resolution of "regular" displays (192 ppi/96 ppi = 2).

window.devicePixelRatio

With the introduction of newer display technology, "device pixels" began to vary in physical size and shape and were no longer the same size as "CSS pixels". The need to define the relationship between the size of "device pixels" and "CSS pixels" is what led to the introduction of the devicePixelRatio (sometimes called the "CSS Pixel Ratio").

devicePixelRatio defines the relationship between device pixels and CSS pixels for a particular device. A 192 ppi device has a devicePixelRatio of 2 (192 ppi/96 ppi = 2) because "2 of its display pixels are the size of 1 CSS pixel".

These days most devices have a device-pixel-ratio between 1.0 and 4.0.

{% Aside %} This ratio doesn't have to be a whole number. 1.5, 2.4, and 2.5 are all device-pixel-ratios of common devices. {% endAside %}

So how do you actually apply this information?

Size images based on device-pixel-ratios

In order for images to look their very best on high resolution screens, it's necessary to provide different image versions for different devicePixelRatios.

Device Pixel Ratio Indicates that: On this device, an <img> tag with a CSS width of 250 pixels, will look best when the source image is...
1 1 device pixel = 1 CSS pixel 250 pixels wide
2 2 device pixels = 1 CSS pixel 500 pixels wide
3 3 device pixels = 1 CSS pixel 750 pixels wide

Things to note:

{% Aside %} Tools like sharp make it easy to create multiple sizes of an image. This is covered in more detail here. {% endAside %}

Use Density Descriptors to serve multiple
images

Density descriptors, in conjunction with the "srcset " attribute, can be used to serve different images to different devicePixelRatios.

<img src="flower.jpg"
  srcset="flower-1x.jpg 1x,
          flower-2x.jpg 2x,
          flower-3x.jpg 3x">

This example put into words:

How to use this:

Extract and inline critical CSS with Critical

{% include 'content/devtools-headsup.njk' %}

Whether you use a UI library or handcraft your styles, shipping a significant amount of CSS delays rendering because the browser must download and parse CSS files before it can show the page.

This responsive ice cream gallery is built with Bootstrap. UI libraries like Bootstrap speed up development, but that often comes at the expense of bloated and unnecessary CSS, which can slow down your load times. Bootstrap 4 is 187 KB, while Semantic UI, another UI library, is a whopping 730 KB uncompressed. Even when minified and gzipped, Bootstrap still weighs around 20 KB, well over the 14 KB threshold for the first roundtrip.

Critical is a tool that extracts, minifies and inlines above-the-fold CSS. This allows above-the-fold content to be rendered as soon as possible, even if CSS for other parts of the page has not yet loaded. In this codelab, you'll learn how to use Critical's npm module.

Measure

{% Instruction 'preview' %}

To run a Lighthouse audit on this site:

{% Instruction 'devtools-lighthouse', 'ol' %}

  1. Click Mobile.
  2. Select the Performance checkbox.
  3. Clear the rest of the checkboxes in the Audits section.
  4. Click Simulated Fast 3G, 4x CPU Slowdown.
  5. Select the Clear Storage checkbox. With this option selected, Lighthouse will not load resources from the cache, which simulates how first-time visitors would experience the page.
  6. Click Run Audits.

{% Img src="image/admin/bk0impRv9Mo0yDpbuIQi.png", alt="Audits panel of Chrome DevTools, powered by Lighthouse", width="800", height="1068" %}

When you run an audit on your machine, the exact results may vary, but in the filmstrip view, you'll notice the app has a blank screen for quite a while before finally rendering the content. This is why First Contentful Paint (FCP) is high and why overall performance score is not great.

{% Img src="image/admin/otfxnEjhjP9cNpSF8W4o.png", alt="Lighthouse audit showing performance score of 84, FCP 3 seconds and a filmstrip view of loading the app", width="710", height="356" %}

Lighthouse is here to help you fix performance issues, so look for solutions in the Opportunities section. Eliminate render-blocking resources is listed as an opportunity and that's where Critical shines!

{% Img src="image/admin/L0i6jb8G1eT0vr2V45vH.png", alt="Lighthouse audit 'Opportunities' section listing 'Eliminate render-blocking resources'", width="743", height="449" %}

Optimize

{% Instruction 'remix' %}

To speed things up, Critical is already installed and an empty configuration file is included in the codelab.

In the configuration file critical.js, add a reference to Critical and then invoke the critical.generate() function. This function accepts an object containing the configuration.

const critical = require('critical');

critical.generate({
	// configuration
},(err, output) => {
  if (err) {
    console.error(err);
  } else if (output) {
    console.log('Generated critical CSS');
  }
});

Error handling isn't mandatory, but it's an easy way to gauge the operation success in the console.

Configure Critical

The table below contains some useful Critical options. You can check out all of the available options on GitHub.

base string The base directory for your files.
src string HTML source file.
dest string The target for the output file. If CSS is inlined the output file is HTML. If not, the output is a CSS file.
width, height numbers Viewport width and height in pixels.
dimensions array Contains objects with width and height properties. These objects represent the viewports you want to target with above-the-fold CSS. If you have media queries in your CSS, this allows you to generate critical CSS that covers multiple viewport sizes.
inline boolean When set to true, the generated critical CSS is inlined in the of the HTML source file.
minify boolean When set to true, Critical minifies the generated critical CSS. Can be omitted when extracting critical CSS for multiple resolutions because Critical automatically minifies it to avoid duplicate rule inclusion.

Below is a configuration example for extracting critical CSS for multiple resolutions. Add it to critical.js or play around and tweak the options.

critical.generate({
  base: 'public/',
  src: './index.html',
  dest: './index.html',
  inline: true,
  dimensions: [
    {
      height: 500,
      width: 300,
    },
    {
      height: 720,
      width: 1280,
    },
  ]
}, (err, output) => {
  if (err) {
    console.error(err);
  } else if (output) {
    console.log('Generated critical CSS');
  }
});

In this example, index.html is both the source file and the destination file because the inline option is set to true. Critical first reads the HTML source file, extracts critical CSS and then overwrites index.html with critical CSS inlined in the <head>.

dimensions array has two viewport sizes specified: 300 x 500 for extra small screens and 1280 x 720 for standard laptop screens.

minify option is omitted because Critical automatically minifies the extracted CSS when there are multiple viewport sizes specified.

Run Critical

Add Critical to your scripts in package.json:

scripts: {
  "start": "node server.js",
  "critical": "node critical.js"
}

{% Instruction 'console', 'ol' %}

To generate critical CSS, in the console, run:

npm run critical
refresh
{% Img src="image/admin/alfJqwxI2WU3bUOT4xUZ.png", alt="Success message saying 'Generated critical CSS' in the console]()", width="243", height="45", class="fixed-width-img" %}
Success message in the console

{% Aside 'note' %} Glitch console and editor don't automatically sync, so refresh command is neccessary to update the editor with files generated from the console. {% endAside %}

Now in the <head> tag of index.html, generated critical CSS is inlined between <style> tags, followed by a script that loads the rest of the CSS asynchronously.

{% Img src="image/admin/NEQAkJJDV712y944PAxo.png", alt="index.html with inlined critical CSS", width="800", height="325" %}
Inlined critical CSS

Measure again

Follow the steps from the beginning of the codelab to run Lighthouse performance audit again. The results you get will look similar to this:

{% Img src="image/admin/7be1SdC4JpavF7338VuT.png", alt="Lighthouse audit showing performance score of 100, FCP 0.9 seconds and improved filmstrip view of loading the app", width="716", height="407" %}

{% Aside 'success' %} The filmstrip view shows that content is rendered much sooner and this is reflected in improved paint metrics. And "Eliminate render-blocking resources" has been eliminated! 🎉 {% endAside %}

Using Imagemin with Grunt

This Glitch already contains grunt, grunt-cli, and the grunt-contrib-imagemin plugin. To add the configuration for Imagemin, you'll need to edit your gruntfile.js file.

{% Instruction 'remix' %}

imagemin: {
  dynamic: {
    files: [{
      cwd: 'images/',
      expand: true,
      src: ['**/*.{png,jpg}'],
    }]
  }
}

This code block tells Grunt which files should be compressed with Imagemin.

{% Aside %} Have more questions about this? You can read more about the Grunt file object here. {% endAside %}

grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('default', [/* list plugins here */]);
grunt.registerTask('default', ['imagemin']);

✔︎ Check-in

The complete gruntfile.js file should now look like this:

const grunt = require('grunt')
grunt.initConfig({
  imagemin: {
    dynamic: {
      files: [{
        cwd: 'images/'
        expand: true,
        src: ['**/*.{png,jpg}'],
      }]
    }
  }
});
grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('default', ['imagemin']);

Customize your Imagemin Configuration

imagemin-pngquant is a plugin for specifying compression quality levels. We've already added imagemin-pngquant to this project in the package.json file so that you can use it to compress your PNGs. To use it, declare the plugin and specify a compression quality level in your Gruntfile.

const pngquant = require('imagemin-pngquant');
const grunt = require('grunt')
grunt.initConfig({
  ...
grunt.initConfig({
  imagemin: {
    options: {
      use: [
        pngquant({quality: [0.5, 0.5]}),
      ]
    },
    dynamic: {
  ...

This code tells Imagemin to compress PNGs using the Pngquant plugin. The quality field uses a min and max range of values to determine the compression level—0 is the lowest and 1 is the highest. To force all images to be compressed at 50% quality, pass 0.5 as both the min and max value.

✔︎ Check-in

Your gruntfile.js file should now look like this:

const pngquant = require('imagemin-pngquant');
const grunt = require('grunt')
grunt.initConfig({
  imagemin: {
    options: {
      use: [
        pngquant({quality: [0.5, 0.5]}),
      ]
    },
    dynamic: {
      files: [{
        cwd: 'images/',
        expand: true,
        src: ['**/*.{png,jpg}'],
      }]
    }
  }
});
grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('default', ['imagemin']);

But what about JPEGs? The project also has JPEG images, so you should specify how they are compressed as well.

Customize your Imagemin configuration (continued)

Use the imagemin-mozjpeg plugin, which has already been installed for you, to compress JPEG images.

const mozjpeg = require('imagemin-mozjpeg');
const pngquant = require('imagemin-pngquant');
const grunt = require('grunt');
grunt.initConfig({
  imagemin: {
    options: {
      use: [
        pngquant({quality: [0.5, 0.5]}),
        mozjpeg({quality: 50})
      ]
    },
    dynamic: {

✔︎ Check-in

Your gruntfile.js file should now look like this:

const mozjpeg = require('imagemin-mozjpeg');
const pngquant = require('imagemin-pngquant');
const grunt = require('grunt');
grunt.initConfig({
  imagemin: {
    options: {
      use: [
        pngquant({quality: [0.5, 0.5]}),
        mozjpeg({quality: 50})
      ]
    },
    dynamic: {
      files: [{
        cwd: 'images/',
        expand: true,
        src: ['**/*.{png,jpg}'],
      }]
    }
  }
});
grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('default', ['imagemin']);

Run Grunt & verify results with Lighthouse

  1. Click the Tools button.
  2. Then click the Console button.
  3. Run Grunt by typing the following command into the console:
grunt

When Grunt completes you should see a message like this in console:

Minified 6 images (saved 667 kB - 66.5%)

Hooray! These results are much better.

Lastly, it's a good idea to use Lighthouse to verify the changes that you just made. Lighthouse's "Efficiently encode images" performance audit will let you know if the JPEG images on your page are optimally compressed.

{% Instruction 'preview' %}

{% Img src="image/admin/TTrEG19zxZRSWNv2pRG4.png", alt="Passing 'Efficiently encode images' audit in Lighthouse", width="766", height="976", class="screenshot" %}

Success! You have used Imagemin to optimally compress the images on your page.

Using Imagemin with Gulp

This Glitch already contains gulp, gulp-cli, and the gulp-imagemin plugin. To add the configuration for Imagemin, you'll need to edit your gulpfile.js file.

{% Instruction 'remix' %}

const imagemin = require('gulp-imagemin');
gulp.src('images/*')
  .pipe(imagemin())
  .pipe(gulp.dest('images/'))

This code adds a Gulp task that uses Imagemin to compress the images in the images/ directory. The original images are overwritten and saved in the same images/ directory.

✔︎ Check-in

Your gulpfile.js file should now look like this:

const imagemin = require('gulp-imagemin');
const gulp = require('gulp');

gulp.task('default', () => {
  gulp.src('images/*')
    .pipe(imagemin())
    .pipe(gulp.dest('images/'))
});

You now have a gulpfile.js that can be used to compress images.

Run Gulp

  1. Click the Tools button.
  2. Then click the Console button.
  3. Run Gulp to compress your images by typing the following command into the console:
gulp

When Gulp completes, you should see a message like this in the terminal:

gulp-imagemin: Minified 6 images (saved 50 kB—14.8%)

A 15% improvement in file size is a good start; however, more improvements can be made by using different compression settings.

Customize your Imagemin Configuration

imagemin-pngquant is a plugin for specifying compression quality levels. We've already added imagemin-pngquant to this project in the package.json file so that you can use it to compress your PNGs. To use it, declare the plugin and specify a compression quality level in your gulpfile.

const pngquant = require('imagemin-pngquant');
[pngquant({quality: [0.5, 0.5]})]

This code tells Imagemin to compress PNGs using the Pngquant plugin. The quality field uses a min and max range of values to determine the compression level—0 is the lowest and 1 is the highest. To force all images to be compressed at 50% quality, pass 0.5 as both the min and max value.

✔︎ Check-in

Your gulpfile.js file should now look like this:

const pngquant = require('imagemin-pngquant');
const imagemin = require('gulp-imagemin');
const gulp = require('gulp');

gulp.task('default', () => {
  gulp.src('images/*')
    .pipe(imagemin([
      pngquant({quality: [0.5, 0.5]})
    ]))
    .pipe(gulp.dest('images/'))
});

But what about JPGs? The project also has JPG images; these need to be compressed too.

Customize your Imagemin Configuration (continued)

Use the imagemin-mozjpeg plugin, which has already been installed for you, to compress JPG images.

const mozjpeg = require('imagemin-mozjpeg');
[
  pngquant({quality: [0.5, 0.5]}),
  mozjpeg({quality: 50})
]

✔︎ Check-in

Your gulpfile.js file should now look like this:

const mozjpeg = require('imagemin-mozjpeg')
const pngquant = require('imagemin-pngquant');
const imagemin = require('gulp-imagemin');
const gulp = require('gulp');

gulp.task('default', () => {
  gulp.src('images/*')
    .pipe(imagemin([
      pngquant({quality: [0.5, 0.5]}),
      mozjpeg({quality: 50})
    ]))
    .pipe(gulp.dest('images/'))
});

Re-run Gulp & verify results with Lighthouse

gulp

When Gulp completes, you should see a message like this in terminal:

gulp-imagemin: Minified 6 images (saved 667 kB—66.5%)

Hooray! These results are much better.

Lastly, it's a good idea to use Lighthouse to verify the changes that you just made.

Lighthouse's "Efficiently encode images" performance audit can let you know if the JPEG images on your page are optimally compressed.

{% Instruction 'preview' %}

{% Img src="image/admin/vNZSjlHVQpG3R8SmojFi.png", alt="Passing 'Efficiently encode images' audit in Lighthouse", width="766", height="976", class="screenshot" %}

Success! You have used Imagemin to optimally compress the images on your page.

Using Imagemin with webpack

This Glitch already contains webpack, webpack-cli, and imagemin-webpack-plugin. To add the configuration for Imagemin, you'll need to edit your webpack.config.js file.

The existing webpack.config.js for this project has been copying images from the images/ directory to the dist/ directory but it hasn't been compressing them.

{% Instruction 'remix' %}

{% Aside %} Why would you copy images to a new dist/ folder? dist/ is short for "distribution" and it's fairly common practice to keep original code, images, etc. separate from their distributed versions because they may be slightly different. {% endAside %}

const ImageminPlugin = require('imagemin-webpack-plugin').default;
new ImageminPlugin()

{% Aside %} Why add it at the end of the array? Adding it there ensures that Imagemin runs last, after all the other plugins. {% endAside %}

✔︎ Check-in

Your complete webpack.config.js file should now look like this:

const ImageminPlugin = require('imagemin-webpack-plugin').default;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
     new CopyWebpackPlugin([{
       from: 'img/**/**',
       to: path.resolve(__dirname, 'dist')
     }]),
     new ImageminPlugin()
  ]
}

You now have a webpack config that compresses images using Imagemin.

Run webpack

{% Instruction 'console' %}

  1. To compress your images, run webpack by typing the following command into the console:
webpack --config webpack.config.js --mode development

But what happens if you run webpack in production mode?

webpack --config webpack.config.js --mode production

This time around, webpack displays a warning letting you know that your PNG files, in spite of some compression, still exceed the recommended size limit. (webpack's development & production modes prioritize different things, which is why you only see this warning while running webpack in production mode.)

Customize our Imagemin configuration to fix this warning.

Customize your Imagemin Configuration

Add settings for compressing PNG images by passing the following object to ImageminPlugin():

{pngquant: ({quality: [0.5, 0.5]})}

This code tells Imagemin to compress PNGs using the Pngquant plugin. The quality field uses a min and max range of values to determine the compression level—0 is the lowest and 1 is the highest. To force all images to be compressed at 50% quality, pass 0.5 as both the min and max value.

✔︎ Check-in

Your webpack.config.js file should now look like this:

const ImageminPlugin = require('imagemin-webpack-plugin').default;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new CopyWebpackPlugin([{
		from: 'img/**/**',
    	to: path.resolve(__dirname, 'dist')
    }]),
    new ImageminPlugin({
      pngquant: ({quality: [0.5, 0.5]}),
	  })
  ]
}

But what about JPEGs? The project also has JPEG images, so you should specify how they are compressed as well.

Customize your Imagemin Configuration (continued)

Instead of using imagemin-webpack-plugin's default plugin for JPG compression (imagemin-jpegtran), use the imagemin-mozjpeg plugin. Unlike Jpegtran, Mozjpeg let's you specify a compression quality for your JPG compression. We've already installed the Mozjpeg plugin for you in this Glitch, but you'll need to edit your webpack.config.js file:

const imageminMozjpeg = require('imagemin-mozjpeg');
new ImageminPlugin({
  pngquant: ({quality: [0.5, 0.5]}),
  plugins: [imageminMozjpeg({quality: 50})]
})

This code tells webpack to compress JPGs to a quality of 50 (0 is the worst; 100 is the best) using the Mozjpeg plugin.

{% Aside %} Are you wondering why Mozjpeg is added to the plugins array, but Pngquant isn't? Good question. {% endAside %}

If you're adding settings for a plugin that is a default plugin of imagemin-webpack-plugin, they can be added as a key-object pair on the object passed to ImageminPlugin(). The settings for Pnquant are a good example of this.

However, if you're adding settings for non-default plugins (for example, Mozjpeg), they should be added by including them in the array corresponding to the plugins property.

✔︎ Check-in

Your code should now look like this:

const imageminMozjpeg = require('imagemin-mozjpeg');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new CopyWebpackPlugin([{
      from: 'img/**/**',
      to: path.resolve(__dirname, 'dist')
    }]),
    new ImageminPlugin({
      pngquant: ({quality: [0.5, 0.5]}),
      plugins: [imageminMozjpeg({quality: 50})]
    })
  ]
}

Re-run webpack and verify results with Lighthouse

webpack --config webpack.config.js --mode production

Hooray! Your changes should have fixed the webpack warnings.

webpack warns you about large images, but it can't tell you if images are uncompressed or undercompressed. This is why it's always a good idea to use Lighthouse to verify your changes.

Lighthouse's "Efficiently encode images" performance audit can let you know if the JPEG images on your page are optimally compressed.

{% Instruction 'preview' %}

{% Img src="image/admin/abAeMMdJEj9j2osonskn.png", alt="Passing 'Efficiently encode images' audit in Lighthouse", width="766", height="976", class="screenshot" %}

Success! You have used Imagemin to optimally compress the images on your page.

Optimize third-party JavaScript

{% include 'content/devtools-headsup.njk' %}

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:

The included sample app features a simple web page with three features coming from third-party sources:

{% Img src="image/admin/cuWcC16X6oKRbXJXIIt7.png", alt="Screenshot of the page with third-party resources highlighted.", width="800", height="1294" %}
Third-party resources in the sample app.

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: {% Instruction 'remix', 'ol' %} {% Instruction 'preview', 'ol' %}

Run a Lighthouse performance audit on the page to establish baseline performance:

{% Instruction 'devtools-lighthouse', 'ol' %}

  1. Click Mobile.
  2. Select the Performance checkbox. (You can clear the rest of the checkboxes in the Audits section.)
  3. Click Simulated Fast 3G, 4x CPU Slowdown.
  4. Select the Clear Storage checkbox.
  5. 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.)

{% Img src="image/admin/gIkIJM2OaocxImjLLjev.png", alt="Screenshot of Lighthouse audit showing 2.4 second FCP and two opportunities: Eliminate render-blocking resources and Preconnect to required origins.", width="741", height="700" %}

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:

{% Img src="image/admin/P9ejh4JMzdpu8N3aZ7bC.png", alt="Screenshot of Eliminate render-blocking resources audit with the d3.v3.min.js script highlighted.", width="718", height="337" %}

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:

{% Img src="image/admin/vRP2oYmijq0sVyLRb2nU.png", alt="Screenshot of index.html with highlighted script tag in the head.", width="718", height="265" %}

Two magic attributes can unblock the parser when added to the script tag:

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:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %}

  1. Select Fast 3G in the Throttling dropdown menu. {% Instruction 'reload-page', 'ol' %}

{% Img src="image/admin/DjnRutx27trV4YkYcKhr.png", alt="Screenshot of DevTools Network panel.", width="783", height="618" %}

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:

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:

{% Instruction 'create' %}

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);
    });
  });

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-console', 'ol' %}

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.

{% Video src="video/tcFciHGuF3MxnTr1y5ue01OGLBn2/bJHFfNjxvIthfRWeidbx.mp4", autoplay=true, loop=true, muted=true, playsinline=true %}

{% Aside %} In this example, the element is loaded when it enters the viewport, which you can see happening in the video above. You can avoid that delay and create a smoother user experience by loading elements a little before they enter the viewport. To do that, use the rootMargin property to define margins around the target element, which effectively grows (or shrinks) the area that triggers the isIntersecting change. {% endAside %}

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:

{% Img src="image/admin/k4PDdjh77dXS1ZdGBSko.png", alt="Preconnect to required origins audit with the staticxx.facebook.com domain highlighted.", width="720", height="279" %}

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>:

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.

{% Img src="image/admin/Iv12shtGoURq2CTqO6jN.png", alt="Lighthouse audit showing 1 second FCP and the performance score of 99.", width="727", height="511" %}

{% Aside 'success' %} You should see that FCP and the performance score have improved! 🎉 The page now loads faster and consumes less data⁠—and all without negatively affecting the user-experience. 🤝 {% endAside %}

Codelab: Preload critical assets to improve loading speed

{% include 'content/devtools-headsup.njk' %}

In this codelab, the performance of the following web page is improved by preloading and prefetching a few resources:

{% Img src="image/admin/Ln5Oxy5vp8QWTn0GoOFh.png", alt="App Screenshot", width="800", height="578" %}

Measure

First measure how the website performs before adding any optimizations.

{% Instruction 'preview' %}

Run the Lighthouse performance audit (Lighthouse > Options > Performance) on the live version of your Glitch (see also Discover performance opportunities with Lighthouse).

Lighthouse shows the following failed audit for a resource that is fetched late:

{% Img src="image/admin/AMhzorWlWYejd0tA11rY.png", alt="Lighthouse: Preload key requests audit", width="800", height="231" %}

{% Instruction 'devtools-network' %}

{% Img src="image/admin/pO1f6x9pOqzYwPciKnTy.png", alt="Network panel with late-discovered resource", width="800", height="166" %}

The main.css file is not fetched by a Link element (<link>) placed in the HTML document, but a separate JavaScript file, fetch-css.js, attaches the Link element to the DOM after the window.onLoad event. This means that the file is only fetched after the browser finishes parsing and executing the JS file. Similarly, a web font (K2D.woff2) specified within main.css is only fetched once the CSS file has finished downloading.

The critical request chain represents the order of resources that are prioritized and fetched by the browser. For this web page, it currently looks like this:

├─┬ / (initial HTML file)
  └── fetch-css.js
    └── main.css
      └── K2D.woff2

Since the CSS file is on the third level of the request chain, Lighthouse has identified it as a late-discovered resource.

Preload critical resources

The main.css file is a critical asset that's needed immediately as soon as the page is loaded. For important files like this resource that are fetched late in your application, use a link preload tag to inform the browser to download it sooner by adding a Link element to the head of the document.

Add a preload tag for this application:

<head>
  <!-- ... -->
  <link rel="preload" href="main.css" as="style">
</head>

The as attribute is used to identify which type of resource is being fetched, and as="style" is used to preload stylesheet files.

Reload the application and take a look at the Network panel in DevTools.

{% Img src="image/admin/3xjh4kLTps0fCqs7wRwP.png", alt="Network panel with preloaded resource", width="800", height="166" %}

Notice how the browser fetches the CSS file before the JavaScript responsible for fetching it has even been finished parsing. With preload, the browser knows to make a preemptive fetch for the resource with the assumption that it is critical for the web page.

{% Aside %} If this was a real production app, it would make more sense to just place a <link> element in index.html to fetch the CSS file instead of using JavaScript to append it. Browsers already know to fetch a CSS file defined at the head of an HTML document with a high priority as soon as possible. However, preload is used in this codelab to demonstrate the best course of action for files that are fetched late in the request chain. For a large application, this can happen quite often. {% endAside %}

If not used correctly, preload can harm performance by making unnecessary requests for resources that aren't used. In this application, details.css is another CSS file located at the root of the project but is used for a separate /details route. To show an example of how preload can be used incorrectly, add a preload hint for this resource as well.

<head>
  <!-- ... -->
  <link rel="preload" href="main.css" as="style">
  <link rel="preload" href="details.css" as="style">
</head>

Reload the application and take a look at the Network panel. A request is made to retrieve details.css even though it is not being used by the web page.

{% Img src="image/admin/vOyq8M3X2UPd5JUTvex0.png", alt="Network panel with unecessary preload", width="800", height="189" %}

Chrome displays a warning in the Console panel when a preloaded resource is not used by the page within a few seconds after it has loaded.

{% Img src="image/admin/elfGvORyRu1s0slntYHo.png", alt="Preload warning in console", width="800", height="150" %}

Use this warning as an indicator to identify if you have any preloaded resources that are not being used immediately by your web page. You can now remove the unnecessary preload link for this page.

<head>
  <!-- ... -->
  <link rel="preload" href="main.css" as="style">
  <link rel="preload" href="details.css" as="style">
</head>

For a list of all the types of resources that can be fetched along with the correct values that should be used for the as attribute, refer to the MDN article on Preloading.

{% Aside %} Cross-origin resources can also be preloaded using the crossorigin attribute. Moreover, same-origin font resources must be fetched using anonymous mode CORS which is why the crossorigin attribute is also used in this preload tag. The Cross Origin Resource Sharing guide explains the topic of same-origin and cross-origin requests in more detail. {% endAside %}

Prefetch future resources

Prefetch is another browser hint that can be used to make a request for an asset used for a different navigation route but at a lower priority than other important assets needed for the current page.

In this website, clicking the image takes you to a separate details/ route.

{% Img src="image/admin/KLzVOClmdtwTZ3nhzGz5.png", alt="Details route", width="800", height="585" %}

A separate CSS file, details.css, contains all the styles needed for this simple page. Add a link element to index.html to prefetch this resource.

<head>
  <!-- ... -->
  <link rel="prefetch" href="details.css">
</head>

To understand how this triggers a request for the file, open the Network panel in DevTools and uncheck the Disable cache option.

{% Img src="image/admin/dddEy2ERtrha2WZKN5Ou.png", alt="Disable cache in Chrome DevTools", width="800", height="145" %}

Reload the application and notice how a very low priority request is made for details.css after all the other files have been fetched.

{% Img src="image/admin/STiBaCDoaZ1Ghzbp5fOH.png", alt="Network panel with prefetched resource", width="800", height="211" %}

With DevTools open, click the image on the website to navigate to the details page. Since a link element is used in details.html to fetch details.css, a request is made for the resource as expected.

{% Img src="image/admin/AfkaIGXdHifq8n1xD8YQ.png", alt="Details page network requests", width="800", height="106" %}

Click the details.css network request in DevTools to view its details. You'll notice that the file is retrieved from the browser's disk cache.

{% Img src="image/admin/3pPpPCjbwugX1nnqCf5e.png", alt="Details request fetched from disk cache", width="800", height="170" %}

By taking advantage of browser idle time, prefetch makes an early request for a resource needed for a different page. This speeds up future navigation requests by allowing the browser to cache the asset sooner and serve it from the cache when needed.

Preloading and prefetching with webpack

The Reduce JavaScript payloads with code splitting post explores the use of dynamic imports to split a bundle into multiple chunks. This is demonstrated with a simple application that dynamically imports a module from Lodash when a form is submitted.

{% Img src="image/admin/yg2lFaDyWQFBvw7YaJ3g.gif", alt="Magic Sorter app that demonstrates code splitting", width="600", height="375" %}

You can access the Glitch for this application here.

The following block of code, which lives in src/index.js, is responsible for dynamically importing the method when the button is clicked.

form.addEventListener("submit", e => {
  e.preventDefault()
  import('lodash.sortby')
    .then(module => module.default)
    .then(sortInput())
    .catch(err => { alert(err) });
});

Splitting a bundle improves page loading times by reducing its initial size. Version 4.6.0 of webpack provides support to preload or prefetch chunks that are imported dynamically. Using this application as an example, the lodash method can be prefetched at browser idle time; when a user presses the button, there is no delay for the resource to be fetched.

Use the specific webpackPrefetch comment parameter within a dynamic import to prefetch a particular chunk. Here is how it would look with this particular application.

form.addEventListener("submit", e => {
  e.preventDefault()
  import(/* webpackPrefetch: true */ 'lodash.sortby')
    .then(module => module.default)
    .then(sortInput())
    .catch(err => { alert(err) });
});

Once the application is reloaded, webpack injects a prefetch tag for the resource into the head of the document. This can be seen in the Elements panel in DevTools.

{% Img src="image/admin/5Zs5L6UMnkUJXyfwtru7.png", alt="Elements panel with prefetch tag", width="800", height="520" %}

Observing the requests in the Network panel also shows that this chunk is fetched with a low priority after all other resources have been requested.

{% Img src="image/admin/nGh06M2LqgTfBUwPaXK7.png", alt="Network panel with prefetched request", width="800", height="181" %}

Although prefetch makes more sense for this use case, webpack also provides support for preloading chunks that are dynamically imported.

import(/* webpackPreload: true */ 'module')

Conclusion

With this codelab, you should have a solid understanding of how preloading or prefetching certain assets can improve the user experience of your site. It is important to mention that these techniques should not be used for every resource and using them incorrectly can harm performance. The best results are noticed by only preloading or prefetching selectively.

To summarize:

Not all browsers currently support both preload and prefetch. This means that not all users of your application may notice performance improvements.

If you would like more information about specific aspects of how preloading and prefetching can affect your web page, refer to these articles:

Preload web fonts to improve loading speed

{% include 'content/devtools-headsup.njk' %}

This codelab shows you how to preload web fonts using rel="preload" to remove any flash of unstyled text (FOUT).

Measure

First measure how the website performs before adding any optimizations. {% Instruction 'preview', 'ol' %} {% Instruction 'audit-performance', 'ol' %}

The Lighthouse report that is generated will show you the fetching sequence of resources under Maximum critical path latency.

{% Img src="image/admin/eperh8ZUnjhsDlnJdNIG.png", alt="Webfonts are present in the critical request chain.", width="704", height="198" %}

In the above audit the web fonts are part of the critical request chain and fetched last. The critical request chain represents the order of resources that are prioritized and fetched by the browser. In this application, the web fonts (Pacfico and Pacifico-Bold) are defined using the @font-face rule and are the last resource fetched by the browser in the critical request chain. Typically, webfonts are lazy loaded which means that they are not loaded until the critical resources are downloaded (CSS, JS).

Here is the sequence of the resources fetched in the application:

{% Img src="image/admin/9oBNjZORrBj6X8RVlr9t.png", alt="Webfonts are lazy loaded.", width="583", height="256" %}

Preloading Web fonts

To avoid FOUT, you can preload web fonts that are required immediately. Add the Link element for this application at the head of the document:

<head>
 <!-- ... -->
 <link rel="preload" href="/assets/Pacifico-Bold.woff2" as="font" type="font/woff2" crossorigin>
</head>

The as="font" type="font/woff2" attributes tell the browser to download this resource as a font and helps in prioritization of the re­source queue.

The crossorigin attribute indicates whether the resource should be fetched with a CORS request as the font may come from a different domain. Without this attribute, the preloaded font is ignored by the browser.

Since Pacifico-Bold is used in the page header, we added a preload tag to fetch it even sooner. It isn't important to preload the Pacifico.woff2 font because it styles the text that is below the fold.

Reload the application and run lighthouse again. Check the Maximum critical path latency section.

{% Img src="image/admin/lC85s7XSc8zEXgtwLsFu.png", alt="Pacifico-Bold webfont is preloaded and removed from the cricical request chain", width="645", height="166" %}

Notice how the Pacifico-Bold.woff2 is removed from the critical request chain. It is fetched earlier in the application.

{% Img src="image/admin/BrXidcKZfCbbUbkcSwas.png", alt="Pacifico-Bold webfont is preloaded", width="553", height="254" %}

With preload, the browser knows that it needs to download this file earlier. It is important to note that if not used correctly, preload can harm performance by making unnecessary requests for resources that are not used.

Remove unused code

{% include 'content/devtools-headsup.njk' %}

In this codelab, improve the performance of the following application by removing any unused and unneeded dependencies.

{% Img src="image/admin/A6WweLPrgyud8VNCP3Dc.png", alt="App screenshot", width="800", height="567" %}

Measure

{% Aside %} Since webpack is used in this application, any changes made to the code will trigger a new build which can take a few seconds. Once it completes, you should see your changes reflected in the application. {% endAside %}

It's always a good idea to first measure how well a website performs before adding optimizations.

{% Instruction 'preview' %}

Go ahead and click on your favorite kitten! Firebase's Realtime Database is used in this application which is why the score updates in real-time and is synchronized with every other person using the application. 🐈

{% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %} {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/GnvBzXKQwy2az8Gyxl6D.png", alt="Original bundle size of 992 KB", width="800", height="169" %}

Almost 1 MB worth of JavaScript is being shipped to load this simple application!

Take a look at the project warnings in DevTools.

{% Img src="image/admin/DMdUUxzxNMEGZ6HsTd3P.png", alt="Warnings filter", width="800", height="244", class="screenshot" %}

{% Img src="image/admin/QGn9dHsaWNjTpMCwc0Rc.png", alt="Console warning", width="800", height="295", class="screenshot" %}

Firebase, which is one of the libraries used in this application, is being a good samaritan by providing a warning to let developers know to not import its entire package but only the components that are used. In other words, there are unused libraries that can be removed in this application to make it load faster.

There are also instances when a particular library is used, but where there may be a simpler alternative. The concept of removing unneeded libraries is explored later in this tutorial.

Analyzing the bundle

There are two main dependencies in the application:

How can just two dependencies contribute to a bundle size of almost 1 MB? Well, one of the reasons is that any dependency can in turn have their own dependencies, so there are a lot more than just two if every depth/branch of the dependency "tree" is considered. It's easy for an application to become large relatively quickly if many dependencies are included.

Analyze the bundler to get a better idea of what is going. There are a number of different community-built tools that can help do this, such as webpack-bundle-analyzer.

The package for this tool is already included in the app as a devDependency.

"devDependencies": {
  //...
  "webpack-bundle-analyzer": "^2.13.1"
},

This means that it can be used directly in the webpack configuration file. Import it at the very beginning of webpack.config.js:

const path = require("path");

//...
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

Now add it as a plugin at the very end of the file within the plugins array:

module.exports = {
  //...
  plugins: [
    //...
    new BundleAnalyzerPlugin()
  ]
};

When the application reloads, you should see a visualization of the entire bundle instead of the app itself.

{% Img src="image/admin/QhYbNr23tGbcHwFZGKIQ.png", alt="Webpack Bundle Analyzer", width="800", height="468" %}

Not as cute as seeing some kittens 🐱, but incredibly helpful nonetheless. Hovering over any of the packages shows its size represented in three different ways:

Stat size Size before any minification or compression.
Parsed size Size of actual package within the bundle after it has been compiled. Version 4 of webpack (which is used in this application) minifies the compiled files automatically which is why this is smaller than the stat size.
Gzipped size Size of package after it has been compressed with gzip encoding. This topic is covered in a separate guide.

With the webpack-bundle-analyzer tool, it is easier to identify unused or unneeded packages that make up a large percentage of the bundle.

Removing unused packages

The visualization shows that the firebase package consists of a lot more than just a database. It includes additional packages such as:

These are all amazing services provided by Firebase (and refer to the documentation to learn more), but none of them are being used in the application, so there's no reason to have them all imported.

Revert the changes in webpack.config.js to see the application again:

plugins: [
  //...
  new BundleAnalyzerPlugin()
];
const path = require("path");

//...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

The application should load normally now. Modify src/index.js to update the Firebase imports.

import firebase from 'firebase';
import firebase from 'firebase/app';
import 'firebase/database';

Now when the app reloads, the DevTools warning does not show. Opening the DevTools Network panel also shows a nice reduction in bundle size:

{% Img src="image/admin/8agHvjw7351Yj8U1rIla.png", alt="Bundle size reduced to 480 KB", width="800", height="152", class="screenshot" %}

More than half the bundle size was removed. Firebase provides many different services and gives developers the option to only include those that are actually needed. In this application, only firebase/database was used to store and sync all of the data. The firebase/app import, which sets up the API surface for each of the different services, is always required.

Although the bundle size has been reduced by quite a bit, there's still more work to do! 😈

Removing unneeded packages

Unlike Firebase, importing parts of the moment library cannot be done as easily, but maybe it can be removed entirely?

The birthday of each cute kitten is stored in Unix format (milliseconds) in the Firebase database.

{% Img src="image/admin/D1ufo1PdyzW3wFSu9sE1.png", alt="Birthdates stored in Unix format", width="560", height="236", class="screenshot" %}

This is a timestamp of a particular date and time represented by the number of milliseconds that have elapsed since January 1, 1970 00:00 UTC. If the current date and time can be calculated in the same format, a small function to find the age of each kitten in weeks can probably be constructed.

Like always, try not to copy and paste as you follow along here. Begin by removing moment from the imports in src/index.js.

import firebase from 'firebase/app';
import 'firebase/database';
import * as moment from 'moment';

There is a Firebase event listener that handles value changes in our database:

favoritesRef.on("value", (snapshot) => { ... })

Above this, add a small function to calculate the number of weeks from a given date:

const ageInWeeks = birthDate => {
  const WEEK_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 7;
  const diff = Math.abs((new Date).getTime() - birthDate);
  return Math.floor(diff / WEEK_IN_MILLISECONDS);
}

In this function, the difference in milliseconds between the current date and time (new Date).getTime() and the birth date (the birthDate argument, already in milliseconds) is calculated and divided by the number of milliseconds in a single week.

Finally, all instances of moment can be removed in the event listener by leveraging this function instead:

favoritesRef.on("value", (snapshot) => {
  const { kitties, favorites, names, birthDates } = snapshot.val();
  favoritesScores = favorites;

  kittiesList.innerHTML = kitties.map((kittiePic, index) => {
    const birthday = moment(birthDates[index]);

    return `
      <li>
        <img src=${kittiePic} onclick="favKittie(${index})">
        <div class="extra">
          <div class="details">
            <p class="name">${names[index]}</p>
            <p class="age">${moment().diff(birthday, 'weeks')} weeks old</p>
            <p class="age">${ageInWeeks(birthDates[index])} weeks old</p>
          </div>
          <p class="score">${favorites[index]} ❤</p>
        </div>
      </li>
    `})
});

Now reload the application and take a look at the Network panel once more.

{% Img src="image/admin/ixTpGQBkJimzUmEFBMsR.png", alt="Bundle size reduced to 225 KB", width="800", height="154", class="screenshot" %}

The size of our bundle was reduced by more than half again!

Conclusion

With this codelab, you should have a decent understanding of how to analyze a particular bundle and why it can be so useful to remove unused or unneeded packages. Before you begin optimizing an application with this technique, it's important to know that this can be significantly more complex in larger applications.

With regards to removing unused libraries, try to find out which parts of a bundle are being used and which parts are not. For a mysterious looking package that looks like it is not being used anywhere, take a step back and check which top-level dependencies might need it. Try to find a way to possibly decouple them from each other.

When it comes to removing unneeded libraries, things can be a little more complicated. It's important to work closely with your team and see if there is potential to simplify parts of the codebase. Removing moment in this application may look like it would be the right thing to do every time, but what if there were time zones and different locales that needed to be handled? Or what if there were more complicated date manipulations? Things can get very tricky when manipulating and parsing dates/times, and libraries like moment and date-fns simplify this significantly.

Everything is a tradeoff, and it's important to gauge whether it's even worth the complexity and effort to roll out a custom solution instead of relying on a third-party library.

Replace GIFs with video

{% include 'content/devtools-headsup.njk' %}

In this codelab, improve performance by replacing an animated GIF with a video.

Measure first

First measure how the website performs:

{% Instruction 'preview', 'ol' %} {% Instruction 'audit-performance', 'ol' %}

When you're finished, you should see that Lighthouse has flagged the GIF as an issue in its "Use video formats for animated content" audit.

Get FFmpeg

There are a number of ways you can convert GIFs to video; this guide uses FFmpeg. It's already installed in the Glitch VM, and, if you want, you can follow these instructions to install it on your local machine as well.

Open the console

Double-check that FFmpeg is installed and working:

{% Instruction 'remix', 'ol' %} {% Instruction 'console', 'ol' %}

  1. In the console, run:
which ffmpeg

You should get a file path back:

/usr/bin/ffmpeg

Change GIF to video

You should see something like this:

$ ls
cat-herd.gif
ffmpeg -i cat-herd.gif -b:v 0 -crf 25 -f mp4 -vcodec libx264 -pix_fmt yuv420p cat-herd.mp4

This tells FFmpeg to take the input, signified by the -i flag, of cat-herd.gif and convert it to a video called cat-herd.mp4. This should take a second to run. When the command finishes, you should be able to type ls again and see two files:

$ ls
cat-herd.gif  cat-herd.mp4

Create WebM videos

While MP4 has been around since 1999, WebM is a relative newcomer having been initially released in 2010. WebM videos can be much smaller than MP4 videos, so it makes sense to generate both. Thankfully the <video> element will let you add multiple sources, so if a browser doesn't support WebM, it can fallback to MP4.

ffmpeg -i cat-herd.gif -c vp9 -b:v 0 -crf 41 cat-herd.webm
ls -lh

You should have one GIF, and two videos:

$ ls -lh
total 4.5M
-rw-r--r-- 1 app app 3.7M May 26 00:02 cat-herd.gif
-rw-r--r-- 1 app app 551K May 31 17:45 cat-herd.mp4
-rw-r--r-- 1 app app 341K May 31 17:44 cat-herd.webm

Notice that the original GIF is 3.7M, whereas the MP4 version is 551K, and the WebM version is only 341K. That's a huge savings!

Update HTML to recreate GIF effect

Animated GIFs have three key traits that videos need to replicate:

Luckily, you can recreate these behaviors using the <video> element.

<img src="/images/cat-herd.gif" alt="Cowboys herding cats.">
<video autoplay loop muted playsinline></video>

A <video> element using these attributes will play automatically, loop endlessly, play no audio, and play inline (that is, not fullscreen), all the behaviors expected of animated GIFs! 🎉

Specify your sources

All that's left to do is specify your video sources. The <video> element requires one or more <source> child elements pointing to different video files the browser can choose from, depending on format support. Update the <video> with <source> elements that link to your cat-herd videos:

<video autoplay loop muted playsinline>
  <source src="/images/cat-herd.webm" type="video/webm">
  <source src="/images/cat-herd.mp4" type="video/mp4">
</video>

{% Aside %} Browsers don't speculate about which <source> is optimal, so the order of <source>s matters. For example, if you specify an MP4 video first and the browser supports WebM, browsers will skip the WebM <source> and use the MPEG-4 instead. If you prefer a WebM <source> be used first, specify it first! {% endAside %}

Preview

{% Instruction 'preview' %}

The experience should look the same. So far so good.

Verify with Lighthouse

With the live site open: {% Instruction 'audit-performance', 'ol' %}

You should see that the "Use video formats for animated content" audit is now passing! Woohoo! 💪

Serve images with correct dimensions

This Glitch is small enough that its images could be inspected by hand. However for most websites, using a tool like Lighthouse to automate this is essential.

{% Instruction 'preview', 'ol' %} {% Instruction 'audit-performance', 'ol' %}

  1. Look for the results of the Properly Size Images audit.

{% Img src="image/admin/rGu0xlOX97DGQlujoHad.png", alt="The properly size images audit failing in Lighthouse.", width="800", height="320" %}

The Lighthouse audit shows that both of this page's images need to be resized.

Fix flower_logo.png

Start at the top of the page and fix the logo image.

{% Img src="image/admin/W7Ig8iyp6dKpQyNkJX3S.png", alt="The DevTools elements panel", width="800", height="316" %}

This is the CSS for flower_logo.png:

.logo {
  width: 50px;
  height: 50px;
}

The CSS width of this image is 50 pixels, so flower_logo.png should be resized to match. You can use ImageMagick to resize the image to fit. ImageMagick is a CLI tool for image editing that comes pre-installed in the codelab environment.

{% Instruction 'remix', 'ol' %} {% Instruction 'console', 'ol' %}

  1. In the console, type:
convert flower_logo.png -resize 50x50 flower_logo.png

Fix flower_photo.jpg

Next, fix the photo of the purple flowers.

{% Img src="image/admin/zGpgvrz00hsMHKYvQoyU.png", alt="The DevTools elements panel", width="800", height="275" %}

This is the CSS for flower_photo.jpg:

.photo {
  width: 50vw;
  margin: 30px auto;
  border: 1px solid black;
}

50vw sets the CSS width of flower_photo.jpg to "half the width of the browser." (1vw is equal to 1% the width of the browser).

The ideal size for this image would depend on the device it is being viewed on, so you should resize it to a size that works well for most of your users. You can check your analytics data to learn which screen resolutions are common amongst your users:

{% Img src="image/admin/6j3106xF9uxWCZGh7PrL.png", alt="Google analytics of screen resolutions.", width="684", height="865" %}

This data indicates that 95%+ of the visitors to this site use screen resolutions 1920 pixels wide or less.

Using this information we can calculate how wide the image should be: (1920 pixels wide) * (50% of browser width) = 960 pixels

On resolutions greater than 1920 pixels wide, the image will be stretched to cover the area. The resized image is still fairly large, so the effects of this should not be very noticeable.

# macOS/Linux
convert flower_photo.jpg -resize 960x flower_photo.jpg

# Windows
magick convert flower_photo.jpg -resize 960x flower_photo.jpg

{% Aside %} 960x is not a typo; it specifies a width, but not a height. The image height will be scaled in proportion to the width. This is a handy trick for when you only care about an image's dimensions in one direction. {% endAside %}

Re-Run Lighthouse

{% Img src="image/admin/ShzwI5v2hLhHR7CGDRqV.png", alt="Lighthouse properly size images audit.", width="800", height="271" %}

… And it fails! Why is that?

Lighthouse runs its tests on a Nexus 5x. The Nexus 5x has a 1080 x 1920 screen. For the Nexus 5x, the optimal size of flower_photo.jpg would be 540 pixels wide (1080 pixels * . 5). This is much smaller than our resized image.

Should you resize the image to be even smaller? Probably. However, the answer to this isn't always clear-cut.

The trade-off here is between image quality on high-resolution devices and performance. It's easy to overestimate how closely users will be inspecting images—so you should probably make them smaller—but there are certainly use cases where image quality is more important.

The good news is that you can bypass this tradeoff altogether by using responsive images to serve multiple images sizes. You can learn more about this in the Responsive Images guide.

Creating WebP Images with the Command Line

The webp command line tool has already been installed for you, so you're all set to get started. This tool converts JPG, PNG, and TIFF images to WebP.

Convert images to WebP

{% Instruction 'remix', 'ol' %} {% Instruction 'console', 'ol' %}

  1. Type the following command:
cwebp -q 50 images/flower1.jpg -o images/flower1.webp

This command converts, at a quality of 50 (0 is the worst; 100 is the best), the images/flower1.jpg file and saves it as images/flower1.webp.

{% Aside %} Are you wondering why you type cwebp instead of webp? WebP has two separate commands for encoding and decoding WebP images. cwebp encodes images to WebP, while dwebp decodes images from WebP. {% endAside %}

After doing this, you should see something like this in the console:

Saving file 'images/flower1.webp'
File:      images/flower1.jpg
Dimension: 504 x 378
Output:    29538 bytes Y-U-V-All-PSNR 34.57 36.57 36.12   35.09 dB
           (1.24 bpp)
block count:  intra4:        750  (97.66%)
              intra16:        18  (2.34%)
              skipped:         0  (0.00%)
bytes used:  header:            116  (0.4%)
             mode-partition:   4014  (13.6%)
 Residuals bytes  |segment 1|segment 2|segment 3|segment 4|  total
    macroblocks:  |      22%|      26%|      36%|      17%|     768
      quantizer:  |      52 |      42 |      33 |      24 |
   filter level:  |      16 |       9 |       6 |      26 |

You've just successfully converted the image to WebP.

However, running the cwebp command one image at a time like this would take a long time to convert many images. If you need to do this, you can use a script instead.

`for file in images/*; do cwebp -q 50 "$file" -o "${file%.*}.webp"; done`

This script converts, at a quality of 50, all the files in the images/ directory, and saves them as a new file (same filename, but with a .webp file extension) in the same directory.

✔︎ Check-in

You should now have 6 files in your images/ directory:

flower1.jpg
flower1.webp
flower2.jpg
flower2.webp
flower3.png
flower3.webp

Next, update this Glitch to serve WebP images to browsers that support it.

Add WebP images using the <picture> tag

The <picture> tag allows you to serve WebP to newer browsers while maintaining support for older browsers.

<picture>
  <source type="image/webp" srcset="images/flower1.webp">
  <source type="image/jpeg" srcset="images/flower1.jpg">
  <img src="images/flower1.jpg">
</picture>

✔︎ Check-in

Once completed, the <picture> tags in index.html should look like this:

<picture>
  <source type="image/webp" srcset="images/flower1.webp">
  <source type="image/jpeg" srcset="images/flower1.jpg">
  <img src="images/flower1.jpg">
</picture>
<picture>
  <source type="image/webp" srcset="images/flower2.webp">
  <source type="image/jpeg" srcset="images/flower2.jpg">
  <img src="images/flower2.jpg">
</picture>
<picture>
  <source type="image/webp" srcset="images/flower3.webp">
  <source type="image/png" srcset="images/flower3.png">
  <img src="images/flower3.png">
</picture>

Next, use Lighthouse to verify you've correctly implemented WebP images on the site.

Verify WebP usage with Lighthouse

Lighthouse's Serve images in next-gen formats performance audit can let you know if all the images on your site are using next-gen formats like WebP.

{% Instruction 'preview', 'ol' %} {% Instruction 'audit-performance', 'ol' %}

  1. Verify the Serve images in next-gen formats audit is passed.

{% Img src="image/admin/Y8x0FLWs1Xsf32DX20DG.png", alt="Passing 'Serve images in next-gen formats' audit in Lighthouse", width="701", height="651" %}

Success! You are now serving WebP images on your site.

Serve modern code to modern browsers for faster page loads

{% include 'content/devtools-headsup.njk' %}

In this codelab, improve the performance of this simple application that allows users to rate random cats. Learn how to optimize the JavaScript bundle by minizming how much code is transpiled.

{% Img src="image/admin/E8nNmPiUnD9unvl0ydwT.png", alt="App screenshot", width="800", height="681" %}

In the sample app, you can select a word or emoji to convey how much you like each cat. When you click a button, the app displays the button's value underneath the current cat image.

Measure

{% Aside %} Since webpack is used in this application, any changes made to the code will trigger a new build, which can take a few seconds. Once it completes, you should see your changes reflected in the application. {% endAside %}

It's always a good idea to begin by inspecting a website before adding any optimizations:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %} {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/WOLgtkSsnLlAVqXXkvS2.png", alt="Original bundle size request", width="717", height="127" %}

Over 80 KB is used for this application! Time to find out if parts of the bundle aren't being used:

{% Instruction 'devtools-command', 'ol' %}

{% Img src="image/admin/bUJOV2XobvxF0LrIvML4.png", alt="Command Menu", width="800", height="170" %}
  1. Enter Show Coverage and hit Enter to display the Coverage tab.

  2. In the Coverage tab, click Reload to reload the application while capturing coverage.

    {% Img src="image/admin/MVS59W0Ei307x4Ab3FvU.png", alt="Reload app with code coverage", width="800", height="363" %}

  3. Take a look at how much code was used versus how much was loaded for the main bundle:

    {% Img src="image/admin/YaXfH1OcPENdyMeBBrqK.png", alt="Code coverage of bundle", width="800", height="77" %}

Over half the bundle (44 KB) is not even utilized. This is because a lot of the code within consists of polyfills to ensure that the application works in older browsers.

Use @babel/preset-env

The syntax of the JavaScript language conforms to a standard known as ECMAScript, or ECMA-262. Newer versions of the specification are released every year and include new features that have passed the proposal process. Each major browser is always at a different stage of supporting these features.

The following ES2015 features are used in the application:

The following ES2017 feature is used as well:

Feel free to dive into the source code in src/index.js to see how all of this is used.

All of these features are supported in the latest version of Chrome, but what about other browsers that don't support them? Babel, which is included in the application, is the most popular library used to compile code that contains newer syntax into code that older browsers and environments can understand. It does this in two ways:

Look at package.json to see which Babel libraries are included:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}

Now look at webpack.config.js to see how babel-loader is included as a rule:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
import "./style.css";
import "@babel/polyfill";

Take a look at the Babel configurations file, .babelrc, to see how it's included:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

This is a Babel and webpack setup. Learn how to include Babel in your application if you happen to use a different module bundler than webpack.

The targets attribute in .babelrc identifies which browsers are being targeted. @babel/preset-env integrates with browserslist, which means you can find a full list of compatible queries that can be used in this field in the browserlist documentation.

The "last 2 versions" value transpiles the code in the application for the last two versions of every browser.

Debugging

To get a complete look at all the browser's Babel targets as well as all the transforms and polyfills that are included, add a debug field to .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}

Reload the application and take a look at the Glitch status logs at the bottom of the editor.

Targeted browsers

Babel logs a number of details to the console about the compilation process, including all the target environments that the code has been compiled for.

{% Img src="image/admin/X6nauvGqKuyWoBekgCbw.png", alt="Targeted browsers", width="464", height="457" %}

Notice how discontinued browsers, such as Internet Explorer, are included in this list. This is a problem because unsupported browsers won't have newer features added, and Babel continues to transpile specific syntax for them. This unnecessarily increases the size of your bundle if users are not using this browser to access your site.

Babel also logs a list of transform plugins used:

{% Img src="image/admin/clNXHWSfeD2RKDkIrJo8.png", alt="List of plugins used", width="800", height="604" %}

That's a pretty long list! These are all the plugins that Babel needs to use to transform any ES2015+ syntax to older syntax for all the targeted browsers.

However, Babel doesn't show any specific polyfills that are used:

{% Img src="image/admin/xPhc2usfwfIJkoK4EubY.png", alt="No polyfills added", width="800", height="38" %}

This is because the entire @babel/polyfill is being imported directly.

Load polyfills individually

By default, Babel includes every polyfill needed for a complete ES2015+ environment when @babel/polyfill is imported into a file. To import specific polyfills needed for the target browsers, add a useBuiltIns: 'entry' to the configuration.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Reload the application. You can now see all the specific polyfills included:

{% Img src="image/admin/VQHDI6qG94zbTiorG3aT.gif", alt="List of polyfills imported", width="800", height="465" %}

Although only needed polyfills for "last 2 versions" is now included, it is still a super long list! This is because polyfills needed for the target browsers for every newer feature is still included. Change the value of the attribute to usage to only include those needed for features that are being used in the code.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

With this, polyfills are automatically included where needed. This means you can remove the @babel/polyfill import in src/index.js.

import "./style.css";
import "@babel/polyfill";

Now only the required polyfills needed for the application are included.

{% Img src="image/admin/ir2qkYhxoJDWoQUhD8KD.png", alt="List of polyfills automatically included", width="800", height="114" %}

The application bundle size is reduced significantly.

{% Img src="image/admin/6cIW1H9dYz38h3jp9QpK.png", alt="Bundle size reduced to 30.1 KB", width="796", height="135" %}

Narrowing the list of supported browsers

The number of browser targets included is still quite large, and not many users use discontinued browsers such as Internet Explorer. Update the configurations to the following:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Take a look at the details for the fetched bundle.

{% Img src="image/admin/LQrCZtEqujASX2KBR9ku.png", alt="Bundle size of 30.0 KB", width="793", height="131" %}

Since the application is so small, there really isn't much of a difference with these changes. However, using a browser market share percentage (such as ">0.25%") along with excluding specific browsers that you are confident your users are not using is the recommended approach. Take a look at the "Last 2 versions" considered harmful article by James Kyle to learn more about this.

Use <script type="module">

There is still more room for improvement. Although a number of unused polyfills have been removed, there are many that are being shipped that are not needed for some browsers. By using modules, newer syntax can be written and shipped to browsers directly without the use of any unnecessary polyfills.

JavaScript modules are a relatively new feature supported in all major browsers. Modules can be created using a type="module" attribute to define scripts that import and export from other modules. For example:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Many newer ECMAScript features are already supported in environments that support JavaScript modules (instead of needing Babel.) This means that the Babel config can be modified to send two different versions of your application to the browser:

Using ES Modules with Babel

To have separate @babel/preset-env settings for the two versions of the application, remove the .babelrc file. Babel settings can be added to the webpack configuration by specifying two different compilation formats for each version of the application.

Begin by adding a configuration for the legacy script to webpack.config.js:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Notice that instead of using the targets value for "@babel/preset-env", esmodules with a value of false is used instead. This means that Babel includes all the necessary transforms and polyfills to target every browser that does not yet support ES modules.

Add entry, cssRule, and corePlugins objects to the beginning of the webpack.config.js file. These are all shared between both the module and legacy scripts served to the browser.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

Now similarly, create a config object for the module script below where legacyConfig is defined:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

The main difference here is that a .mjs file extension is used for the output filename. The esmodules value is set to true here which means that the code that is outputted into this module is a smaller, less compiled script that does not go through any transformation in this example since all the features used are already supported in browsers that support modules.

At the very end of the file, export both configurations in a single array.

module.exports = [
  legacyConfig, moduleConfig
];

Now this builds both a smaller module for browsers that support it and a larger transpiled script for older browsers.

Browsers that support modules ignore scripts with a nomodule attribute. Conversely, browsers that do not support modules ignore script elements with type="module". This means you can include a module as well as a compiled fallback. Ideally, the two versions of the application should be in index.html like this:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Browsers that support modules fetch and execute main.mjs and ignore main.bundle.js. The browsers that do not support modules do the opposite.

It is important to note that unlike regular scripts, module scripts are always deferred by default. If you would like the equivalent nomodule script to also be deferred and only executed after parsing, then you'll need to add the defer attribute:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

The last thing that needs to be done here is to add the module and nomodule attributes to the module and legacy script respectively, Import the ScriptExtHtmlWebpackPlugin at the very top of webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

Now update the plugins array in the configurations to include this plugin:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

These plugin settings add a type="module" attribute for all .mjs script elements as well as a nomodule attribute for all .js script modules.

{% Aside %} If you're having trouble understanding how to add all of these configurations to webpack.config.js, take a look at the complete version of the file. {% endAside %}

Serving modules in the HTML document

The last thing that needs to be done is to output both the legacy and modern script elements to the HTML file. Unfortunately, the plugin that creates the final HTML file, HTMLWebpackPlugin, does not currently support the output of both the module and nomodule scripts. Although there are workarounds and separate plugins created to solve this problem, such as BabelMultiTargetPlugin and HTMLWebpackMultiBuildPlugin, a simpler approach of adding the module script element manually is used for the purpose of this tutorial.

Add the following to src/index.js at the end of the file:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Now load the application in a browser that supports modules, such as the latest version of Chrome.

{% Img src="image/admin/7DHv0rPqfFE14Qp7OtqJ.png", alt="5.2 KB module fetched over network for newer browsers", width="800", height="100" %}

Only the module is fetched, with a much smaller bundle size due to it being largely untranspiled! The other script element is completely ignored by the browser.

If you load the application on an older browser, only the larger, transpiled script with all the needed polyfills and transforms are be fetched. Here is a screenshot for all the requests made on an older version of Chrome (version 38).

{% Img src="image/admin/xapbFvWftJjVdsekOAFJ.png", alt="30 KB script fetched for older browsers", width="800", height="324" %}

Conclusion

You now understand how to use @babel/preset-env to provide only the necessary polyfills required for targeted browsers. You also know how JavaScript modules can improve performance further by shipping two different transpiled versions of an application. With a decent understanding of how both these techniques can cut your bundle size down significantly, go forth and optimize!

Setting performance budgets with webpack

Webpack combines all your imported files and packages them into one or more output files known as bundles. Bundling is neat, but as your app grows your bundles will grow too. You need to monitor bundle sizes to ensure that they don't grow too large and affect how long your application takes to load. Webpack supports setting performance budgets based on asset size and it can keep an eye on bundle sizes for you.

To see it in action, here's an app that counts the days left until New Year's. It's built with React and moment.js. (Just like real-world apps that increasingly rely on frameworks and libraries. 😉)

{% Img src="image/admin/xG1cKnEmDUJk6Dd4892x.png", alt="An app that counts the days left until New Year's day", width="800", height="459" %}

Measure

This codelab already contains the app bundled with webpack.

{% Instruction 'remix', 'ol' %} {% Instruction 'console', 'ol' %}

  1. To get a color-coded list of assets and their sizes, type webpack in the console.
webpack

The main bundle is highlighted in yellow because it's larger than 244 KiB (250 KB).

{% Aside %} Webpack uses the unit "KiB", which you might not have seen before. 1 "KiB" and 1 "KB" are fairly close in size: 1 KiB is 1024 bytes, while 1 KB is 1000 bytes. {% endAside %}

{% Img src="image/admin/MpeaVZE4nJHOAAnlQ8Sz.png", alt="Webpack output showing bundle size of 323 KiB", width="640", height="59" %}
Webpack warning you about bulky JS bundle ⚠️

These warnings are enabled by default in production mode and the default threshold is 244 KiB uncompressed, for both assets and entry points (the combination of all assets used during the initial load of a page).

{% Img src="image/admin/tXgrsOqdJAzf6LTelr0v.png", alt="Webpack warning that the asset exceeds the recommended size limit", width="642", height="108" %}
Webpack warning you about bulky JS bundle ⚠️

Webpack will not only warn you, but it will also give you a recommendation on how to downsize your bundles. You can learn more about the recommended techniques on Web Fundamentals.

{% Img src="image/admin/pygtt0At7nmByNKm38hr.png", alt="Webpack performance optimization recommendation", width="641", height="96" %}
Webpack performance optimization recommendation 💁

Set a custom performance budget

An appropriate performance budget will depend on the nature of your project. It's always best to do your own research. A good rule of thumb is to deliver under 170 KB of compressed/minified critical-path resources.

{% Aside %} Critical-path resources are the minimal set of resources required for the browser to display the first screen's worth of content. {% endAside %}

For this simple demo, try being even more conservative and set the budget to 100 KB (97.7 KiB). In webpack.config.js, add the following:

module.exports = {
  //...
  performance: {
    maxAssetSize: 100000,
    maxEntrypointSize: 100000,
    hints: "warning"
  }
};

The new performance budget is set in bytes:

In this case, there's only one bundle which also acts as the entry point.

Possible values for hints are:

  1. warning (default): Displays a yellow warning message, but the build passes. It's best to use this in development environments.
  2. error: Displays a red error message, but the build still passes. This setting is recommended for production builds.
  3. false: No warnings or errors are shown.
{% Img src="image/admin/ZFP0SZfO4zzD0lyD29Vk.png", alt="Webpack performance error in red font", width="606", height="107" %}
Webpack performance hint "error" 🚨

Optimize

The purpose of a performance budget is to warn you about performance issues before they become too difficult to fix. There is always more than one way to build an app and some techniques will make for faster load times. (A lot of them are documented right here in Optimizing your JavaScript. 🤓)

Frameworks and libraries make developers' lives easier, but end users don't really care how apps are built, only that they're functional and fast. If you find yourself going over the performance budget, it's time to think about possible optimizations.

In the real world, large client-side frameworks are usually hard to swap out, so it's important to use them wisely. With a bit of research, you can often find smaller alternatives to popular libraries that do the job just as well (date-fns is a good alternative for moment.js). Sometimes it makes more sense to not use a framework or library at all if it has a significant performance impact.

Removing unused code a good way to optimize apps that include large third-party libraries. The Remove unused code guide explains this process in detail and here's a quick way to rewrite the countdown code without using moment.js.

In app/components/Countdown.jsx remove:

const today = moment();
const yearEnd = moment().endOf('year');
const daysLeft = yearEnd.diff(today, 'days');

And delete this line:

const moment = require('moment');

It takes a bit of math, but you can implement the same countdown with vanilla JavaScript:

const today = new Date();
const year = today.getFullYear();
const yearEnd = new Date(year,11,31); //months are zero indexed in JS
const timeDiff = Math.abs(yearEnd.getTime() - today.getTime());
const daysLeft = Math.ceil(timeDiff / (1000 * 3600 * 24));

Now remove moment.js from package.json and run webpack in the console again to build the optimized bundle.

Ta-da! You have shaved off 223 KiB (230KB) and the app is under budget.🎉

{% Img src="image/admin/A6TIDToLthjRrHvxluND.png", alt="Webpack bundle size output after optimization is 97.7 KiB", width="473", height="58" %}

Monitor

Setting up a performance budget in webpack takes just a couple of lines of code and it will warn you if you ever (accidentally) add a big dependency. The saying goes "out of sight, out of mind" but webpack can make sure that you are aware of performance implications at all times.

Specifying multiple slot widths

{% Instruction 'preview' %}

View the code

<img src="flower.jpg"
  srcset="flower-small.jpg 480w, flower-large.jpg 800w"
  sizes="(max-width: 480px) 100vw, (max-width: 1024px) 50vw, 800px">

What's going on here?

The value of the sizes attribute dictates that image display width will be: "100% of the viewport width" on viewports up to 480px wide, "50% of the viewport width" on screens 481–1024px wide, and 800px on screens wider than 1024px. These widths match the widths specified in the CSS.

The ability to specify multiple slot widths accommodates page layouts that use different styling (that is, image widths) for different viewport sizes.

How to specify multiple slot widths

The following is not a valid width:

Minify and compress network payloads with gzip

{% include 'content/devtools-headsup.njk' %}

This codelab explores how both minifying and compressing the JavaScript bundle for the following application improves page performance by reducing the app's request size.

{% Img src="image/admin/Ga0pUShY7cQ0BDAPgPuh.png", alt="App screenshot", width="800", height="535" %}

Measure

{% Aside %} Since webpack is used in this application, any changes made to the code will trigger a new build which can take a few seconds. Once it completes, you should see your changes reflected in the application. {% endAside %}

Before diving in to add optimizations, it's always a good idea to first analyze the current state of the application.

{% Instruction 'preview' %}

This app, which was also covered in the "Remove unused code" codelab, lets you vote for your favorite kitten. 🐈

Now take a look at how large this application is:

{% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %} {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/Zzm8kiE2W29yGEZC7C2u.png", alt="Original bundle size in Network panel", width="800", height="186" %}

Although a lot of progress was made in the "Remove unused code" codelab to trim this bundle size down, 225 KB is still quite large.

Minification

Consider the following block of code.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

If this function is saved in a file of its own, the file size is around 112 B (bytes).

If all whitespace is removed, the resulting code looks like this:

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

The file size would now be around 83 B. If it gets further mangled by reducing the length of variable name and modifying some expressions, the final code may end up looking like this:

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

The file size now reaches 62 B.

With each step, the code is becoming harder to read. However, the browser's JavaScript engine interprets each of these in the exact same way. The benefit of obfuscating code in this manner can help achieve smaller file sizes. 112 B really was not much to begin with, but there was still a 50% reduction in size!

In this application, webpack version 4 is used as a module bundler. The specific version can be seen in package.json.

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

Version 4 already minifies the bundle by default during production mode. It uses TerserWebpackPlugin a plugin for Terser. Terser is a popular tool used to compress JavaScript code.

To get an idea of what the minified code looks like, go ahead and click main.bundle.js while still in the DevTools Network panel. Now click the Response tab.

{% Img src="image/admin/uti2q15O2MtiEsegYoV9.png", alt="Minified response", width="800", height="249" %}

The code in its final form, minified and mangled, is shown in the response body. To find out how large the bundle may have been if it was not minified, open webpack.config.js and update the mode configuration.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

Reload the application and take a look at the bundle size again through the DevTools Network panel

{% Img src="image/admin/H0lINRmM2gF6NHnzmkGM.png", alt="Bundle size of 767 KB", width="700", height="129" %}

That's a pretty big difference! 😅

Make sure to revert the changes here before continuing.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

Including a process to minify code in your application depends on the tools that you use:

Compression

{% Aside 'warning' %} Many hosting platforms, CDNs and reverse proxy servers either encode assets with compression by default or allow you to easily configure them. This means that you may rarely ever need to set up your server similar to how it is done in the compression section of this tutorial, but you can continue to read if you are interested in learning how compression works. {% endAside %}

Although the term "compression" is sometimes loosely used to explain how code is reduced during the minification process, it isn't actually compressed in the literal sense.

Compression usually refers to code that has been modified using a data compression algorithm. Unlike minification which ends up providing perfectly valid code, compressed code needs to be decompressed before being used.

With every HTTP request and response, browsers and web servers can add headers to include additional information about the asset being fetched or received. This can be seen in the Headers tab within the DevTools Network panel where three types are shown:

Take a look at the accept-encoding header in the Request Headers.

{% Img src="image/admin/shmaD9cmjjFMITKL0TAW.png", alt="Accept encoding header", width="800", height="361" %}

accept-encoding is used by the browser to specify which content encoding formats, or compression algorithms, it supports. There are many text-compression algorithms out there, but there are only three that are supported here for the compression (and decompression) of HTTP network requests:

The sample application in this tutorial is identical to the app completed in the "Remove unused code" codelab except for the fact that Express is now used as a server framework. In the next few sections, both static and dynamic compression is explored.

Dynamic compression

Dynamic compression involves compressing assets on-the-fly as they get requested by the browser.

Pros

Cons

Dynamic compression with Node/Express

The server.js file is responsible for setting up the Node server that hosts the application.

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

All this currently does is import express and use the express.static middleware to load all the static HTML, JS and CSS files in the public/ directory (and those files are created by webpack with every build).

To make sure all of the assets are compressed every time they're requested, the compression middleware library can be used. Begin by adding it as a devDependency in package.json:

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

And import it into the server file, server.js:

const express = require('express');
const compression = require('compression');

And add it as a middleware before express.static is mounted:

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

Now reload the app and take a look at the bundle size in the Network panel.

{% Img src="image/admin/MMtTKWPKfht8RYd8BMYF.png", alt="Bundle size with dynamic compression", width="800", height="161" %}

From 225 KB to 61.6 KB! In the Response Headers now, a content-encoding header shows that the server is sending down this file encoded with gzip.

{% Img src="image/admin/523FjNSQOK95lGzg0B6D.png", alt="Content encoding header", width="800", height="470" %}

Static compression

The idea behind static compression is to have assets compressed and saved ahead of time.

Pros

Cons

Static compression with Node/Express and webpack

Since static compression involves compressing files ahead of time, webpack settings can be modified to compress assets as part of the build step. CompressionPlugin can be used for this.

Begin by adding it as a devDependency in package.json:

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

Like any other webpack plugin, import it in the configurations file, webpack.config.js:

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

And include it within the plugins array:

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

By default, the plugin compresses the build files using gzip. Take a look at the documentation to learn how to add options to use a different algorithm or include/exclude certain files.

When the app reloads and rebuilds, a compressed version of the main bundle is now created. Open the Glitch Console to take a look at what's inside the final public/ directory that's served by the Node server.

{% Aside %} The git.ignore file includes the public/ directory. Directories that contain build files are usually included here. Glitch also hides these files from the editor tree. {% endAside %}

cd public
ls

{% Img src="image/admin/YQwCT87xMfMwWiH63lTS.png", alt="Final outputted files in public directory", width="800", height="188" %}

The gzipped version of the bundle, main.bundle.js.gz, is now saved here as well. CompressionPlugin also compresses index.html by default.

The next thing that needs to be done is tell the server to send these gzipped files whenever their original JS versions are being requested. This can be done by defining a new route in server.js before the files are served with express.static.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get is used to tell the server how to respond to a GET request for a specific endpoint. A callback function is then used to define how to handle this request. The route works like this:

Once the app reloads, take a look at the Network panel once more.

{% Img src="image/admin/b7xYRsSdNhWX5Lc8zE51.png", alt="Bundle size reduction with static compression", width="800", height="176" %}

Just like before, a significant reduction in bundle size!

Conclusion

This codelab covered the process of minifying and compressing source code. Both these techniques are becoming a default in many of the tools available today, so it's important to find out whether your toolchain already supports them or if you should begin applying both processes yourself.

Minify and compress network payloads with brotli

{% include 'content/devtools-headsup.njk' %}

This codelab is an extension of the Minify and compress network payloads codelab and assumes you are familiar with the basics concepts of compression. As compared to other compression algorithms like gzip, this codelab explores how Brotli compression can further reduce compression ratios and your app's overall size.

{% Img src="image/admin/k94QI9lhJ39aXpwUrMNk.png", alt="App screenshot", width="800", height="535" %}

Measure

{% Aside %} Since webpack is used in this application, any changes made to the codewill trigger a new build which can take a few seconds. Once it completes, you should see your changes reflectedin the application. {% endAside %}

Before diving in to add optimizations, it's always a good idea to first analyze the current state of the application.

{% Instruction 'remix', 'ol' %} {% Instruction 'preview', 'ol' %}

In the previous Minify and compress network payloads codelab, we reduced the size of main.js from 225 KB to 61.6 KB. In this codelab, you will explore how Brotli compression can reduce this bundle size even further.

Brotli Compression

{% Aside 'warning' %} Many hosting platforms, CDNs and reverse proxy servers either encode assets with compression by default or allow you to easily configure them. If your hosting platform supports Brotli, you may not need to setup your server to compress your assets with Brotli as described in this tutorial. {% endAside %}

Brotli is a newer compression algorithm which can provide even better text compression results than gzip. According to CertSimple, Brotli performance is:

To use Brotli, your server must support HTTPS. Brotli is supported in the latest versions of most browsers. Browsers that support Brotli will include br in Accept-Encoding headers:

Accept-Encoding: gzip, deflate, br

You can determine which compression algorithm is used via the Content-Encoding field in the Chrome Developer Tools Network tab (Command+Option+I or Ctrl+Alt+I):

{% Img src="image/admin/ZddI1FFjEckeO8mabgYl.png", alt="Network panel", width="800", height="136" %}

Enabling Brotli

Dynamic compression

Dynamic compression involves compressing assets on-the-fly as they get requested by the browser.

Pros

Cons

Dynamic compression with Node/Express

The server.js file is responsible for setting up the Node server that hosts the application.

var express = require('express');

var app = express();

app.use(express.static('public'));

var listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

All this currently does is import express and use the express.static middleware to load all the static HTML, JS and CSS files in the public/directory (and those files are created by webpack with every build).

To make sure all of the assets are compressed using brotli every time they're requested, the shrink-ray module can be used. Begin by adding it as a devDependency in package.json:

"devDependencies": {
  //...
  "shrink-ray": "^0.1.3"
},

And import it into the server file, server.js:

var express = require('express');
var shrinkRay = require('shrink-ray');

And add it as a middleware before express.static is mounted:

//...
var app = express();

// compress all requests
app.use(shrinkRay());

app.use(express.static('public'));

Now reload the app, and take a look at the bundle size in the Network panel:

{% Img src="image/admin/KXDWBC6aH4o6MVJvBu8X.png", alt="Bundle size with dynamic Brotli compression", width="724", height="97" %}

You can now see brotli is applied from bz in the Content-Encoding header. main.bundle.js is reduced from 225 KB to 53.1 KB! This is ~14% smaller compared to gzip (61.6 KB).

{% Aside %} Brotli has eleven quality levels from 0 (no compression) to 9 (maximum compression). A quality of 1 is very fast but less effective, whereas a quality setting of 11 is very slow but provides big savings in file size. Note that unlike the standard brotli library, which defaults to quality 11, shrink-ray defaults to quality 4, which is generally more appropriate for dynamic content. You can adjust the parameters of the brotli algorithm by passing in an options parameters to shrinkRay([options]). {% endAside %}

Static compression

The idea behind static compression is to have assets compressed and saved ahead of time.

Pros

Cons

Static compression with Node/Express and webpack

Since static compression involves compressing files ahead of time, webpack settings can be modified to compress assets as part of the build step. The brotli-webpack-plugin can be used for this.

Begin by adding it as a devDependency in package.json:

"devDependencies": {
  //...
 "brotli-webpack-plugin": "^1.1.0"
},

Like any other webpack plugin, import it in the configurations file, webpack.config.js:

var path = require("path");

//...
var BrotliPlugin = require('brotli-webpack-plugin');

And include it within the plugins array:

module.exports = {
  // ...
  plugins: [
    // ...
    new BrotliPlugin({
      asset: '[file].br',
      test: /\.(js)$/
    })
  ]
},

The plugin array uses the following arguments:

For example, main.js would be renamed to main.js.br.

When the app reloads and rebuilds, a compressed version of the main bundle is now created. Open the Glitch Console to take a look at what's inside the final public/ directory that's served by the Node server.

  1. Click the Tools button.
  2. Click the Console button.
  3. In the console, run the following commands to change into the public directory and see all of its files:
cd public
ls -lh
{% Img src="image/admin/GOlRrAhdDWO7yi7Bm1Pg.png", alt="Bundle size with static Brotli compression", width="489", height="133" %}

The brotli compressed version of the bundle, main.bundle.js.br, is now saved here as well and is ~76% smaller in size (225 KB vs. 53 KB) than main.bundle.js.

Next, tell the server to send these brotli-compressed files whenever their original JS versions are being requested. This can be done by defining a new route in server.js before the files are served with express.static.

var express = require('express');

var app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.br';
  res.set('Content-Encoding', 'br');
  res.set('Content-Type', 'application/javascript; charset=UTF-8');
  next();
});

app.use(express.static('public'));

app.get is used to tell the server how to respond to a GET request for a specific endpoint. A callback function is then used to define how to handle this request. The route works like this:

Because some browsers may not support brotli compression, confirm brotli is supported before returning the brotli-compressed file by checking the Accept-Encoding request header includes br:

var express = require('express');

var app = express();

app.get('*.js', (req, res, next) => {
  if (req.header('Accept-Encoding').includes('br')) {
    req.url = req.url + '.br';
    console.log(req.header('Accept-Encoding'));
    res.set('Content-Encoding', 'br');
    res.set('Content-Type', 'application/javascript; charset=UTF-8');
  }
  next();
});

app.use(express.static('public'));

Once the app reloads, take a look at the Network panel once more.

{% Img src="image/admin/2rJrkqLzWt3MT4XxNsYn.png", alt="Bundle size of 53.1 KB (from 225KB)", width="724", height="97" %}

Success! You have used Brotli compression to further compress your assets!

Conclusion

This codelab illustrated how brotli can further reduce your app's overall size. Where supported, brotli is a more powerful compression algorithm than gzip.

{% Aside 'warning' %} Remember to check if your CDN supports brotli before manually implementing. If you need to implement brotli manually (as described in this codelab) but have CDN support for other compression algorithms such as gzip, it is a good idea to weigh the benefits of brotli against the effort required to implement. {% endAside %}

Two ways to prefetch: `<link>` tags and HTTP headers

{% include 'content/devtools-headsup.njk' %}

In this codelab, you'll implement prefetching in two ways: with <link rel="prefetch"> and with HTTP Link header.

The sample app is a website that has a promotional landing page with a special discount for the shop's best selling t-shirt. Since the landing page links to a single product, it's safe to assume that a high percentage of users will navigate to the product details page. This makes the product page a great candidate to prefetch on the landing page.

Measure performance

First establish the baseline performance:

{% Instruction 'remix', 'ol' %} {% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %}

  1. In the Throttling drop-down list, select Fast 3G to simulate a slow connection type.
  2. To load the product page, click Buy now in the sample app.

The product-details.html page takes about 600 ms to load:

{% Img src="image/admin/MVpybZcY1aF8slLFgJ0n.png", alt="Network panel showing load times for product-details.html", width="800", height="186" %}

Prefetch the product page with <link rel="prefetch">

To improve navigation, insert a prefetch tag in the landing page to prefetch the product-details.html page:

<!doctype html>
  <html>
    <head>
       <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">

      <link rel="prefetch" href="/product-details.html" as="document">
	  ...
</head>

The as attribute is optional but recommended; it helps the browser set the right headers and determine whether the resource is already in the cache. Example values for this attribute include: document, script, style, font, image, and others.

To verify that prefetching is working:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %}

  1. In the Throttling drop-down list, select Fast 3G to simulate a slow connection type.
  2. Clear the Disable cache checkbox. {% Instruction 'reload-app', 'ol' %}

Now when the landing page loads, the product-details.html page loads too, but at the lowest priority:

{% Img src="image/admin/LDkU6zNbFU7GhPuUcaCR.png", alt="Network panel showing product-details.html prefetched.", width="800", height="172" %}

The page is kept in the HTTP cache for five minutes, after which the normal Cache-Control rules for the document apply. In this case, product-details.html has a cache-control header with a value of public, max-age=0, which means that the page is kept for a total of five minutes.

Reevaluate performance

{% Instruction 'reload-app', 'ol' %}

  1. To load the product page, click Buy now in the sample app.

Take a look at the Network panel. There are two differences compared to the initial network trace:

This is approximately a 98% reduction compared to the previous version, which took about 600 ms.

{% Img src="image/admin/gJZPsifaqFPozkhnMznX.png", alt="Network panel showing product-details.html retrieved from prefetch cache.", width="800", height="223" %}

Extra credit: Use prefetch as a progressive enhancement

Prefetching is best implemented as a progressive enhancement for the users who are browsing on fast connections. You can use the Network Information API to check the network conditions and based on that dynamically inject prefetch tags. That way, you can minimize data consumption and save costs for users on slow or expensive data plans.

To implement adaptive prefetching, first remove the <link rel="prefetch"> tag from views/index.html:

<!doctype html>
  <html>
    <head>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
	   <link rel="prefetch" href="/product-details.html" as="document">
	   ...
	</head>

Then add the following code to public/script.js to declare a function that dynamically injects the prefetch tag when the user is on a fast connection:

function injectLinkPrefetchIn4g(url) {
	if (window.navigator.connection.effectiveType === '4g') {
		//generate link prefetch tag
		const linkTag = document.createElement('link');
		linkTag.rel = 'prefetch';
		linkTag.href = url;
		linkTag.as = 'document';

		//inject tag in the head of the document
		document.head.appendChild(linkTag);
	}
}

The function works as follows:

Next add script.js to views/index.html, just before the closing </body> tag:

<body>
      ...
      <script src="/script.js"></script>
</body>

Requesting script.js at the end of the page ensures that it will be loaded and executed after the page is parsed and loaded.

To make sure that the prefetching doesn't interfere with critical resources for the current page, add the following code snippet to call injectLinkPrefetchIn4g() on the window.load event:

<body>
      ...
      <script src="/script.js"></script>
      <script>
           window.addEventListener('load', () => {
                injectLinkPrefetchIn4g('/product-details.html');
           });
      </script>
</body>

The landing page now prefetches product-details.html only on fast connections. To verify that:

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %}

  1. In the Throttling drop-down list, select Online. {% Instruction 'reload-app', 'ol' %}

You should see product-details.html in the Network panel:

{% Img src="image/admin/NCoFDNGs0iSfBkDjiwzd.png", alt="Network panel showing product-details.html prefetched.", width="800", height="201" %}

To verify that the product page isn't prefetched on slow connections:

  1. In the Throttling drop-down list, select Slow 3G. {% Instruction 'reload-app', 'ol' %}

The Network panel should include only the resources for the landing page without product-details.html:

{% Img src="image/admin/xpHuregNQIEKrVylhG3G.png", alt="Network panel showing product-details.html not being prefetched.", width="800", height="171" %}

Prefetch the stylesheet for the product page with the HTTP Link header

  1. Open the server.js file and look for the get() handler for the root url: /.
  2. Add the following line at the beginning of the handler:
app.get('/', function(request, response) {
	response.set('Link', '</style-product.css>; rel=prefetch');
	response.sendFile(__dirname + '/views/index.html');
});

{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %} {% Instruction 'reload-app', 'ol' %}

The style-product.css is now prefetched at the lowest priority after the landing page loads:

{% Img src="image/admin/Memd8AIP4Yr5dhGGi240.png", alt="Network panel showing style-product.css prefetched.", width="800", height="205" %}

To navigate to the product page, click Buy now. Take a look at the Network panel:

{% Img src="image/admin/12tQkKKPqx4JjaWofEYK.png", alt="Network panel showing style-product.css retrieved from prefetch cache.", width="800", height="223" %}

The style-product.css file is retrieved from the "prefetch cache" and it took only 12 ms to load.

{% Aside %} When using HTTP Link header, you can decide whether to prefetch depending on the network conditions based on the information available in client hints. {% endAside %}

Lazy load offscreen images with lazysizes

Lazy loading is the approach of waiting to load resources until they are needed, rather than loading them in advance. This can improve performance by reducing the amount of resources that need to be loaded and parsed on initial page load.

Images that are offscreen during the initial pageload are ideal candidates for this technique. Best of all, lazysizes makes this a very simple strategy to implement.

Add the lazysizes script to the page

{% Instruction 'remix' %}

lazysizes.min.js has already been downloaded and added to this Glitch. To include it in the page:

  <script src="lazysizes.min.js" async></script>
  <!-- Images End -->
</body>

{% Aside %} The lazysizes.min.js file has already been added to this project, so there is no need to add it separately. The script you just added can use this script. {% endAside %}

lazysizes will intelligently load images as the user scrolls through the page and prioritize the images that the user is going to encounter soon.

Indicate the images to lazy load

For example, the changes for flower3.png would look like this:

<img src="images/flower3.png" alt="">
<img data-src="images/flower3.png" class="lazyload" alt="">

For this example, try lazy loading flower3.png, flower4.jpg, and flower5.jpg.

{% Aside %} You may be wondering why it is necessary to change the src attribute to data-src. If this attribute is not changed, all the images will load immediately instead of being lazy-loaded. data-src is not an attribute that the browser recognizes, so when it encounters an image tag with this attribute, it doesn't load the image. In this case, that is a good thing, because it then allows the lazysizes script to decide when the image should be loaded, rather than the browser. {% endAside %}

See it in action

That's it! To see these changes in action, follow these steps:

{% Instruction 'preview' %}

{% Img src="image/admin/yXej5KAOMzoqoQAB2paq.png", alt="Images being lazy loaded", width="428", height="252" %}

{% Img src="image/admin/tcQpLeAubOW1l42eyXiW.png", alt="Images being lazy loaded", width="418", height="233" %}

Verify using Lighthouse

Lastly, it's a good idea to use Lighthouse to verify these changes. Lighthouse's "Defer offscreen images" performance audit will indicate if you've forgotten to add lazy loading to any offscreen images.

{% Instruction 'preview', 'ol' %} {% Instruction 'audit-performance', 'ol' %}

  1. Verify the Defer offscreen images audit was passed.

{% Img src="image/admin/AWMJnCEi3IAgANHhTgiC.png", alt="Passing 'Efficiently encode images' audit in Lighthouse", width="800", height="774" %}

Success! You have used lazysizes to lazy load the images on your page.

Choose the correct level of compression

Images often account for most of the downloaded bytes on a web page and also often occupy a significant amount of visual space. As a result, optimizing images can often yield some of the largest byte savings and performance improvements for your website: the fewer bytes the browser has to download, the less competition there is for the client's bandwidth and the faster the browser can download and render useful content on the screen.

Image optimization is both an art and science: an art because there is no one definitive answer for how best to compress an individual image, and a science because there are many well developed techniques and algorithms that can significantly reduce the size of an image. Finding the optimal settings for your image requires careful analysis along many dimensions: format capabilities, content of encoded data, quality, pixel dimensions, and more.

Optimizing vector images

All modern browsers support Scalable Vector Graphics (SVG), which is an XML-based image format for two-dimensional graphics. You can embed the SVG markup directly on the page or as an external resource. Most vector-based drawing software can create SVG files or you can write them by hand directly in your favorite text editor.

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
    x="0px" y="0px" viewBox="0 0 612 792" xml:space="preserve">
<g id="XMLID_1_">
  <g>
    <circle fill="red" stroke="black" stroke-width="2" stroke-miterlimit="10" cx="50" cy="50" r="40"/>
  </g>
</g>
</svg>

The above example renders the below simple circle shape with a black outline and red background and was exported from Adobe Illustrator.

<?xml version="1.0" encoding="utf-8"?>

As you can tell, it contains a lot of metadata, such as layer information, comments, and XML namespaces that are often unnecessary to render the asset in the browser. As a result, it is always a good idea to minify your SVG files by running through a tool like SVGO.

Case in point, SVGO reduces the size of the above SVG file generated by Illustrator by 58%, taking it from 470 to 199 bytes.

<svg version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792"><circle fill="red" stroke="#000" stroke-width="2" stroke-miterlimit="10" cx="50" cy="50" r="40"/></svg>

Because SVG is an XML-based format, you can also apply GZIP compression to reduce its transfer size—make sure your server is configured to compress SVG assets!

A raster image is simply a two-dimensional grid of individual "pixels"—for example, a 100x100 pixel image is a sequence of 10,000 pixels. In turn, each pixel stores the "RGBA" values: (R) red channel, (G) green channel, (B) blue channel, and (A) alpha (transparency) channel.

Internally, the browser allocates 256 values (shades) for each channel, which translates to 8 bits per channel (2 ^ 8 = 256), and 4 bytes per pixel (4 channels x 8 bits = 32 bits = 4 bytes). As a result, if we know the dimensions of the grid we can easily calculate the filesize:

{% Aside %} As an aside, regardless of the image format used to transfer the data from the server to the client, when the image is decoded by the browser, each pixel always occupies 4 bytes of memory. This can be an important constraint for large images and devices which do not have a lot of available memory —for example, low-end mobile devices. {% endAside %}

Dimensions Pixels File size
100 x 100 10,000 39 KB
200 x 200 40,000 156 KB
300 x 300 90,000 351 KB
500 x 500 250,000 977 KB
800 x 800 640,000 2500 KB

39 KB for a 100x100 pixel image may not seem like a big deal, but the filesize quickly explodes for larger images and makes image assets both slow and expensive to download. This post has so far only focused on the "uncompressed" image format. Thankfully, a lot can be done to reduce the image file size.

One simple strategy is to reduce the "bit-depth" of the image from 8 bits per channel to a smaller color palette: 8 bits per channel gives us 256 values per channel and 16,777,216 (256 ^ 3) colors in total. What if you reduce the palette to 256 colors? Then you would only need 8 bits in total for the RGB channels and immediately save two bytes per pixel—that's 50% compression savings over the original 4 bytes per pixel format!

{% Img src="image/admin/ssek7uXzhs67joEbp0P8.png", alt="Compression artifacts", width="612", height="266" %}
Left to right (PNG): 32-bit (16M colors), 7-bit (128 colors), 5-bit (32 colors).

Complex scenes with gradual color transitions (for example, gradients or sky) require larger color palettes to avoid visual artifacts such as the pixelated sky in the 5-bit asset. On the other hand, if the image only uses a few colors, then a large palette is simply wasting precious bits!

Next, once you've optimized the data stored in individual pixels you could get more clever and look at nearby pixels as well: turns out, many images, and especially photos, have many nearby pixels with similar colors— for example, the sky, repeating textures, and so on. Using this information to your advantage the compressor can apply delta encoding where instead of storing the individual values for each pixel, you can store the difference between nearby pixels: if the adjacent pixels are the same, then the delta is "zero" and you only need to store a single bit! But why stop there…

The human eye has different level of sensitivity to different colors: you can optimize your color encoding to account for this by reducing or increasing the palette for those colors. "Nearby" pixels form a two-dimensional grid. This means that each pixel has multiple neighbors: you can use this fact to further improve delta encoding. Instead of looking at just the immediate neighbors for each pixel, you can look at larger blocks of nearby pixels and encode different blocks with different settings.

As you can tell, image optimization gets complicated quickly (or fun, depending on your perspective), and is an active area of academic and commercial research. Images occupy a lot of bytes and there is a lot of value in developing better image compression techniques! If you're curious to learn more, head to the Wikipedia page, or check out the WebP compression techniques whitepaper for a hands-on example.

So, once again, this is all great, but also very academic: how does it help you to optimize images on your site? Well, it's important to understand the shape of the problem: RGBA pixels, bit-depth, and various optimization techniques. All of these concepts are critical to understand and keep in mind before you dive into the discussions of various raster image formats.

Lossless versus lossy image compression

For certain types of data, such as source code for a page, or an executable file, it is critical that a compressor does not alter or lose any of the original information: a single missing or wrong bit of data could completely change the meaning of the contents of the file, or worse, break it entirely. For some other types of data, such as images, audio, and video, it may be perfectly acceptable to deliver an "approximate" representation of the original data.

In fact, due to how the eye works, we can often get away with discarding some information about each pixel in order to reduce the filesize of an image— for example, our eyes have different sensitivity to different colors, which means that we can use fewer bits to encode some colors. As a result, a typical image optimization pipeline consists of two high level steps:

  1. Image is processed with a lossy filter that eliminates some pixel data.
  2. Image is processed with a lossless filter that compresses the pixel data.

The first step is optional, and the exact algorithm will depend on the particular image format, but it is important to understand that any image can undergo a lossy compression step to reduce its size. In fact, the difference between various image formats, such as GIF, PNG, JPEG, and others, is in the combination of the specific algorithms they use (or omit) when applying the lossy and lossless steps.

So, what is the "optimal" configuration of lossy and lossless optimization? The answer depends on the image contents and your own criteria such as the tradeoff between filesize and artifacts introduced by lossy compression: In some cases, you may want to skip lossy optimization to communicate intricate detail in its full fidelity. In other cases, you may be able to apply aggressive lossy optimization to reduce the filesize of the image asset. This is where your own judgment and context need to come into play—there is no one universal setting.

As a hands-on example, when using a lossy format such as JPEG, the compressor will typically expose a customizable "quality" setting (for example, the quality slider provided by the "Save for Web" functionality in Adobe Photoshop), which is typically a number between 1 and 100 that controls the inner workings of the specific collection of lossy and lossless algorithms. For best results, experiment with various quality settings for your images, and don't be afraid to dial down the quality—the visual results are often very good and the filesize savings can be quite large.

{% Aside %} Note that quality levels for different image formats are not directly comparable due to differences in algorithms used to encode the image: quality 90 JPEG will produce a very different result than a quality 90 WebP. In fact, even quality levels for the same image format may produce visibly different output based on implementation of the compressor! {% endAside %}

Effects of image compression on Core Web Vitals

Because images are often candidates for Largest Contentful Paint, reducing the resource load time of an image can translate into better LCP in both the lab and in the field.

When playing with compression settings on raster image formats, be sure to experiment with WebP and AVIF formats to see if you can deliver the same image in a small footprint as compared to older formats.

You want to be careful not to overcompress raster images, though. A good solution is to use an image optimization CDN to find the best compression settings for you, but an alternative may be to use tools like Butteraugli to estimate visual differences so that you don't encode images too aggressively and lose too much quality.

Image optimization checklist

Some tips and techniques to keep in mind as you work on optimizing your images:

Content delivery networks (CDNs)

Content delivery networks (CDNs) improve site performance by using a distributed network of servers to deliver resources to users. Because CDNs reduce server load, they reduce server costs and are well-suited to handling traffic spikes. This article discusses how CDNs work and provides platform-agnostic guidance on choosing, configuring, and optimizing a CDN setup.

Overview

A content delivery network consists of a network of servers that are optimized for quickly delivering content to users. Although CDNs are arguably best known for serving cached content, CDNs can also improve the delivery of uncacheable content. Generally speaking, the more of your site delivered by your CDN, the better.

At a high-level, the performance benefits of CDNs stem from a handful of principles: CDN servers are located closer to users than origin servers and therefore have a shorter round-trip time (RTT) latency; networking optimizations allow CDNs to deliver content more quickly than if the content was loaded "directly" from the origin server; lastly, CDN caches eliminate the need for a request to travel to the origin server.

{% Aside 'key-term' %} Origin server refers to the server that a CDN retrieves content from. {% endAside %}

Resource delivery

Although it may seem non-intuitive, using a CDN to deliver resources (even uncacheable ones) will typically be faster than having the user load the resource "directly" from your servers.

When a CDN is used to deliver resources from the origin, a new connection is established between the client and a nearby CDN server. The remainder of the journey (in other words, the data transfer between the CDN server and origin) occurs over the CDN's network - which often includes existing, persistent connections with the origin. The benefits of this are twofold: terminating the new connection as close to the user as possible eliminates unnecessary connection setup costs (establishing a new connection is expensive and requires multiple roundtrips); using a pre-warmed connection allows data to be immediately transferred at the maximum possible throughput.

{% Img src="image/admin/M9kzM7J7FenUyO7E9MF0.png", alt="Comparison of connection setup with and without a CDN", width="800", height="512" %}

Some CDNs improve upon this even further by routing traffic to the origin through multiple CDN servers spread across the Internet. Connections between CDN servers occur over reliable and highly optimized routes, rather than routes determined by the Border Gateway Protocol (BGP). Although BGP is the internet's de facto routing protocol, its routing decisions are not always performance-oriented. Therefore, BGP-determined routes are likely to be less performant than the finely-tuned routes between CDN servers.

{% Img src="image/admin/ZLMPFySQgBkpWvgujuJP.png", alt="Comparison of connection setup with and without a CDN", width="800", height="449" %}

Caching

Caching resources on a CDN's servers eliminates the need for a request to travel all the way to the origin in order to be served. As a result, the resource is delivered more quickly; this also reduces the load on the origin server.

Adding resources to the cache

The most commonly used method of populating CDN caches is to have the CDN "pull" resources as they are needed - this is known as "origin pull". The first time that a particular resource is requested from the cache the CDN will request it from the origin server and cache the response. In this manner, the contents of the cache are built-up over time as additional uncached resources are requested.

Removing resources from the cache

CDNs use cache eviction to periodically remove not-so-useful resources from the cache. In addition, site owners can use purging to explicitly remove resources.

Cacheable resources

If and how a resource should be cached depends on whether it is public or private; static or dynamic.

Private and public resources
Dynamic and static content

Choosing a CDN

Performance is typically a top consideration when choosing a CDN. However, the other features that a CDN offers (for example, security and analytics features), as well as a CDN's pricing, support, and onboarding are all important to consider when choosing a CDN.

Performance

At a high-level, a CDN's performance strategy can be thought of in terms of the tradeoff between minimizing latency and maximizing cache hit ratio. CDNs with many points of presence (PoPs) can deliver lower latency but may experience lower cache hit ratios as a result of traffic being split across more caches. Conversely, CDNs with fewer PoPs may be located geographically further from users, but can achieve higher cache hit ratios.

As a result of this tradeoff, some CDNs use a tiered approach to caching: PoPs located close to users (also known as "edge caches") are supplemented with central PoPs that have higher cache hit ratios. When an edge cache can't find a resource, it will look to a central PoP for the resource. This approach trades slightly greater latency for a higher likelihood that the resource can be served from a CDN cache - though not necessarily an edge cache.

The tradeoff between minimizing latency and maximizing cache hit ratio is a spectrum. No particular approach is universally better; however, depending on the nature of your site and its user base, you may find that one of these approaches delivers significantly better performance than the other.

It's also worth noting that CDN performance can vary significantly depending on geography, time of day, and even current events. Although it's always a good idea to do your own research on a CDN's performance, it can be difficult to predict the exact performance you'll get from a CDN.

Additional features

CDNs typically offer a wide variety of features in addition to their core CDN offering. Commonly offered features include: load balancing, image optimization, video streaming, edge computing, and security products.

How to setup and configure a CDN

Ideally you should use a CDN to serve your entire site. At a high-level, the setup process for this consists of signing up with a CDN provider, then updating your CNAME DNS record to point at the CDN provider. For example, the CNAME record for www.example.com might point to example.my-cdn.com. As a result of this DNS change, traffic to your site will be routed through the CDN.

If using a CDN to serve all resources is not an option, you can configure a CDN to only serve a subset of resources - for example, only static resources. You can do this by creating a separate CNAME record that will only be used for resources that should be served by the CDN. For example, you might create a static.example.com CNAME record that points to example.my-cdn.com. You would also need to rewrite the URLs of resources being served by the CDN to point to the static.example.com subdomain that you created.

Although your CDN will be set up at this point, there will likely be inefficiencies in your configuration. The next two sections of this article will explain how to get the most out of your CDN by increasing cache hit ratio and enabling performance features.

Improving cache hit ratio

An effective CDN setup will serve as many resources as possible from the cache. This is commonly measured by cache hit ratio (CHR). Cache hit ratio is defined as the number of cache hits divided by the number of total requests during a given time interval.

A freshly initialized cache will have a CHR of 0 but this increases as the cache is populated with resources. A CHR of 90% is a good goal for most sites. Your CDN provider should supply you with analytics and reporting regarding your CHR.

When optimizing CHR, the first thing to verify is that all cacheable resources are being cached and cached for the correct length of time. This is a simple assessment that should be undertaken by all sites.

The next level of CHR optimization, broadly speaking, is to fine tune your CDN settings to make sure that logically equivalent server responses aren't being cached separately. This is a common inefficiency that occurs due to the impact of factors like query params, cookies, and request headers on caching.

Initial audit

Most CDNs will provide cache analytics. In addition, tools like WebPageTest and Lighthouse can also be used to quickly verify that all of a page's static resources are being cached for the correct length of time. This is accomplished by checking the HTTP Cache headers of each resource. Caching a resource using the maximum appropriate Time To Live (TTL) will avoid unnecessary origin fetches in the future and therefore increase CHR.

At a minimum, one of these headers typically needs to be set in order for a resource to be cached by a CDN:

In addition, although it does not impact if or how a resource is cached by a CDN, it is good practice to also set the Cache-Control: immutable directive.Cache-Control: immutable indicates that a resource "will not be updated during its freshness lifetime". As a result, the browser will not revalidate the resource when serving it from the browser cache, thereby eliminating an unnecessary server request. Unfortunately, this directive is only supported by Firefox and Safari - it is not supported by Chromium-based browsers. This issue tracks Chromium support for Cache-Control: immutable. Starring this issue can help encourage support for this feature.

For a more detailed explanation of HTTP caching, refer to Prevent unnecessary network requests with the HTTP Cache.

Fine tuning

A slightly simplified explanation of how CDN caches work is that the URL of a resource is used as the key for caching and retrieving the resource from the cache. In practice, this is still overwhelmingly true, but is complicated slightly by the impact of things like request headers and query params. As a result, rewriting request URLs is an important technique for both maximizing CHR and ensuring that the correct content is served to users. A properly configured CDN instance strikes the correct balance between overly granular caching (which hurts CHR) and insufficiently granular caching (which results in incorrect responses being served to users).

Query params

By default, CDNs take query params into consideration when caching a resource. However, small adjustments to query param handling can have a significant impact on CHR. For example:

Vary

The Vary response header informs caches that the server response corresponding to a particular URL can vary depending on the headers set on the request (for example, the Accept-Language or Accept-Encoding request headers). As a result, it instructs a CDN to cache these responses separately. The Vary header is not widely supported by CDNs and may result in an otherwise cacheable resource not being served from a cache.

Although the Vary header can be a useful tool, inappropriate usage hurts CHR. In addition, if you do use Vary, normalizing request headers will help improve CHR. For example, without normalization the request headers Accept-Language: en-US and Accept-Language: en-US,en;q=0.9 would result in two separate cache entries, even though their contents would likely be identical.

Cookies

Cookies are set on requests via the Cookie header; they are set on responses via the Set-Cookie header. Unnecessary use of Set-Cookie header should be avoided given that caches will typically not cache server responses containing this header.

Performance features

This section discusses performance features that are commonly offered by CDNs as part of their core product offering. Many sites forget to enable these features, thereby losing out on easy performance wins.

Compression

All text-based responses should be compressed with either gzip or Brotli. If you have the choice, choose Brotli over gzip. Brotli is a newer compression algorithm, and compared to gzip, it can achieve higher compression ratios.

There are two types of CDN support for Brotli compression: "Brotli from origin" and "automatic Brotli compression".

Brotli from origin

Brotli from origin is when a CDN serves resources that were Brotli-compressed by the origin. Although this may seem like a feature that all CDNs should be able to support out of the box, it requires that a CDN be able to cache multiple versions (in other words, gzip-compressed and Brotli-compressed versions) of the resource corresponding to a given URL.

Automatic Brotli compression

Automatic Brotli compression is when resources are Brotli compressed by the CDN. CDNs can compress both cacheable and non-cacheable resources.

The first time that a resource is requested it is served using "good enough" compression - for example, Brotli-5. This type of compression is applicable to both cacheable and non-cacheable resources.

Meanwhile, if a resource is cacheable, the CDN will use offline processing to compress the resource at a more powerful but far slower compression level - for example, Brotli-11. Once this compression completes, the more compressed version will be cached and used for subsequent requests.

Compression best practices

Sites that want to maximize performance should apply Brotli compression at both their origin server and CDN. Brotli compression at the origin minimizes the transfer size of resources that can't be served from the cache. To prevent delays in serving requests, the origin should compress dynamic resources using a fairly conservative compression level - for example, Brotli-4; static resources can be compressed using Brotli-11. If an origin does not support Brotli, gzip-6 can be used to compress dynamic resources; gzip-9 can be used to compress static resources.

TLS 1.3

TLS 1.3 is the newest version of Transport Layer Security (TLS), the cryptographic protocol used by HTTPS. TLS 1.3 provides better privacy and performance compared to TLS 1.2.

TLS 1.3 shortens the TLS handshake from two roundtrips to one. For connections using HTTP/1 or HTTP/2, shortening the TLS handshake to one roundtrip effectively reduces connection setup time by 33%.

{% Img src="image/admin/FnCSj1W23jXaiOWCp0Bw.png", alt="Comparison of the TLS 1.2 and TLS 1.3 handshakes", width="800", height="448" %}

HTTP/2 and HTTP/3

HTTP/2 and HTTP/3 both provide performance benefits over HTTP/1. Of the two, HTTP/3 offers greater potential performance benefits. HTTP/3 isn't fully standardized yet, but it will be widely supported once this occurs.

HTTP/2

If your CDN hasn't already enabled HTTP/2 by default, you should consider turning it on. HTTP/2 provides multiple performance benefits over HTTP/1 and is supported by all major browsers. Performance features of HTTP/2 include: multiplexing, stream prioritization, server push, and header compression.

Stream prioritization is expressed by the browser via a dependency tree and is merely a statement of preference: in other words, the server is not obligated to meet (or even consider) the priorities supplied by the browser. Stream prioritization becomes more effective when more of a site is served through a CDN.

CDN implementations of HTTP/2 resource prioritization vary wildly. To identify whether your CDN fully and properly supports HTTP/2 resource prioritization, check out Is HTTP/2 Fast Yet?.

Although switching your CDN instance to HTTP/2 is largely a matter of flipping a switch, it's important to thoroughly test this change before enabling it in production. HTTP/1 and HTTP/2 use the same conventions for request and response headers - but HTTP/2 is far less forgiving when these conventions aren't adhered to. As a result, non-spec practices like including non-ASCII or uppercase characters in headers may begin causing errors once HTTP/2 is enabled. If this occurs, a browser's attempts to download the resource will fail. The failed download attempt will be visible in the "Network" tab of DevTools. In addition, the error message "ERR_HTTP2_PROTOCOL_ERROR" will be displayed in the console.

HTTP/3

HTTP/3 is the successor to HTTP/2. As of September 2020, all major browsers have experimental support for HTTP/3 and some CDNs support it. Performance is the primary benefit of HTTP/3 over HTTP/2. Specifically, HTTP/3 eliminates head-of-line blocking at the connection level and reduces connection setup time.

{% Img src="image/admin/B7YKfqGG4eS2toSoTDdS.png", alt="Diagram showing the differences in data transmission between HTTP/1, HTTP/2, and HTTP/3", width="800", height="449" %}
{% Img src="image/admin/7ffDEjblsisTNsfkynt6.png", alt="Comparison of connection resumption between TLS 1.2, TLS 1.3, TLS 1.3 0-RTT, and HTTP/3", width="800", height="400" %}

HTTP/3 will have the biggest impact on users on poor network connections: not only because HTTP/3 handles packet loss better than its predecessors, but also because the absolute time savings resulting from a 0-RTT or 1-RTT connection setup will be greater on networks with high latency.

Image optimization

CDN image optimization services typically focus on image optimizations that can be applied automatically in order to reduce image transfer size. For example: stripping EXIF data, applying lossless compression, and converting images to newer file formats (for example, WebP). Images make up ~50% of the transfer bytes on the median web page, so optimizing images can significantly reduce page size.

Minification

Minification removes unnecessary characters from JavaScript, CSS, and HTML. It's preferable to do minification at the origin server, rather than the CDN. Site owners have more context about the code to be minified and therefore can often use more aggressive minification techniques than those employed by CDNs. However, if minifying code at the origin is not an option, minification by the CDN is a good alternative.

Conclusion

Best practices for cookie notices

This article discusses how cookie notices can affect performance, performance measurement, and user experience.

Performance

Generally speaking, you can expect a cookie notice from third-party providers to have a greater impact on performance than a cookie notice that you build yourself. This is not a problem unique to cookie notices—but rather the nature of third-party scripts in general.

Best practices

The best practices in this section focus on third-party cookie notices. Some, but not all, of these best practices will also be applicable to first-party cookie notices.

<script src="https://cookie-notice.com/script.js" async>

Scripts that are not asynchronous block the browser parser. This delays page load and LCP. For more information, see Efficiently load third-party JavaScript.

{% Aside %} If you must use synchronous scripts (for example, some cookie notices rely on synchronous scripts to implement cookie blocking) you should make sure that this request loads as quickly as possible. One way to do this is to use resource hints. {% endAside %}

Establish an early connection with the cookie notice origin

All sites that load their cookie notice scripts from a third-party location should use either the dns-prefetch or preconnect resource hints to help establish an early connection with the origin that hosts cookie notice resources. For more information, see Establish network connections early to improve perceived page speed.

{% Aside %} It is common for cookie notices to load resources from multiple origins— for example, loading resources from both www.cookie-notice.com and cdn.cookie-notice.com. Separate origins require separate connections and therefore separate resource hints. {% endAside %}

Some sites would benefit from using the preload resource hint to load their cookie notice script. The preload resource hint informs the browser to initiate an early request for the specified resource.

preload is most powerful when its usage is limited to fetching a couple key resources per page. Thus, the usefulness of preloading the cookie notice script will vary depending on the situation.

Be aware of performance tradeoffs when styling cookie notices

Customizing the look and feel of a third-party cookie notice may incur additional performance costs. For example, third-party cookie notices aren't always able to reuse the same resources (for example, web fonts) that are used elsewhere on the page. In addition, third-party cookie notices tend to load styling at the end of long request chains. To avoid any surprises, be aware of how your cookie notice loads and applies styling and related resources.

Avoid layout shifts

These are some of the most common layout shift issues associated with cookie notices:

Advanced loading optimizations

These techniques take more work to implement but can further optimize the loading of cookie notice scripts:

Performance measurement

Real User Monitoring (RUM)

Some analytics and RUM tools use cookies to collect performance data. In the event that a user declines usage of cookies these tools cannot capture performance data.

Sites should be aware of this phenomenon; it is also worthwhile to understand the mechanisms that your RUM tooling uses to collect its data. However, for the typical site this discrepancy probably isn't a cause for alarm given the direction and magnitude of the data skew. Cookie usage is not a technical requirement for performance measurement. The web-vitals JavaScript library is an example of a library that does not use cookies.

Depending on how your site uses cookies to collect performance data (that is, whether the cookies contain personal information), as well as the legislation in question, the use of cookies for performance measurement might not be subject to the same legislative requirements as some of the cookies used on your site for other purposes—for example, advertising cookies. Some sites choose to break out performance cookies as a separate category of cookies when asking for user consent.

Synthetic monitoring

Without custom configuration, most synthetic tools (such as Lighthouse and WebPageTest) will only measure the experience of a first-time user who has not responded to a cookie consent notice. However, not only do variations in cache state (for example, an initial visit versus a repeat visit) need to be considered when collecting performance data, but also variations in cookie acceptance state—accepted, rejected, or unresponded.

The following sections discuss WebPageTest and Lighthouse settings that can be helpful for incorporating cookie notices into performance measurement workflows. However, cookies and cookie notices are just one of many factors that can be difficult to perfectly simulate in lab environments. For this reason, it is important to make RUM data the cornerstone of your performance benchmarking, rather than synthetic tooling.

Scripting

You can use scripting to have a WebPageTest "click" the cookie consent banner while collecting a trace.

Add a script by going to the Script tab. The script below navigates to the URL to be tested and then clicks the DOM element with the id cookieButton.

{% Aside 'caution' %} WebPageTest scripts are tab-delimited. {% endAside %}

combineSteps
navigate    %URL%
clickAndWait    id=cookieButton

When using this script be aware that:

If you've configured this script correctly, the screenshot taken by WebPageTest should not show a cookie notice (the cookie notice has been accepted).

For more information on WebPageTest scripting, check out WebPageTest documentation.

Set cookies

To run WebPageTest with a cookie set, go to the Advanced tab and add the cookie header to the Custom headers field:

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/qSccrAxF0H4yoSzYRYdh.png", alt="Screeshot showing the 'Custom headers' field in WebPageTest", width="800", height="181" %}

Change the test location

To change the test location used by WebPageTest, click the Test Location dropdown located on the Advanced Testing tab.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/J27NcDQ5LTtXYloaA1DN.png", alt="Screenshot of the 'Test Location' dropdown in WebPageTest", width="800", height="267" %}

Setting cookies on a Lighthouse run can serve as a mechanism for getting a page into a particular state for testing by Lighthouse. Lighthouse's cookie behavior varies slightly by context (DevTools, CLI, or PageSpeed Insights).

DevTools

Cookies are not cleared when Lighthouse is run from DevTools. However, other types of storage are cleared by default. This behavior can be changed by using the Clear Storage option in the Lighthouse settings panel.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/nmNDeSoGEQUVKeTP7q7R.png", alt="Screenshot higlighting the Lighthouse 'Clear Storage' option", width="800", height="304" %}

CLI

Running Lighthouse from the CLI uses a fresh Chrome instance, so no cookies are set by default. To run Lighthouse from the CLI with a particular cookie set, use the following command:

lighthouse <url> --extra-headers "{\"Cookie\":\"cookie1=abc; cookie2=def; \_id=foo\"}"

For more information on setting custom request headers in Lighthouse CLI, see Running Lighthouse on Authenticated Pages.

PageSpeed Insights

Running Lighthouse from PageSpeed Insights uses a fresh Chrome instance and does not set any cookies. PageSeed Insights cannot be configured to set particular cookies.

User experience

The user experience (UX) of different cookie consent notices is primarily the result of two decisions: the location of the cookie notice within the page and the extent to which the user can customize a site's use of cookies. This section discusses potential approaches to these two decisions.

{% Aside 'caution' %} Cookie notice UX is often heavily influenced by legislation which can vary widely by geography. Thus, some of the design patterns discussed in this section may not be relevant to your particular situation. This article should not be considered a substitute for legal advice. {% endAside %}

When considering potential designs for your cookie notice, here are some things to think about:

Placement

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/LLqHAhp7W6x4E3rZh0Oc.png", alt="Diagram showing examples of different placement options for cookie notices", width="800", height="345" %}

Header, footer, and inline cookie notices

Although inline cookie notices are an option, they can be difficult to integrate into existing user interfaces, and therefore are uncommon.

Modals

Smaller, partial-screen modals can be a good alternative for sites that are struggling to implement cookie notices in a way that doesn't cause layout shifts.

On the other hand, large modals that obscure the majority of page content should be used carefully. In particular, smaller sites may find that users bounce rather than accept the cookie notice of an unfamiliar site with obscured content. Although they are not necessarily synonymous concepts, if you are considering using a full-screen cookie consent modal, you should be aware of legislation regarding cookie walls.

{% Aside %} Large modals can be considered a type of interstitial. Google Search does not penalize the usage of interstitials when they are used to comply with legal regulations such as in the case of cookie banners. However, their usage of interstitials in other contexts—particularly if they are intrusive or create a poor user experience—may be penalized. {% endAside %}

Configurability

No configurability

These notice-style cookie banners do not present users with direct UX controls for opting out of cookies. Instead, they typically include a link to the site's cookie policy which may provide users with information about managing cookies using their web browser. These notices typically include either a "dismiss" and/or "Accept" button.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/RlAg8DCjBC0bX7Ki5MuE.png", alt="Diagram showing examples of cookie notices with no cookie configurability", width="800", height="518" %}

Some configurability

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/MOl8u9NcnyjCWogxzjdz.png", alt="Diagram showing examples of cookie notices with some cookie configurability", width="800", height="508" %}

Full configurability

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/QfFoqkkmdKHAYlftIH0n.png", alt="Diagram showing examples of chookie notices with full cookie configurability", width="800", height="467" %}

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/z7zFPtCkFi8GEpkfubek.png", alt="Diagram showing examples of cookie notices with full cookie configurability", width="800", height="372" %}

Debug layout shifts

The first part of this article discusses tooling for debugging layout shifts, while the second part discusses the thought process to use when identifying the cause of a layout shift.

Tooling

Layout Instability API

The Layout Instability API is the browser mechanism for measuring and reporting layout shifts. All tools for debugging layout shifts, including DevTools, are ultimately built upon the Layout Instability API. However, using the Layout Instability API directly is a powerful debugging tool due to its flexibility.

{% Aside %} The Layout Instability API is only supported by Chromium browsers. At the current time there is no way to measure or debug layout shifts in non-Chromium browsers. {% endAside %}

Usage

The same code snippet that measures Cumulative Layout Shift (CLS) can also serve to debug layout shifts. The snippet below logs information about layout shifts to the console. Inspecting this log will provide you information about when, where, and how a layout shift occurred.

let cls = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      console.log('Current CLS value:', cls, entry);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

When running this script be aware that:

Information about layout shifts is reported using a combination of two APIs: the LayoutShift and LayoutShiftAttribution interfaces. Each of these interfaces are explained in more detail in the following sections.

LayoutShift

Each layout shift is reported using the LayoutShift interface. The contents of an entry look like this:

duration: 0
entryType: "layout-shift"
hadRecentInput: false
lastInputTime: 0
name: ""
sources: (3) [LayoutShiftAttribution, LayoutShiftAttribution, LayoutShiftAttribution]
startTime: 11317.934999999125
value: 0.17508567530168798

The entry above indicates a layout shift during which three DOM elements changed position. The layout shift score of this particular layout shift was 0.175.

These are the properties of a LayoutShift instance that are most relevant to debugging layout shifts:

Property Description
sources The sources property lists the DOM elements that moved during the layout shift. This array can contain up to five sources. In the event that there are more than five elements impacted by the layout shift, the five largest (as measured by impact on layout stability) sources of layout shift are reported. This information is reported using the LayoutShiftAttribution interface (explained in more detail below).
value The value property reports the layout shift score for a particular layout shift.
hadRecentInput The hadRecentInput property indicates whether a layout shift occurred within 500 milliseconds of user input.
startTime The startTime property indicates when a layout shift occurred. startTime is indicated in milliseconds and is measured relative to the time that the page load was initiated.
duration The duration property will always be set to 0. This property is inherited from the PerformanceEntry interface (the LayoutShift interface extends the PerformanceEntry interface). However, the concept of duration does not apply to layout shift events, so it is set to 0. For information on the PerformanceEntry interface, refer to the spec.

{% Aside %} The Web Vitals Extension can log layout shift info to the console. To enable this feature, go to Options > Console Logging. {% endAside %}

LayoutShiftAttribution

The LayoutShiftAttribution interface describes a single shift of a single DOM element. If multiple elements shift during a layout shift, the sources property contains multiple entries.

For example, the JSON below corresponds to a layout shift with one source: the downward shift of the <div id='banner'> DOM element from y: 76 to y:246.

// ...
  "sources": [
    {
      "node": "div#banner",
      "previousRect": {
        "x": 311,
        "y": 76,
        "width": 4,
        "height": 18,
        "top": 76,
        "right": 315,
        "bottom": 94,
        "left": 311
      },
      "currentRect": {
        "x": 311,
        "y": 246,
        "width": 4,
        "height": 18,
        "top": 246,
        "right": 315,
        "bottom": 264,
        "left": 311
      }
    }
  ]

The node property identifies the HTML element that shifted. Hovering on this property in DevTools highlights the corresponding page element.

The previousRect and currentRect properties report the size and position of the node.

If all properties of previousRect are set to 0 this means that the element has shifted into view. If all properties of currentRect are set to 0 this means that the element has shifted out of view.

{% Aside 'caution' %} Layout Instability API currently does not ignore elements that shifted but were not visible due to another element being positioned in front of them. Use display: none, visibility: hidden, or opacity: 0 to avoid layout shifts in cases where you need to run layout on some elements before you make them visible to the user. {% endAside %}

One of the most important things to understand when interpreting these outputs is that elements listed as sources are the elements that shifted during the layout shift. However, it's possible that these elements are only indirectly related to the "root cause" of layout instability. Here are a few examples.

Example #1

This layout shift would be reported with one source: element B. However, the root cause of this layout shift is the change in size of element A.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/oaM41OYL7mFtGcpIN8KF.png", alt="Example showing a layout shift caused by a change in element dimensions", width="800", height="452" %}

Example #2

The layout shift in this example would be reported with two sources: element A and element B. The root cause of this layout shift is the change in position of element A.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/AhaslIEWb5fFMMgiZcI2.png", alt="Example showing a layout shift caused by a change in element position", width="800", height="451" %}

Example #3

The layout shift in this example would be reported with one source: element B. Changing the position of element B resulted in this layout shift.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/6zKjd4Ua6YJ94LMlqiMR.png", alt="Example showing a layout shift caused by a change in element position", width="800", height="451" %}

Example #4

Although element B changes size, there is no layout shift in this example.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/ZujHWxsXI3C7tupe42oD.png", alt="Example showing a element changing size but not causing a layout shift", width="800", height="446" %}

Check out a demo of how DOM changes are reported by the Layout Instability API.

DevTools

Performance panel

The Experience pane of the DevTools Performance panel displays all layout shifts that occur during a given performance trace—even if they occur within 500 ms of a user interaction and therefore don't count towards CLS. Hovering over a particular layout shift in the Experience panel highlights the affected DOM element.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/Uug2fnJT8mOc2YQmxo2l.png", alt="Screenshot of a layout shift displayed in the DevTools Network panel", width="724", height="629" %}

To view more information about the layout shift, click on the layout shift, then open the Summary drawer. Changes to the element's dimensions are listed using the format [width, height]; changes to the element's position are listed using the format [x,y]. The Had recent input property indicates whether a layout shift occurred within 500 ms of a user interaction.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/AfVjsH9Nl9w0lJwQZEjR.png", alt="Screenshot of the DevTools 'Summary' tab for a layout shift", width="612", height="354" %}

For information on the duration of a layout shift, open the Event Log tab. The duration of a layout shift can also be approximated by looking in the Experience pane for the length of the red layout shift rectangle.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/124Dm7vV3EGM7M9fiugs.png", alt="Screenshot of the DevTools 'Event Log' tab for a layout shift", width="612", height="354" %}

{% Aside %} The duration of a layout shift has no impact on its layout shift score. {% endAside %}

For more information on using the Performance panel, refer to Performance Analysis Reference.

Highlight layout shift regions

Highlighting layout shift regions can be a helpful technique for getting a quick, at-a-glance feel for the location and timing of the layout shifts occurring on a page.

To enable Layout Shift Regions in DevTools, go to Settings > More Tools > Rendering > Layout Shift Regions then refresh the page that you wish to debug. Areas of layout shift will be briefly highlighted in purple.

Thought process for identifying the cause of layout shifts

You can use the steps below to identify the cause of layout shifts regardless of when or how the layout shift occurs. These steps can be supplemented with running Lighthouse—however, keep in mind that Lighthouse can only identify layout shifts that occurred during the initial page load. In addition, Lighthouse also can only provide suggestions for some causes of layout shifts—for example, image elements that do not have explicit width and height.

Identifying the cause of a layout shift

Layout shifts can be caused by the following events:

In particular, the DOM element immediately preceding the shifted element is the element most likely to be involved in "causing" layout shift. Thus, when investigating why a layout shift occurred consider:

If the preceding element did not cause the layout shift, continue your search by considering other preceding and nearby elements.

In addition, the direction and distance of a layout shift can provide hints about root cause. For example, a large downward shift often indicates the insertion of a DOM element, whereas a 1 px or 2 px layout shift often indicates the application of conflicting CSS styles or the loading and application of a web font.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/g0892nhvz3SnSaasaO1b.png", alt="Diagram showing a layout shift caused by a font swap", width="800", height="452" %}
In this example, font swapping caused page elements to shift upwards by five pixels.

These are some of the specific behaviors that most frequently cause layout shift events:

Changes to the position of an element (that aren't due to the movement of another element)

This type of change is often a result of:

Changes to the dimensions of an element

This type of change is often a result of:

The insertion or removal of DOM elements

This is often the result of:

Animations that trigger layout

Some animation effects can trigger layout. A common example of this is when DOM elements are 'animated' by incrementing properties like top or left rather than using CSS's transform property. Read How to create high-performance CSS animations for more information.

Reproducing layout shifts

You can't fix layout shifts that you can't reproduce. One of the simplest, yet most effective things you can do to get a better sense of your site's layout stability is take 5-10 minutes to interact with your site with the goal triggering layout shifts. Keep the console open while doing this and use the Layout Instability API to report on layout shifts.

For hard to locate layout shifts, consider repeating this exercise with different devices and connection speeds. In particular, using a slower connection speed can make it easier to identify layout shifts. In addition, you can use a debugger statement to make it easier to step through layout shifts.

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      debugger;
      console.log('Current CLS value:', cls, entry);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

Lastly, for layout issues that aren't reproducible in development, consider using the Layout Instability API in conjunction with your front-end logging tool of choice to collect more information on these issues. Check out the example code for how to track the largest shifted element on a page.

Defer non-critical CSS

CSS files are render-blocking resources: they must be loaded and processed before the browser renders the page. Web pages that contain unnecessarily large styles take longer to render.

In this guide, you'll learn how to defer non-critical CSS with the goal of optimizing the Critical Rendering Path, and improving First Contentful Paint (FCP).

Loading CSS in a suboptimal way

The following example contains an accordion with three hidden paragraphs of text, each of which is styled with a different class:

{% Glitch { id: 'defer-css-unoptimized', path: 'index.html' } %}

This page requests a CSS file with eight classes, but not all of them are necessary to render the "visible" content.

The goal of this guide is to optimize this page, so only the critical styles are loaded synchronously, while the rest (like the ones applied to paragraphs), are loaded in a non-blocking way.

Measure

Run Lighthouse on the page and go to the Performance section.

The report shows the First Contentful Paint metric with a value of "1s", and the opportunity Eliminate render-blocking resources, pointing to the style.css file:

{% Img src="image/admin/eZtuQ2IwL3Mtnmz09bmp.png", alt="Lighthouse report for unoptimized page, showing FCP of '1s' and 'Eliminate blocking resources' under 'Opportunities'", width="800", height="640" %}

{% Aside %} The CSS we are using for this demo site is quite small. If you were requesting larger CSS files (which are not uncommon in production scenarios), and if Lighthouse detects that a page has at least 2048 bytes of CSS rules that weren't used while rendering the above the fold content, you'll also receive a suggestion called Remove Unused CSS. {% endAside %}

To visualize how this CSS blocks rendering:

  1. Open the page in Chrome. {% Instruction 'devtools-performance', 'ol' %}
  2. In the Performance panel, click Reload.

In the resulting trace, you'll see that the FCP marker is placed immediately after the CSS finishes loading:

{% Img src="image/admin/WhpaDYb98Rf03JmuPenp.png", alt="DevTools performance trace for unoptimized page, showing FCP starting after CSS loads.", width="800", height="352" %}

This means that the browser needs to wait for all CSS to load and get processed before painting a single pixel on the screen.

Optimize

To optimize this page, you need to know which classes are considered "critical". You'll use the Coverage Tool for that:

  1. In DevTools, open the Command Menu, by pressing Control+Shift+P or Command+Shift+P (Mac).
  2. Type "Coverage" and select Show Coverage.
  3. Click the Reload button, to reload the page and start capturing the coverage.
{% Img src="image/admin/JTFK7wjhlTzd2cCfkpps.png", alt="Coverage for CSS file, showing 55.9% unused bytes.", width="800", height="82" %}

Double-click on the report, to see classes marked in two colors:

With this information, optimize your CSS so that the browser starts processing critical styles immediately after page loads, while deferring non-critical CSS for later:

<style type="text/css">
.accordion-btn {background-color: #ADD8E6;color: #444;cursor: pointer;padding: 18px;width: 100%;border: none;text-align: left;outline: none;font-size: 15px;transition: 0.4s;}.container {padding: 0 18px;display: none;background-color: white;overflow: hidden;}h1 {word-spacing: 5px;color: blue;font-weight: bold;text-align: center;}
</style>

This is not the standard way of loading CSS. Here's how it works:

{% Aside %}

In this guide, you used vanilla code to implement this optimization. In a real production scenario, it's a good practice to use functions like [loadCSS](https://github.com/filamentgroup/loadCSS/blob/master/README.md), that can encapsulate this behavior and work well across browsers, and can support a [Content Security Policy](/csp/) that may not allow inline `onload` JavaScript.

Alternatively, placing the CSS link at the bottom of the page, allows content above this to be rendered in Firefox and Chrome—though not in Safari—without waiting for the stylesheet. However, it still prioritizes that stylesheet, so it can still block other, more critical, content in all browsers.

{% endAside %}

The resulting page looks exactly like the previous version, even when most styles load asynchronously. Here's how the inlined styles and asynchronous request to the CSS file look like in the HTML file:

{% Glitch { id: 'defer-css-optimized', path: 'index.html', previewSize: 0 } %}

Monitor

Use DevTools to run another Performance trace on the optimized page.

The FCP marker appears before the page requests the CSS, which means the browser doesn't need to wait for the CSS to load before rendering the page:

{% Img src="image/admin/0mVq3q760y37JSn2MmCP.png", alt="DevTools performance trace for unoptimized page, showing FCP starting before CSS loads.", width="800", height="389" %}

As a final step, run Lighthouse on the optimized page.

In the report you'll see that the FCP page has been reduced by 0.2s (a 20% improvement!):

{% Img src="image/admin/oTDQFSlfQwS9SbqE0D0K.png", alt="Lighthouse report, showing an FCP value of '0.8s'.", width="800", height="324" %}

The Eliminate render-blocking resources suggestion is no longer under Opportunities, and now belongs to the Passed Audits section:

{% Img src="image/admin/yDjEvZAcjPouC6I3I7qB.png", alt="Lighthouse report, showing 'Eliminate blocing resources' on the 'Passed Audits' section.", width="800", height="237" %}

Next steps & references

In this guide, you learned how to defer non-critical CSS by manually extracting the unused code in the page. As a complement to this, the extract critical CSS guide covers some of the most popular tools to extract critical CSS and includes a codelab to see how they work in practice.

Efficiently load third-party JavaScript

If a third-party script is slowing down your page load, you have two options to improve performance:

This post explains how to optimize the loading process of third-party scripts with the following techniques:

  1. Using the async or defer attribute on <script> tags

  2. Establishing early connections to required origins

  3. Lazy loading

  4. Optimizing how you serve third-party scripts

Use async or defer

Because synchronous scripts delay DOM construction and rendering, you should always load third-party scripts asynchronously unless the script has to run before the page can be rendered.

The async and defer attributes tell the browser that it may go on parsing the HTML while loading the script in the background, and then execute the script after it loads. This way, script downloads don't block DOM construction and page rendering. The result is that the user can see the page before all scripts have finished loading.

<script async src="script.js">

<script defer src="script.js">

The difference between async and defer is when they start executing the scripts.

async

Scripts with the async attribute execute at the first opportunity after they finish downloading and before the window's load event. This means it's possible (and likely) that async scripts will not be executed in the order in which they appear in the HTML. It also means they can interrupt DOM building if they finish downloading while the parser is still at work.

{% Img src="image/admin/tCqsJ3E7m4lpKOprXu5B.png", alt="Diagram of parser blocking script with async attribute", width="800", height="252" %}

defer

Scripts with the defer attribute execute after HTML parsing is completely finished, but before the DOMContentLoaded event. defer guarantees scripts will be executed in the order they appear in the HTML and will not block the parser.

{% Img src="image/admin/Eq0mcvDALKibHe15HspN.png", alt="Diagram of parser flow with a script with defer attribute", width="800", height="253" %}

Using these attributes can significantly speed up page load. For example, Telegraph recently deferred all of their scripts, including ads and analytics, and improved the ad loading time by an average of four seconds.

{% Aside %} Analytics scripts are usually loaded early so you don't miss any valuable analytics data. Fortunately, there are patterns to initialize analytics lazily while retaining early page-load data. {% endAside %}

Establish early connections to required origins

You can save 100–500 ms by establishing early connections to important third-party origins.

Two <link> types can help here:

preconnect

{% Aside 'caution' %} Only preconnect to critical domains you will use soon because the browser closes any connection that isn't used within 10 seconds. Unnecessary preconnecting can delay other important resources, so limit the number of preconnected domains and test the impact preconnecting makes. {% endAside %}

dns-prefetch

The preconnect hint is best used for only the most critical connections; for less critical third-party domains use <link rel=dns-prefetch>.

Browser support for dns-prefetch is slightly different from preconnect support, so dns-prefetch can serve as a fallback for browsers that don't support preconnect. Use separate link tags to implement this safely:

Lazy-load third-party resources

Embedded third-party resources can be a big contributor to slow page speed when constructed poorly. If they aren't critical or are below the fold (that is, if users have to scroll to view them), lazy loading is a good way to improve page speed and paint metrics. This way, users will get the main page content faster and have a better experience.

{% Img src="image/admin/uzPZzkgzfrv2Oy3UQPrN.png", alt="A diagram of a webpage shown on a mobile device with scrollable content extending beyond the screen. The content that's below-the-fold is desaturated because it's not loaded yet.", width="366", height="438" %}

One effective approach is to lazy-load third-party content after the main page content loads. Ads are a good candidate for this approach.

Ads are an important source of income for many sites, but users come for the content. By lazy loading ads and delivering the main content faster, you can increase the overall viewability percentage of an ad. For example, MediaVine switched to lazy-loading ads and saw a 200% improvement in page load speed. DoubleClick have guidance on how to lazy-load ads in their official documentation.

An alternative approach is to load third-party content only when users scroll down to that section of the page.

Intersection Observer is a browser API that efficiently detects when an element enters or exits the browser's viewport and it can be used to implement this technique. lazysizes is a popular JavaScript library for lazy loading images and iframes. It supports YouTube embeds and widgets. It also has optional support for IntersectionObserver.

{% Aside 'caution' %} Be careful when lazy loading resources with JavaScript. If JavaScript fails to load, perhaps due to flaky network conditions, your resources won't load at all. {% endAside %}

Using the loading attribute for lazy loading images and iframes is a great alternative to JavaScript techniques, and it has recently become available in Chrome 76!

Optimize how you serve third-party scripts

Third-party CDN hosting

It's common for third-party vendors to provide URLs of JavaScript files that they host, usually on a content delivery network (CDN). The benefits of this approach are that you can get started quickly—just copy and paste the URL—and there's no maintenance overhead. The third-party vendor handles server configuration and script updates.

But because they are not on the same origin as the rest of your resources, loading files from a public CDN comes with a network cost. The browser needs to perform a DNS lookup, establish a new HTTP connection, and, on secure origins, perform an SSL handshake with the vendor's server.

When you use files from third-party servers, you rarely have control over caching. Relying on someone else's caching strategy might cause scripts to be unnecessarily re-fetched from the network too often.

Self-host third-party scripts

Self-hosting third-party scripts is an option that gives you more control over a script's loading process. By self-hosting you can:

For example, Casper managed to shave 1.7 seconds off load time by self-hosting an A/B testing script.

Self-hosting comes with one big downside though: scripts can go out of date and won't get automatic updates when there's an API change or a security fix.

{% Aside 'caution' %}

Manually updating scripts can add a lot of overhead to your development process and you might miss out on important updates. If you are not using CDN hosting for serving all the resources, you are also missing out on edge-caching and you have to optimize your server's compression. {% endAside%}

Use service workers to cache scripts from third-party servers

An alternative to self-hosting that allows you greater control over caching while still getting the third-party CDN benefits is using service workers to cache scripts from third-party servers. This gives you control over how often scripts are re-fetched from the network and makes it possible to create a loading strategy that throttles requests for non-essential third-party resources until the page reaches a key user moment. Using preconnect to establish early connections in this case can also mitigate the network costs to an extent.

Extract critical CSS

The browser must download and parse CSS files before it can show the page, which makes CSS a render-blocking resource. If CSS files are big, or network conditions are poor, requests for CSS files can significantly increase the time it takes for a web page to render.

{% Aside 'key-term' %} Critical CSS is a technique that extracts the CSS for above-the-fold content in order to render content to the user as fast as possible. {% endAside %}

{% Img src="image/admin/t3Kkvh265zi6naTBga41.png", alt="An illustration of a laptop and a mobile device with web pages overflowing the edges of screens", width="800", height="469", class="" %}

{% Aside 'note' %} Above-the-fold is all the content a viewer sees on page load, before scrolling. There is no universally defined pixel height of what is considered above the fold content since there is a myriad of devices and screen sizes. {% endAside %}

Inlining extracted styles in the <head> of the HTML document eliminates the need to make an additional request to fetch these styles. The remainder of the CSS can be loaded asynchronously.

{% Img src="image/admin/RVU3OphqtjlkrlAtKLEn.png", alt="HTML file with critical CSS inlined in the head", width="800", height="325" %}
Inlined critical CSS

Improving render times can make a huge difference in perceived performance, especially under poor network conditions. On mobile networks, high latency is an issue regardless of bandwidth.

{% Img src="image/admin/NdQz49RVgdHoh3Fff0yr.png", alt="Filmstrip view comparison of loading a page with render-blocking CSS (top) and the same page with inlined critical CSS (bottom) on a 3G connection. Top filmstrip shows six blank frames before finally displaying content. Bottom filmstrip displays meaningful content in the first frame.", width="800", height="363" %}
Comparison of loading a page with render-blocking CSS (top) and the same page with inlined critical CSS (bottom) on a 3G connection

If you have poor First Contentful Paint (FCP) and see "Eliminate render-blocking resource" opportunity in Lighthouse audits it's a good idea to give critical CSS a go.

{% Img src="image/admin/0xea7menL90lWHwbjZoP.png", alt="Lighthouse audit with 'Eliminate render-blocking resource' or 'Defer unused CSS' opportunities", width="743", height="449" %}

{% Aside 'gotchas' %} Keep in mind that if you inline a large amount of CSS, it delays the transmission of the rest of the HTML document. If everything is prioritized then nothing is. Inlining also has some downsides in that it prevents the browser from caching the CSS for reuse on subsequent page loads, so it's best to use it sparingly. {% endAside %}

To minimize the number of roundtrips to first render, aim to keep above-the-fold content under 14 KB (compressed).

{% Aside 'note' %} New TCP connections cannot immediately use the full available bandwidth between the client and the server, they all go through slow-start to avoid overloading the connection with more data than it can carry. In this process, the server starts the transfer with a small amount of data and if it reaches the client in perfect condition, doubles the amount in the next roundtrip. For most servers, 10 packets or approximately 14 KB is the maximum that can be transferred in the first roundtrip. {% endAside %}

The performance impact you can achieve with this technique depends on the type of your website. Generally speaking, the more CSS a site has, the greater the possible impact of inlined CSS.

Overview of tools

There are a number of great tools that can automatically determine the critical CSS for a page. This is good news because doing this manually would be a tedious process. It requires analysis of the entire DOM to determine the styles that are applied to each element in the viewport.

Critical

Critical extracts, minifies and inlines above-the-fold CSS and is available as npm module. It can be used with Gulp (directly) or with Grunt (as a plugin) and there's a webpack plugin too.

It's a simple tool that takes a lot of thinking out of the process. You don't even have to specify the stylesheets, Critical automatically detects them. It also supports extracting critical CSS for multiple screen resolutions.

criticalCSS

CriticalCSS is another npm module that extracts above-the-fold CSS. It is also available as a CLI.

It doesn't have options to inline and minify critical CSS, but it does let you force-include rules that don't actually belong in critical CSS and gives you more granular control over including @font-face declarations.

Penthouse

Penthouse is a good choice if your site or app has a large number of styles or styles which are being dynamically injected into the DOM (common in Angular apps). It uses Puppeteer under the hood and even features an online hosted version too.

Penthouse doesn't detect stylesheets automatically, you have to specify the HTML and CSS files that you want to generate critical CSS for. The upside is that it's good at running many jobs in parallel.

Optimizing resource loading with the Fetch Priority API

{% BrowserCompat 'api.HTMLImageElement.fetchPriority' %}

{% Aside %} This feature was originally called Priority Hints but was renamed to Fetch Priority after standardization. See History below for more details. {% endAside %}

When a browser parses a web page and begins to discover and download resources such as images, scripts, or CSS, it assigns them a fetch priority in an attempt to download resources in an optimal order. These priorities can depend on the kind of resource and where it is in the document. For example, in-viewport images may have a High priority while the priority for early loaded, render-blocking CSS via <link>s in the <head> could be Very High. Browsers are pretty good at assigning priorities that work well but may not be optimal in all cases.

In this article, we'll discuss the Fetch Priority API and the fetchpriority HTML attribute, which allow you to hint at the relative priority of a resource (high or low). Fetch Priority can help optimize the Core Web Vitals.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/UQ60oFwWrVCPbFYx3pJY.png", alt="A filmstrip view comparing two tests of the Google Flights homepage. At bottom, Fetch Priority are used to boost the priority of the hero image, resulting in a 0.7 second decrease in LCP. ", width="800", height="400" %}
Fetch Priority improving Largest Contentful Paint from 2.6 s to 1.9 s in a test of Google Flights

Summary

A few key areas where Fetch Priority can help:

Historically, developers have had some, but limited, influence over resource priority using preload and preconnect. Fetch Priority complements these Resource Hints, but it's essential to understand where they all fit in. Preload lets you tell the browser about critical resources you want to load early before they are discovered naturally. This is especially useful for resources that are not easily discovered, such as fonts included in stylesheets, background images, or resources loaded from a script. Preconnect helps warm up connections to cross-origin servers and can help improve metrics like Time-to-first-byte and is useful when you know an origin but not necessarily the exact URL of a resource that will be needed.

Fetch Priority is a markup-based signal (available through the fetchpriority attribute) that developers can use to indicate the relative priority of a particular resource. You can also use these hints via JavaScript and the Fetch API with the priority property to influence the priority of resource fetches made for data. Fetch Priority can also complement preload. Take a Largest Contentful Paint image, which, when preloaded, will still get a low priority. If it is pushed back by other early low-priority resources, using Fetch Priority can help how soon the image gets loaded.

Fetch Priority is available in Chrome 101 or later.

Resource priority

The resource download sequence depends on the browser's assigned priority for every resource on the page. Different factors can affect priority computation logic. For example,

The following table considers such factors to show how most resources are currently prioritized and sequenced in Chrome.

  Layout-blocking Load in layout-blocking phase Load one-at-a-time in layout-blocking phase
Blink
Priority
VeryHigh High Medium Low VeryLow
DevTools
Priority
Highest High Medium Low Lowest
Main resource
CSS*** (early**) CSS*** (late**) CSS (mismatch)
Script (early** or not from preload scanner) Script (**late) Script (async)
Font Font (preload)
Import
Image (in viewport) Image
Media
SVG document
Prefetch
Preload*
XSL
XHR (sync) XHR/fetch* (async)

{% Aside %} * Preload using "as" or fetch using "type" use the priority of the type they are requesting (e.g., preload as="stylesheet" will use Highest priority). With no "as", they will behave like an XHR.
** "Early" is defined as being requested before any non-preloaded images have been requested ("late" is after).
*** CSS where the media type doesn't match is not fetched by the preload scanner and is only processed when the main parser reaches it, which usually means it will be fetched very late and with a "late" priority. {% endAside %}

The browser downloads resources with the same computed priority in the order they are discovered. You can check the priority assigned to different resources when loading a page under the Chrome Dev Tools Network tab. (Ensure that you include the priority column by right-clicking on the table headings).

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/Lwm9jwJF5sQ3gQ7PHSs1.png", alt="A screenshot of assets listed in the network tab of Chrome's DevTools. The columns read, from left to right: name, status, type, initiator, size, time, and priority.", width="800", height="263" %}
Priority for resource type = "font" on BBC news detail page
{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/K5U3maOt2TFBbtScW7TY.png", alt="A screenshot of assets listed in the network tab of Chrome's DevTools. The columns read, from left to right: name, status, type, initiator, size, time, and priority.", width="800", height="175" %}
Priority for resource type = "script" on BBC news detail page

When would you need Fetch Priority?

Knowledge of the browser's prioritization logic provides you with a few existing knobs to tweak the order of downloads. You can

  1. Place resource tags such as <script> and <link> depending on the order you want to download them. Resources with the same priority are generally loaded in the order they are discovered.
  2. Use the preload resource hint to download necessary resources earlier, particularly for resources that are not easily discovered early by the browser.
  3. Use async or defer to download scripts without blocking other resources.
  4. Lazy-load below-the-fold content so that the browser can use the available bandwidth for more critical above-the-fold resources.

These techniques help to control the browser's priority computation, thereby improving performance and Core Web Vitals. For example, when a critical background image is preloaded, it can be discovered much earlier, improving the Largest Contentful Paint (LCP).

Sometimes these handles may not be enough to prioritize resources optimally for your application. Here are some of the scenarios where Fetch Priority could be helpful:

  1. You have several above-the-fold images, but all of them need not have the same priority. For example, in an image carousel, only the first visible image needs a higher priority compared to the others.
  2. Hero images inside the viewport start at a "Low" priority. After the layout is complete, Chrome discovers they are in the viewport and boosts their priority (unfortunately, dev tools only shows the final priority—WebPageTest will show both). This usually adds a significant delay to loading the image. Providing the Fetch Priority in markup lets the image start at a "High" priority and start loading much earlier.

    Note that preload is still required for the early discovery of LCP images included as CSS backgrounds and can be combined with Fetch Priority by including the fetchpriority='high' on the preload, otherwise it will still start with the default "Low" priority for images.
  3. Declaring scripts as async or defer tells the browser to load them asynchronously. However, as seen in the figure above, these scripts are also assigned a "low" priority. You may want to bump up their priority while ensuring asynchronous download, particularly for any scripts that are critical for the user experience.
  4. You may use the JavaScript fetch() API to fetch resources or data asynchronously. Fetch is assigned a "High" priority by the browser. There may be situations where you do not want all your fetches to be executed with "High" priority and prefer using different Fetch Priority instead. This can be helpful when running background API calls and mixing them with API calls that are responding to user input, like with autocomplete. The background API calls can be tagged as "Low" priority and the interactive API calls marked as "High" priority.
  5. The browser assigns CSS and fonts a "High" priority, but all such resources may not be equally important or required for LCP. You can use Fetch Priority to lower the priority of some of these resources.

The fetchpriority attribute

You can provide a Fetch Priority using the fetchpriority HTML attribute. You can use the attribute with link, img, and script tags. The attribute allows you to specify the priority for resource types such as CSS, fonts, scripts, and images when downloaded using the supported tags. The fetchpriority attribute accepts one of three values:

Here are a few examples of using the fetchpriority attribute in markup and the script-equivalent priority property.

<!-- We don't want a high priority for this above-the-fold image -->
<img src="/images/in_viewport_but_not_important.svg" fetchpriority="low" alt="I'm an unimportant image!">

<!-- We want to initiate an early fetch for a resource, but also deprioritize it -->
<link rel="preload" href="/js/script.js" as="script" fetchpriority="low">

<script>
  fetch('https://example.com/', {priority: 'low'})
  .then(data => {
    // Trigger a low priority fetch
  });
</script>

Browser priority and fetchpriority

You can apply the fetchpriority attribute to different resources as shown in the following figure to potentially increase or reduce their computed priority. fetchpriority="auto" (◉) in each row denotes the default priority for that type of resource.

  Layout-blocking Load in layout-blocking phase Load one-at-a-time in layout-blocking phase
Blink
Priority
VeryHigh High Medium Low VeryLow
DevTools
Priority
Highest High Medium Low Lowest
Main Resource ◉
CSS*** (early**) ⬆◉ ⬇
CSS*** (late**) ⬆ ◉ ⬇
Script (early** or not from preload scanner) ⬆◉ ⬇
Script*** (late**) ⬆ ◉ ⬇
Script (async/defer) ⬆ ◉⬇
Font ◉
Font (preload) ⬆◉ ⬇
Import ◉
Image (in viewport) ⬆◉ ⬇
Image ⬆ ◉⬇
Media (video/audio) ⬆ ◉⬇
SVG document ⬆ ◉⬇
XHR (sync) - deprecated ◉
XHR/fetch* (async) ⬆◉ ⬇
Preload* ⬆◉ ⬇
Prefetch ◉
Favicon ◉
XSL ◉

{% Aside %} * Preload using "as" or fetch using "type" use the priority of the type they are requesting (e.g., preload as="stylesheet" will use Highest priority). With no "as", they will behave like an XHR.
** "Early" is defined as being requested before any non-preloaded images have been requested ("late" is after).
*** CSS where the media type doesn't match is not fetched by the preload scanner and is only processed when the main parser reaches it, which usually means it will be fetched very late and with a "late" priority.
◉: fetchpriority="auto"
⬆: fetchpriority="high"
⬇: fetchpriority="low"
Images within the viewport start at "Low" priority and then at layout time are boosted to "High". By tagging it in markup using fetchpriority="high", it can start at "High" immediately and load much faster. {% endAside %}

Note that fetchpriority sets the relative priority—that is it raises or lowers the default priority by an appropriate amount, rather than explicitly setting the priority to "High" or "Low" and the browser decides the relative priority. Often this is "High" or "Low", but not always. For example, critical CSS with fetchpriority="high" will still retain the "VeryHigh"/"Highest" priority, and using fetchpriority="low" on these will still retain the "High" priority—in neither case is the Priority explicitly set to "High" or "Low".

Use cases

You can use the fetchpriority attribute to address scenarios where you may wish to provide the browser with an extra hint as to what priority to fetch a resource with.

Increase the priority of the LCP image

You can specify fetchpriority="high" to boost the priority of the LCP or other critical images.

<img src="lcp-image.jpg" fetchpriority="high">

The following comparison shows the Google Flights page with an LCP background image loaded with and without Fetch Priority. With the priority set to high, the LCP improved from 2.6s to 1.9s.

{% Video src="video/1L2RBhCLSnXjCnSlevaDjy3vba73/BCngJfoVOy0YbUz8wFrM.mp4", autoplay=true, muted=true, playsinline=true, loop=true, controls=true %}
An experiment conducted using Cloudflare workers to rewrite the Google Flights page to use Fetch Priority.

Lower the priority of above-the-fold images

You can use fetchpriority="low" to lower the priority of above-the-fold images that may not be important for example in an image carousel.

<ul class="carousel">
  <img src="img/carousel-1.jpg" fetchpriority="high">
  <img src="img/carousel-2.jpg" fetchpriority="low">
  <img src="img/carousel-3.jpg" fetchpriority="low">
  <img src="img/carousel-4.jpg" fetchpriority="low">
</ul>

In an earlier experiment with the Oodle app, we used this to lower the priority of images that do not appear on load. It helped to cut down the load time by 2 seconds.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/Tn4OkGpqPbrSQtd1j3GV.png", alt="A side-by-side comparison of Fetch Priority when used on the Oodle app's image carousel. On the left, the browser sets default priorities for carousel images, but downloads and paints those images around two seconds slower than the example on the right, which sets a higher priority on only the first carousel image.", width="800", height="460" %}

Lower the priority of preloaded resources

To stop preloaded resources from competing with other critical resources, you could provide a hint to reduce their priority. You can use this technique with images, scripts, and CSS.

<!-- Lower priority only for non-critical preloaded scripts -->
<link rel="preload" as="script" href="critical-script.js">
<link rel="preload" href="/js/script.js" as="script" fetchpriority="low">

<!-- Preload CSS without blocking other resources -->
<link rel="preload" as="style" href="theme.css" fetchpriority="low" onload="this.rel='stylesheet'">

Reprioritize scripts

Scripts required to make some parts of the page interactive are essential but should not block other resources. You can mark these as async with high priority.

<script src="async_but_important.js" async fetchpriority="high"></script>

Scripts cannot be marked as async if they rely on specific DOM states. However if they are lower down on the page, they may be downloaded with a lower priority as shown.

<script src="blocking_but_unimportant.js" fetchpriority="low"></script>

Lower the priority for non-critical data fetches

The browser executes fetch with a high priority. If you have multiple fetches that may be fired simultaneously, you can use the high default priority for the more critical data fetches and lower it for less critical data.

// Important validation data (high by default)
let authenticate = await fetch('/user');

// Less important content data (suggested low)
let suggestedContent = await fetch('/content/suggested', {priority: 'low'});

Fetch Priority implementation notes

Fetch Priority can improve performance in specific use cases, as discussed above. There are some things to be aware of:

Using Preload after Chrome 95

The Fetch Priority feature was available for trial in Chrome 73 to 76 but was not released due to prioritization issues with preloads fixed in Chrome 95. Prior to Chrome 95, requests issued via <link rel=preload> always start before other requests seen by the preload scanner, even if the other requests have a higher priority.

With the fix in Chrome 95 and the enhancement for Fetch Priority, we hope that developers will start using preload for its intended purpose—to preload resources not detected by the parser (fonts, imports, background LCP images). The placement of the preload hint will affect when the resource is preloaded. Some key points on using preload are:

History

Fetch Priority was first experimented with in Chrome as an origin trial in 2018 and then again in 2021 using the importance attribute. At that time it was known as Priority Hints. The interface has since changed to fetchpriority for HTML and priority for JavaScript's Fetch API as part of the web standards process. To reduce confusion we now refer to this API as Fetch Priority.

Browser compatibility

{% BrowserCompat 'api.HTMLImageElement.fetchPriority' %}

As of this writing, Fetch Priority is only available in Chromium-based browsers. Other browser engines or earlier versions of Chromium browsers will ignore the attribute and use their default prioritization heuristics. Until another browser implements Fetch Priority, you may notice some references—such as MDN—mark this as Experimental, however Fetch Priority is now standardized and included in the HTML living standard and Fetch living standard.

Conclusion

Developers are likely to be interested in Fetch Priority with the fixes in preload behavior and the recent focus on Core Web Vitals and LCP. They now have additional knobs available to achieve their desired loading sequence.

Fixing website speed cross-functionally

One common myth connected to site speed is that it's only the development team's responsibility. The reality is that a fast site requires help from several departments. Developers can rarely solve the issues alone, no matter how talented they are. So how do you fix speed together with your colleagues? This article explains how you (as a web developer) can convince your company to prioritize site speed, get help from different departments while optimizing, and increase awareness of your optimization project's success.

{% Aside %} This guide has a slight focus on e-commerce (particularly the business stakeholders section), but a lot of the guidance is general enough to be useful to any website that wants to improve mobile conversions. {% endAside %}

Fix speed with the help of your business stakeholders {: #stakeholders }

Many companies are in the middle of a disruptive shift in consumer behavior, where the growth of people buying on digital platforms instead of physical stores tells the tale that companies need to adjust quickly or risk facing decreasing revenue. This will require both new organizational processes and mindsets, and a strong buy-in from the stakeholders in order to succeed.

The shared challenge

The way companies organize themselves will most likely need to change within several levels. First of all, a speed project, with the potential uplift in revenue that it can create, will be difficult to perform without developers getting time to work on it. The resource allocation needs to be in place, and the collaboration with other departments such as marketing and web designers will often go smoother if stakeholders and executives show that it's a priority project.

Apart from this operational level, it's also important for stakeholders to understand that there are two shifts happening right now:

This means that companies that make executive decisions informed by the people who are experts on digital platforms will have an edge. One suggestion could thereby be to invite one developer to the meetings where business decisions are being made, to ensure that the site and its constraints are being considered from the start, and that way, secure increased sales.

Send a proposal to your stakeholders

In general, around 10% of sales happen on digital platforms, but studies show that as much as 85% of the sales in physical stores are influenced digitally first. Having a site that performs well, especially on mobile (which is becoming the primary device), is thereby crucial for our business's revenue. We propose a speed project that will help the site increase conversions, and to set up streams of communication so that the insights in technical teams can be better put to use before decisions that will affect the site are made.

Prepare for meeting with your stakeholders

Before meeting with stakeholders, make sure you're prepared with data for your website and your competitors' sites, and data that demonstrates high ROI for the changes you're suggesting. Gather data around the importance of speed. The best scenario is if it's connected to the company's revenue. Here are some ways to achieve this:

Meet with your stakeholders

Fix speed with the help of your marketing team {: #marketing }

The shared challenge

Marketing teams need tools to handle ads and data in order to make informed decisions, but most tools slow down websites and are often implemented with render-blocking scripts which makes visitors who enter the site have to wait until they see anything on the screen. There is therefore a need for a balance; the marketing department needs data and tools, but for the site to get low bounce rates and high conversion rates it has to be fast, so that marketing gets a good return on their investments.

The balance is achieved by being careful with scripts:

Since it's difficult for the developers to know what the marketing department is dependent on, and since the marketing department might not know which tools are especially heavy for the site, you need to collaborate.

Send a proposal to your marketing team

Tools used for tracking and ads often block how quickly visitors see content when they enter the site. This reduces the value of marketing investments, since we pay for ads, people get interested, they click but then have to wait. This risks them getting annoyed and leaving. One study found that conversion rate decreased from around 50% to around 35% when the speed metric Time to Interactive increased from 0.15 s to 0.3 s. Can we work together on solving this to create a balance between the tools you need and site performance, making increased return on marketing investments a result?

Prepare for meeting with your marketing team

Meet with your marketing team

{% Aside %} One common challenge is that be that people become defensive when approached with suggestions. The key might be to always emphasize respect for the knowledge areas of other departments. By formulating that you want to understand and listen, an openness can be created for listening to your ideas as well. And finally, try inviting other teams to social gatherings or coffee and bakery with different discussion topics. Sometimes the most powerful meetings take place outside the meeting room. {% endAside %}

Agree on next steps

Fix speed with the help of your web design team {: #design }

One of the biggest challenges for web performance is that while connectivity might be getting faster in many regions, websites are becoming heavier and as a result actually getting slower. Developers need to collaborate with web designers in order to solve this.

The shared challenge

In the end, all departments within a company are usually dependent on the site leading to conversions, whether that's for revenue generation or something else. And the harsh truth is that it doesn't matter how beautiful web pages are if they take such a long time to load that people don't stick around to see them.

But slim web pages are a shared responsibility. Developers can use various image optimization techniques to make images load faster. Web designers can create pages that help keep page weight below an agreed level.

Send a proposal to your web design team

Studies show that pages with fewer images and less elements create more conversions. For example, a study by Google and SOASTA found that sessions that converted users had 38% fewer images than sessions that didn't convert. Can we work together to reach speed targets and set up a joint collaboration that can increase conversions on the site? Examples of what we can achieve together include finding the right level of image compression with a balance between quality and speed, or reducing complex layouts or functionalities that often result in heavy, slow sites.

Prepare for a meeting with your web design team

Meet with your web design team

Fix speed with the help of your analytics team {: #analytics }

Businesses need data to know what to focus on and to get a basis for decisions. But even though many talk about the importance of site speed and its connection to revenue and sales, there are still many companies that don't include speed in their weekly reports outside tech teams. This can be changed if developers start collaborating with analytics teams.

The shared challenge

The reality is this: what's not reported will not be focused on. If site speed is not part of weekly or monthly reports to business stakeholders, it will easily be forgotten and you will have a hard time getting resources to focus on it. But since analytics teams often aren't experienced with tracking speed, they will need help from developers. And the developers will need help from the analytics team to calculate the effect on revenue when speed improvements are made. These departments are more dependent on each other than one might at first think.

Send a proposal to your analytics team

Many case studies show the effect site speed has on revenue and bounce rate. Could we possibly collaborate to set up site speed tracking for our company, and perhaps get speed as a part of the reports going to stakeholders so that we get more buy-in?

Prepare for a meeting with your analytics team

Meet with your analytics team

Next steps

Ask your analytics team to perform the analysis outlined in The value of speed after the speed project has led to big improvements. It will show the company how much more conversions and revenue it has gained, rewarding your team as well as all of the departments that collaborated with you. This can be included in a weekly report to stakeholders, so the project doesn't become a one-off effort but is continuously maintained.

Best practices for fonts

This article discusses performance best practices for fonts. There are a variety of ways in which web fonts impact performance:

This article is broken down into three sections: font loading, font delivery, and font rendering. Each section explains how that particular aspect of the font lifecycle works and provides corresponding best practices.

Font loading

Fonts are typically important resources, as without them the user might be unable to view page content. Thus, best practices for font loading generally focus on making sure that fonts get loaded as early as possible. Particular care should be given to fonts loaded from third-party sites as downloading these font files requires separate connection setups.

If you're unsure if your page's fonts are being requested in time, check the Timing tab within the Network panel in Chrome DevTools for more information.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/hFVGVzDQHymbC5aIAtD9.png", alt="Screenshot of the Timing tab in DevTools", width="800", height="472" %}

Understanding @font-face

Before diving into best practices for font loading it's important to understand how @font-face works and how this impacts font loading.

The @font-face declaration is an essential part of working with any web font. At a minimum, it declares the name that will be used to refer to the font and indicates the location of the corresponding font file.

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}

A common misconception is that a font is requested when a @font-face declaration is encountered—this is not true. By itself, @font-face declaration does not trigger font download. Rather, a font is downloaded only if it is referenced by styling that is used on the page. For example, like this:

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}

h1 {
  font-family: "Open Sans"
}

In other words, in the example above, Open Sans would only be downloaded if the page contained a <h1> element.

{% Aside %} Other ways of loading a font are the preload resource hint and the Font Loading API. {% endAside %}

Thus, when thinking about font optimization, it's important to give stylesheets just as much consideration as the font files themselves. Changing the contents or delivery of stylesheets can have a significant impact on when fonts arrive. Similarly, removing unused CSS and splitting stylesheets can reduce the number of fonts loaded by a page.

Inline font declarations

Most sites would strongly benefit from inlining font declarations and other critical styling in the <head> of the main document rather than including them in an external stylesheet. This allows the browser to discover the font declarations sooner as the browser doesn't need to wait for the external stylesheet to download.

<head>
  <style>
    @font-face {
        font-family: "Open Sans";
        src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
    }

    body {
        font-family: "Open Sans";
    }

    ...etc.

  </style>
</head>

{% Aside %} Note that if only some of the CSS is inlined, then the browser will still need to wait for all the CSS to be loaded, before it can discover if fonts are needed.

Also note that inlining the font files themselves is not recommended. Inlining large resources like fonts is likely to delay the delivery of the main document, and with it, the discovery of other resources. {% endAside %}

Inlining critical CSS can be a more advanced technique that not all sites will be able to achieve. The performance benefits are clear, but it requires additional processes and build tools to ensure the necessarily CSS—and ideally only the critical CSS—is inlined correctly and that any additional CSS is delivered in a non-render blocking fashion.

Preconnect to critical third-party origins

If your site loads fonts from a third-party site, it is highly recommended that you use the preconnect resource hint to establish early connection(s) with the third-party origin. Resource hints should be placed in the <head> of the document. The resource hint below sets up a connection for loading the font stylesheet.

To preconnect the connection that is used to download the font file, add a separate preconnect resource hint that uses the crossorigin attribute. Unlike stylesheets, font files must be sent over a CORS connection.

When using the preconnect resource hint, keep in mind that a font provider may serve stylesheets and fonts from separate origins. For example, this is how the preconnect resource hint would be used for Google Fonts.

{% Aside %} Google Fonts provides the option to load fonts via <link> tags or an @import statement. The <link> code snippet includes a preconnect resource hint and therefore will likely result in faster stylesheet delivery than using @import version. These <link> tags should be placed as early in the document as possible. {% endAside %}

Be cautious when using preload to load fonts

Although preload is highly effective at making fonts discoverable early in the page load process, this comes at the cost of taking away browser resources from the loading of other resources.

Inlining font declarations and adjusting stylesheets may be a more effective approach. These adjustments come closer to addressing the root cause of late-discovered fonts—rather than just providing a workaround.

In addition, using preload as a font-loading strategy should also be used carefully as it bypasses some of the browser's built-in content negotiation strategies. For example, preload ignores unicode-range declarations, and if used prudently, should only be used to load a single font format.

However, when using external stylesheets, preloading the most important fonts can be very effective since the browser will not otherwise discover whether the font is needed until much later.

Font delivery

Faster font delivery yields faster text rendering. In addition, if a font is delivered early enough, this can help eliminate layout shifts resulting from font swapping.

Using self-hosted fonts

On paper, using a self-hosted font should deliver better performance as it eliminates a third-party connection setup. However, in practice, the performance differences between these two options is less clear cut: for example, the Web Almanac found that sites using third-party fonts had a faster render than fonts that used first-party fonts.

If you are considering using self-hosted fonts, confirm that your site is using a Content Delivery Network (CDN) and HTTP/2. Without use of these technologies, it is much less likely that self-hosted fonts will deliver better performance. For more information, see Content Delivery Networks.

{% Aside %} If you're unsure if using self-hosted fonts will deliver better performance, try serving a font file from your own servers and compare the transfer time (including connection setup) with that of a third-party font. If you have slow servers, don't use a CDN, or don't use HTTP/2 it becomes less likely that the self-hosted font will be more performant. {% endAside %}

If you use a self-hosted font, it is recommended that you also apply some of the font file optimizations that third-party font providers typically provide automatically—for example, font subsetting and WOFF2 compression. The amount of effort required to apply these optimizations will depend somewhat on the languages that your site supports. In particular, be aware that optimizing fonts for CJK languages can be particularly challenging.

Use WOFF2

Of the modern font fonts, WOFF2 is the newest, has the widest browser support, and offers the best compression. Because it uses Brotli, WOFF2 compresses 30% better than WOFF, leading to less data to download and therefore faster performance.

Given the browser support, experts now recommend only using WOFF2:

In fact, we think it is also time to proclaim: Use only WOFF2 and forget about everything else.

This will simplify your CSS and workflow massively and also prevents any accidental double or incorrect font downloads. WOFF2 is now supported everywhere. So, unless you need to support really ancient browsers, just use WOFF2. If you can't, consider not serving any web fonts to those older browsers at all. This will not be a problem if you have a robust fallback strategy in place. Visitors on older browsers will simply see your fallback fonts.

Bram Stein, from the 2022 Web Almanac

Subset fonts

Font files typically include a large number of glyphs for all the various characters they support. But you may not need all the characters on your page and can reduce the size of font files by subsetting fonts.

The unicode-range descriptor in the @font-face declartion informs the browser which characters a font can be used for.

@font-face {
    font-family: "Open Sans";
    src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
    unicode-range: U+0025-00FF;
}

A font file will be downloaded if the page contains one or more characters matching the unicode range. unicode-range is commonly used to serve different font files depending on the language used by page content.

unicode-range is often used in conjunction with the technique of subsetting. A subset font includes a smaller portion of the that were contained in the original font file. For example, rather than serve all characters to all users, a site might generate separate subset fonts for Latin and Cyrillic characters. The number of glyphs per font varies wildly: Latin fonts are usually on the magnitude of 100 to 1000 glyphs per font; CJK fonts may have over 10,000 characters. Removing unused glyphs can significantly reduce the filesize of a font.

Some font providers may provide different versions of fonts files with different subsets automatically. For example, Google Fonts does this by default:

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;
}
/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

When moving to self-hosting, this is an optimization that can easily be missed and lead to larger font files locally.

It is also possible to manually subset fonts if your font provider allows this, either through an API (Google Fonts supports this by providing a text parameter), or by manually editing the font files and then self-hosting. Tools for generating font subsets include subfont and glyphanger. However, do check the licence for the fonts you use allow subsetting and self-hosting.

Use fewer web fonts

The fastest font to deliver is a font that isn't requested in the first place. System fonts and variable fonts are two ways to potentially reduce the number of web fonts used on your site.

A system font is the default font used by the user interface of a user's device. System fonts typically vary by operating system and version. Because the font is already installed, the font does not need to be downloaded. System fonts can work particularly well for body text.

To use the system font in your CSS, list system-ui as the font-family:

font-family: system-ui

The idea behind variable fonts is that a single variable font can be used as a replacement for multiple font files. Variable fonts work by defining a "default" font style and providing "axes" for manipulating the font. For example, a variable font with a Weight axis could be used to implement lettering that would previously require separate fonts for light, regular, bold, and extra bold.

{% Aside %} We often refer to "Times New Roman" and "Helvetica" as fonts. However, technically speaking, these are font families. A family is composed of styles, which are particular variations of the typeface (for example, light, medium, or bold italic). A font file contains a single style unless it is a variable font. A typeface is the underlying design, which can be expressed as digital fonts—and in physical type, like carved woodblocks or metal. {% endAside %}

Not everyone will benefit from switching to variable fonts. Variable fonts contain many styles, so typically have larger file sizes than individual non-variable fonts that only contain one style. Sites that will see the largest improvement from using variable fonts are those that use (and need to use) a variety of font styles and weights.

Font rendering

When faced with a web font that has not yet loaded, the browser is faced with a dilemma: should it hold off on rendering text until the web font has arrived? Or should it render the text in a fallback font until the web font arrives?

Different browsers handle this scenario differently. By default, Chromium-based and Firefox browsers will block text rendering for up to 3 seconds if the associated web font has not loaded; Safari will block text rendering indefinitely.

This behavior can be configured by using the font-display attribute. This choice can have significant implications: font-display has the potential to impact LCP, FCP, and layout stability.

Choose an appropriate font-display strategy

font-display informs the browser how it should proceed with text rendering when the associated web font has not loaded. It is defined per font-face.

@font-face {
  font-family: Roboto, Sans-Serif
  src: url(/fonts/roboto.woff) format('woff'),
  font-display: swap;
}

There are five possible values for font-display:

Value Block period Swap period
Auto Varies by browser Varies by browser
Block 2-3 seconds Infinite
Swap 0ms Infinite
Fallback 100ms 3 seconds
Optional 100ms None

font-display strategies reflect different viewpoints about the tradeoff between performance and aesthetics. As such it's difficult to give a recommended approach as it does depend on individual preferences, how important the web font is to the page and brand, and how jarring a late-arriving font can be when swapped in.

For most sites, these are the three strategies that will be most applicable:

{% Aside %} font-display: auto, font-display: block, font-display: swap, and font-display: fallback all have the potential to cause layout shifts when the font is swapped. However, of these approaches, font-display: swap will delay text render the least. Thus, it can be the preferred approach for situations where it is important that text is displayed as quickly as possible, but ultimately gets rendered as a web font. {% endAside %}

Also keep in mind that these two approaches can be combined: for example, use font-display: swap for branding and other visually distinctive page elements; use font-display: optional for fonts used in body text.

{% Aside %} The font-display strategies that work well for traditional web fonts don't work nearly as well for icon fonts. The fallback font for an icon font typically looks significantly different than the icon font and its characters may convey a completely different meaning. As a result, icon fonts are more likely to cause significant layout shifts. In addition, using a fallback font may not be practical. If possible, it's best to replace icon fonts with SVG (this is also better for accessibility). Newer versions of popular icon fonts typically support SVG. For more information on switching to SVG, see Font Awesome and Material Icons. {% endAside %}

Reduce the shift between your fallback font and your webfont

To reduce the CLS impact, you can use the new size-adjust attributes. For more information see this article. This is a very new addition to our toolset, so is more advanced and a bit manual at present. But definitely one to experiment with and watch for tooling improvements in the future!

Conclusion

Web fonts still be a performance bottleneck but we have an ever-growing range of options to allow us to optimize them to reduce this bottleneck as much as possible.

How can performance improve conversion?

In our other e-commerce guides you have learned about what you should measure to improve performance, and how to measure and report metrics to build a performance culture.

{% Img src="image/admin/tRFlwIEQJcf52vGBEoDp.png", alt="A conversion funnel going from discover to engage to convert to re-engage.", width="800", height="399" %}
A conversion funnel.

In this guide we'll address the different ways in which a website should be optimized for performance to yield maximum conversions at the end of the funnel.

Discovery

New users discover a website in most cases through organic search, social sharing, website links or paid campaigns. Some important discovery mechanisms are directly affected by website performance. Website crawlers may have difficulty indexing sites that are slow to load or have extensive client side rendering and JavaScript.

Speed can also be a direct ranking factor, for example on web search, ad campaigns or social networks. Keep in mind that new users who discover your website will get an uncached first load, so basically the worst possible experience. This can be especially frustrating if good money was spent to get the user to the website, just to see them dropping off due to a long first load.

Make sure to use appropriate tools as described in Fast Load Times to optimize towards a first load, because first impressions matter—if the first load is too slow, the user may never see the optimized second load or stay around to look at your products. In general loading times of a website map very well to bounce rates, which in turn often correlate well with conversions.

Engagement

After getting users to your site, you need to keep them engaged with your content, which you can verify in your analytics of choice by looking at session length, time-on-page, pages-per-session and general user flows.

{% Img src="image/admin/Apz0cUkUGVR7m9gKepkt.png", alt="A Google Analytics dashboard shows the number of users that drop off from starting page to first and second interactions.", width="800", height="416" %}
A user flow through the funnel as seen by Google Analytics.

Besides various UX best practices, a smooth, fast and responsive experience is key here. While optimizing a website for discovery means optimizing for first load, optimizing for engagement means fast navigations and fast repeat loads. Analyze at which steps of the flow users drop out, and then relate back to speed metrics for these navigations. This can be analyzed for example via WebPageTest, Puppeteer or via the Chrome DevTools Record feature. We will show you more examples of those in the following guides.

Conversion

While website conversions are often bound to good discovery paired with great engagement, there are a couple of additional points to remember. Users expect hero images to load fast, call-to-action buttons should be rendered and labeled quickly, page should be responsive and layout jumps should be avoided. A user won't buy anything if they can't click the Buy Now button due to busy CPU or a jumping or unlabeled button. In general it's best to measure and track the time-to-action towards a conversion or a subgoal for it, for example the median time it takes shoppers to get from landing on your site, to viewing a product, to completing payment .

Re-Engagement

It turns out that only 2% of users convert on first visit, so it is important to get the other 98% to come back, and re-engage them with your content. Modern websites have different ways to do this, for example via mail, tailored display ads in remarketing or notifications. This works best if the flow from reengagement to website is as smooth as possible. Unfortunately this is not always the case, as for example mail apps often open links in their in-app webview, slowing down page load and complicating logins through different cache and cookie storage. Make sure to optimize for fast repeat loads and smooth UX flows to increase chances of reengagement.

Recap

E-commerce sites always strive for conversions, which are at the end of a purchase funnel. Every step along the funnel needs to be optimized for website speed to minimize bounce rates and drop-offs, and for each step there are different things to optimize, different pitfalls and culprits:

{% Img src="image/admin/tRFlwIEQJcf52vGBEoDp.png", alt="A conversion funnel going from discover to engage to convert to re-engage.", width="800", height="399" %}
An e-commerce funnel showing which metric to optimize in which step.

To learn more, be sure to check out the other posts in this series on measuring to improve performance, and how report metrics to build a performance culture.

Hero image by Campaign Creators on Unsplash.

How to measure speed?

Real-world performance is highly variable due to differences in users' devices, network connections, and other factors. For example, if you load your website using a cable network connection in your office and compare it against the load using WiFi in a coffee shop, the experiences are likely to be very different. There are many tools on the market that can help you collect lab or field data to assess page performance.

Lab data vs Field data

{% Img src="image/admin/6OMEfvIKRuDWWSiVDto4.png", alt="Speed tools graphics", width="800", height="232" %}

Lab data is performance data collected within a controlled environment with predefined device and network settings, while Field data is performance data collected from real page loads experienced by your users in the wild. Each type has its own strengths and limitations.

Lab data offers reproducible results and a debugging environment, but might not capture real-world bottlenecks and cannot correlate against real-world page KPIs. With lab data, you need to understand your users' typical devices and networks and appropriately mirror those conditions when you test performance. Have in mind that even in areas with 4G, users may still experience slower or intermittent connections when in elevators, while commuting, or in comparable environments.

Field data (also called Real User Monitoring or RUM) captures true real-world user experience and enables correlation to business KPIs, but has a restricted set of metrics and limited debugging capabilities.

Tools

Lab data

Lighthouse takes a URL and runs a series of audits against the page, generating a report on how well the page did. There are multiple ways to run Lighthouse, including an option to easily audit a page from within Chrome DevTools.

Field data

Chrome User Experience Report (CrUX) provides metrics showing how real-world Chrome users experience popular destinations on the web.

Other tools

PageSpeed Insights provides both lab and field data about a page. It uses Lighthouse to collect and analyze lab data about the page, while real-world field data is based on the Chrome User Experience Report dataset.

Chrome Developer Tools is a set of web developer tools built directly into the Google Chrome browser. It allows you to profile the runtime of a page, as well as identify and debug performance bottlenecks.

How to report metrics and build a performance culture

Website performance is a key goal towards e-commerce conversions, so it must be a core priority for business stakeholders and leadership, not just development teams.

For that reason, it's crucial to make performance metrics visible and tangible for all stakeholders, and to build reporting and monitoring into your workflow.

This guide describes what to be aware of and what to avoid to achieve this successfully.

A word about metrics

There are countless metrics to evaluate website performance, and while it may seem useful to collect all of them, too many metrics can be confusing and misleading. There are several ways to deal with this:

In reality, a pragmatic combination makes the most sense here. Gather more rather than less to begin with, report an abstract score to management, and optimize towards the one metric which best predicts your conversions.

Finding this metric can be done by using your analytics tool of choice to map performance metrics to user engagement, conversions and transaction values. For example, a custom report to do so looks like this in Google Analytics:

{% Img src="image/admin/YI31t9fE82uEhrrYKTVg.png", alt="A Google Analytics dashboard shows a number of fields being added to a custom report.", width="800", height="430" %}
Fig 1: Custom report in Google Analytics to analyze the impact of speed on conversions and engagement.

{% Aside 'caution' %} Metrics can be deceiving. Unfortunately metrics can sometimes be misleading with respect to performance. Keep reading for specifics. {% endAside %}

Increasing bounce rate

Problem

Often it is assumed that bounce rate will drop when page load speed increases. While this normally does hold true, measurements sometimes show the opposite. This is because analytics can only measure a bounce after the analytics library is loaded. A faster page load means analytics code also loads faster, so analytics may see more bounces even if there aren't more happening.

Solution

This can be eased by measuring real page abandonment instead.

Decreasing relative conversions

Problem

Relative conversions may sometimes seem to drop for faster sites. This is because faster pages reach a bigger audience who might be less engaged or committed. While incremental traffic and conversions increase with faster pages, relative conversions (the ratio of conversions to page views or visitors) might still drop.

Solution

This effect can be eased by looking out for absolute conversions instead, and even calculate Cost Per Sales (conversions divided with investment level) or ROI.

Dropping engagement

Problem

Page engagement may seem to drop for a faster page.

Solution

This can actually be a positive signal! If the page is faster users can reach their goal more quickly and might just have shorter sessions and less time on page.

Using averages or medians

Problem

In general, it can be deceiving to look at averages and medians of performance metrics (see this nicely described in Everything You Know About Latency Is Wrong). Similar to a chain being just as strong as the weakest link, the performance of a funnel is only as good as its slowest load. A single slow load may be enough to lose the user. Therefore averages and medians are more likely to hide the real performance issues, than to reveal them.

Solution

It's best to analyze complete distributions, but as this is not always easily possible we recommend using 90th percentiles—this is the value for which 90% of loads were faster. Even then a user hitting ten pages will still have one load slower than this, and might drop out there.

While performance measurement is highly important, make sure to keep an open mind and question unexpected results—and make sure to not report misleading numbers to stakeholders and management. If unsure on what to pick and report, we'd advise as a minimum for 90th percentile First Contentful Paint, which is also what we use across our public tooling.

Third party content

Website performance is especially prone to being dragged down by third party content (see Eliminate render-blocking resources). This is a particular problem for e-commerce, often due to trackers and widgets.

Some ways to handle third party content with respect to performance:

{% Img src="image/admin/w2xb4mUJ7kDpt254fq0k.png", alt="A Ghostery report showing all loaded trackers.", width="800", height="710" %}
A Ghostery report showing all loaded trackers

Learn more in Loading Third-Party JavaScript.

Performance Culture

Unfortunately performance is often seen as a one-off optimization task, and then regresses over time as stakeholders raise new feature requests or insist on adding new trackers and widgets.

Performance must be a continuous goal to improve acquisition, discovery, and conversion rates as well as safeguarding the reputation of your brand. This can be achieved with performance budgets like Tinder did, and by establishing and fostering a performance culture, where all employees and especially decision makers recognize speed as a core feature of the website.

Make metrics tangible

Reports on metrics are often abstract and easy to challenge or dismiss. It's best to make your performance tangible and visible. There are several good ways to do this:

Recap

This guide explains why the selection of metrics, and how they are reported and handled, is as important as the measurement and optimization itself—or even more. Make sure to prefer percentiles or distributions over averages, be cautious of using just bounce rate or relative conversion rate as impact measures, and make sure that metrics are easy to understand and tangible for stakeholders across the company. Establishing a performance culture is also an important step towards a well performing e-commerce site.

How to stay fast?

Brands that optimize speed will often find they regress quickly. This is because website performance is a lot like getting fit: it's not enough to make a one time effort; you have to change your lifestyle.

{% Aside 'note' %} Internal Google study has found that 40% of brands regress on web performance after 6 months. {% endAside %}

Performance budgets are one way to address this. A performance budget is a set of limits on metrics that affect site performance. The concept is similar to a financial budget: you set a limit and make sure you stay within it. In general, a good performance budget combines different types of metrics; so, for example, the performance budget for a product page might look as follows:

Example performance budget.
Time To Interactive on slow 3G Less than 5 seconds
First Contentful Paint on slow 3G Less than 2 seconds
Lighthouse performance score Greater than 80
Total JavaScript size Less than 170 kb

Once set, a performance budget has to be enforced, which means for example incorporating the budget into your build process and reporting. Tools like Lighthouse can be included in your continuous integration, and you can write tests that fail a build if key metrics drop below a set threshold. Additionally, regular reporting through dashboards or summary reports can help with visibility and accountability. Pinterest are one example of a business that have implemented performance budgets to make sure their fast experience stays fast, while brands like Experian are now using site speed as a key metric in their monthly executive KPI reports.

You can find more details on performance budgets here. A guide that describes how to further instill performance culture and make speed metrics visible and tangible for all stakeholders can be found here.

Identify slow third-party JavaScript

As a developer, you often don't have control over which third-party scripts your site loads. Before you can optimize third-party content you have to do some detective work to find out what's making your site slow. 🕵️

In this post, you'll learn how to use Lighthouse and Chrome DevTools to identify slow third-party resources. The post walks through increasingly robust techniques which are best used in combination.

If you only have 5 minutes

The Lighthouse Performance audit helps you discover opportunities to speed up page loads. Slow third-party scripts are likely to appear in the Diagnostics section under the Reduce JavaScript execution time and Avoid enormous network payloads audits.

To run an audit:

{% Instruction 'devtools-lighthouse', 'ol' %}

  1. Click Mobile.
  2. Select the Performance checkbox. (You can clear the rest of the checkboxes in the Audits section.)
  3. Click Simulated Fast 3G, 4x CPU Slowdown.
  4. Select the Clear Storage checkbox.
  5. Click Run audits.

{% Img src="image/admin/XLNFxdEOc7739bcIwERq.png", alt="Screenshot of the Chrome DevTools Audits panel.", width="800", height="1068" %}

Third-party usage

The Lighthouse Third-party usage audit shows a list of the third-party providers a page uses. This overview can help you better understand the big picture and identify redundant third-party code. The audit is available in the Lighthouse extension and will soon be added to DevTools in Chrome 77.

{% Img src="image/admin/4JXHK0FkgJIfKED16BnF.png", alt="Screenshot showing that 51 third-parties were found and a list of imaginary startups.", width="728", height="646" %}
Third-party provider names generated with Startup generator. Any similarity to actual startups, living or dead, is purely coincidental.

Reduce JavaScript execution time

The Lighthouse Reduce JavaScript execution time audit highlights scripts that take a long time to parse, compile, or evaluate. Select the Show 3rd-party resources checkbox to discover CPU-intensive third-party scripts.

{% Img src="image/admin/O7vN1En6dtbL3Q8TbufC.png", alt="Screenshot showing that the 'Show third-party resources' checkbox is checked.", width="800", height="981" %}

Avoid enormous network payloads

The Lighthouse Avoid enormous network payloads audit identifies network requests—including those from third-parties—that may slow down page load time. The audit fails when your network payload exceeds 4,000 KB.

{% Img src="image/admin/9Pnoz73MLeNzooUQLuam.png", alt="Screenshot of the Chrome DevTools 'Avoid enormous network payloads' audit.", width="799", height="631" %}

Block network requests in Chrome DevTools

Chrome DevTools network request blocking allows you to see how your page behaves when a particular script, stylesheet, or other resource isn't available. After you identify third-party scripts that you suspect affect performance, measure how your load time changes by blocking the requests to those scripts.

To enable request blocking: {% Instruction 'devtools-network', 'ol' %}

  1. Right-click any request in the Network panel.
  2. Select Block request URL.

{% Img src="image/admin/UbedvjrtP9si1l0X2QVA.png", alt="A screenshot of the context menu in the Chrome DevTools Performance panel. The 'Block request URL' option is highlighted.", width="800", height="529" %}

A Request blocking tab will appear in the DevTools drawer. You can manage which requests have been blocked there.

To measure the impact of third-party scripts:

  1. Measure how long your page takes to load using the Network panel. To emulate real-world conditions, turn on network throttling and CPU throttling. (On faster connections and desktop hardware, the impact of expensive scripts may not be as representative as it would be on a mobile phone.)
  2. Block the URLs or domains responsible for third-party scripts you believe are an issue.
  3. Reload the page and re-measure how long it takes to load without the blocked third-party scripts.

You should hopefully see a speed improvement, but occasionally blocking third-party scripts might not have the effect you expect. If that's the case, reduce the list of blocked URLs until you isolate the one that's causing slowness.

Note that doing three or more runs of measurement and looking at the median values will likely produce more stable results. As third-party content can occasionally pull in different resources per page load, this approach can give you a more realistic estimate. DevTools now supports multiple recordings in the Performance panel, making this a little easier.

Use image CDNs to optimize images

Image content delivery networks (CDNs) are excellent at optimizing images. Switching to an image CDN can yield a 40–80% savings in image file size. In theory, it's possible to achieve the same results using only build scripts, but it's rare in practice.

What's an image CDN?

Image CDNs specialize in the transformation, optimization, and delivery of images. You can also think of them as APIs for accessing and manipulating the images used on your site. For images loaded from an image CDN, an image URL indicates not only which image to load, but also parameters like size, format, and quality. This makes it easy to create variations of an image for different use cases.

{% Img src="image/admin/OIF2VcXp8P6O7tQvw53B.jpg", alt="Shows the request/response flow between the image CDN and the client. Parameters like size and format are used to request variations of the same image.", width="800", height="408" %}
Examples of transformations image CDNs can perform based on parameters in image URLs.

Image CDNs are different from build-time image optimization scripts in that they create new versions of images as they're needed. As a result, CDNs are generally better suited to creating images that are heavily customized for each individual client than build scripts are.

How image CDNs use URLs to indicate optimization options

Image URLs used by image CDNs convey important information about an image and the transformations and optimizations that should be applied to it. URL formats will vary depending on image CDN, but at a high-level, they all have similar features. Let's walk through some of the most common features.

{% Img src="image/admin/GA4udXeYUEjHSY4N0Qew.jpg", alt="Image URLs typically consist of the following components: origin, image, security key, and transformations.", width="800", height="127" %}

Origin

An image CDN can live on your own domain or the domain of your image CDN. Third-party image CDNs typically offer the option of using a custom domain for a fee. Using your own domain makes it easier to switch image CDNs at a later date because no URL changes will be needed.

The example above uses the image CDN's domain ("example-cdn.com") with a personalized subdomain, rather than a custom domain.

Image

Image CDNs can usually be configured to automatically retrieve images from their existing locations when they're needed. This capability is often achieved by including the complete URL of the existing image within the URL for the image generated by the image CDN. For example, you might see a URL that looks like this: https://my-site.example-cdn.com/https://flowers.com/daisy.jpg/quality=auto. This URL would retrieve and optimize the image that exists at https://flowers.com/daisy.jpg.

{% Aside %} As the above example shows, the file exension requested (.jpg is this case) may not be the same as the image file format returned (WebP in this example). The content-type HTTP Header will inform the browser which format the URL is in so it can process it appropriately. However, this may cause some confusion if the file is saved to disk and used by another program that expects the file format to match the file extension. {% endAside %}

Another widely supported way of uploading images to an image CDN is to send them via an HTTP POST request to the image CDN's API.

Security key

A security key prevents other people from creating new versions of your images. If this feature is enabled, each new version of an image requires a unique security key. If someone tries to change the parameters of the image URL but doesn't provide a valid security key, they won't be able to create a new version. Your image CDN will take care of the details of generating and tracking security keys for you.

Transformations

Image CDNs offer tens, and in some cases hundreds, of different image transformations. These transformations are specified via the URL string, and there are no restrictions on using multiple transformations at the same time. In the context of web performance, the most important image transformations are size, pixel density, format, and compression. These transformations are the reason why switching to an image CDN typically results in a significant reduction in image size.

There tends to be an objectively best setting for performance transformations, so some image CDNs support an "auto" mode for these transformations. For example, instead of specifying that images be transformed to the WebP format, you could allow the CDN to automatically select and serve the optimal format. Signals that an image CDN can use to determine the best way to transform an image include:

For example, the image CDN might serve AVIF to a Chrome browser, WebP to an Edge browser, and JPEG to a very old browser. Auto settings are popular because they allow you to take advantage of image CDNs' significant expertise in optimizing images without the need for code changes to adopt new technologies once they're supported by the image CDN.

Types of Image CDNs

Image CDNs can be broken down into two categories: self-managed and third-party-managed.

Self-managed image CDNs

Self-managed CDNs can be a good choice for sites with engineering staff who are comfortable maintaining their own infrastructure.

Third-party image CDNs

Third-party image CDNs provide image CDNs as a service. Just as cloud providers provide servers and other infrastructure for a fee; image CDNs provide image optimization and delivery for a fee. Because third-party image CDNs maintain the underlying technology, getting started is fairly simple and can usually be accomplished in 10-15 minutes, although a complete migration for a large site might take far longer. Third-party image CDNs are typically priced based on usage tiers, with most image CDNs providing either a free tier or a free trial to give you an opportunity to try out their product.

Choosing an image CDN

There are many good options for image CDNs. Some will have more features than others, but all of them will probably help you save bytes on your images and therefore load your pages faster. Besides feature sets, other factors to consider when choosing an image CDN are cost, support, documentation, and ease of setup or migration.

Trying them out yourself before making a decision can also be helpful. Below you can find codelabs with instructions on how to quickly get started with several image CDNs.

Effects on Largest Contentful Paint (LCP)

Images are a vital part of the user experience on many websites, and thus factor into how well sites do when it comes to Largest Contentful Paint. Here are a few things to keep in mind if you decide to use an image CDN:

  1. Images served from CDNs may come from a cross-origin server, which involves extra connection setup time. When possible, try to use an image CDN that proxies through the primary origin so that you're not adding extra origins for the browser to connect to. This has the same effect as self-hosting images on the primary origin.
  2. Consider using a fetchpriority attribute value of "high" on the LCP image element so that the browser can begin loading that image as soon as possible.
  3. If an image is not immediately discoverable in the initial HTML, consider using a rel=preload hint for your LCP candidate image so that the browser can load that image ahead of time.
  4. If proxying through your origin is not possible, and the exact image to be loaded will not be known until later during page load, you should set up a connection to the cross-origin image CDN as early as possible to shorten the resource loading phase of what could be your page's LCP candidate image.

Image CDNs are indispensable tools that eliminate the toil of manually optimizing images, and should be considered. As always, though, there are trade-offs to consider, and keeping an eye on your website's LCP candidate images and adding hints as appropriate can mitigate any added latency to those key requests.

Incorporate performance budgets into your build process

Once you've defined a performance budget, it's time to set up the build process to keep track of it. There are a number of tools that let you define thresholds for chosen performance metrics and warn you if you go over budget. Find out how to choose one that best fits your needs and current setup. 🕵️‍♀️

Lighthouse performance budgets

Lighthouse is an auditing tool that tests sites in a few key areas—performance, accessibility, best practices and how well your site performs as a progressive web application.

The command line version of Lighthouse (v5+) supports setting performance budgets based on:

You can set budgets for any of the following resource types:

Budgets are set in a JSON file and after defining them the new "Over Budget" column tells you whether you're exceeding any limits.

{% Img src="image/admin/fsRNXJXQ5JESUuwsIlLn.png", alt="Budgets section in Lighthouse report", width="800", height="199" %}
"Budgets" section in Lighthouse report

Webpack performance hints

Webpack is a powerful build tool for optimizing how your code is delivered to the users. It also supports setting performance budgets based on asset size.

Turn on performance hints in webpack.config.js to get command line warnings or errors when your bundle size grows over the limit. It's a great way to stay mindful about asset sizes throughout the development.

After the build step, webpack outputs a color-coded list of assets and their sizes. Anything over budget is highlighted in yellow.

{% Img src="image/admin/G96Z2pZVL6aowVRa49rA.png", alt="Webpack output highlighting bundle.js", width="617", height="86" %}
The highlighted bundle.js is bigger than your budget

The default limit for both assets and entry-points is 250 KB. You can set your own targets in the configuration file.

{% Img src="image/admin/StfxwSScNVzmeM70gwp4.jpg", alt="Webpack bundle size warning", width="566", height="104" %}
Webpack bundle size warning ⚠️

The budgets are compared against uncompressed asset sizes. Uncompressed JavaScript size is related to the execution time and big files can take a long time to execute, especially on mobile devices.

{% Aside %} Compressed asset sizes affect the transfer time, which is very important on slow networks. {% endAside %}

{% Img src="image/admin/ogH1cMiF96iyhj8CiENK.jpg", alt="Webpack performance optimization recommendation", width="556", height="84" %}
Bonus feature: webpack won’t only warn you, it will give you a recommendation on how to downsize your bundles. 💁

Bundlesize

Bundlesize is a simple npm package that tests asset size against a threshold you've set. It can run locally and integrate with your CI.

Bundlesize CLI

Run bundlesize CLI by specifying a threshold and the file that you want to test.

bundlesize -f "dist/bundle.js" -s 170kB

Bundlesize outputs color-coded test results in one line.

{% Img src="image/admin/6YsTnhpUlzbTB1ja23yP.png", alt="Failing bundlesize CLI test", width="473", height="36" %}
Failing bundlesize CLI test ❌
{% Img src="image/admin/TjynyaIzZlRa6xE2uVcD.png", alt="Passing bundlesize CLI test", width="460", height="36" %}
Passing bundlesize CLI test ✔️

Bundlesize for CI

You'll get the most value out of bundlesize if you integrate it with a CI to automatically enforce size limits on pull requests. If bundlesize test fails, that pull request is not merged. It works for pull requests on GitHub with Travis CI, CircleCI, Wercker, and Drone.

{% Img src="image/admin/RKpNwfcXRoKZ34qDoxcO.jpg", alt="Bundlesize check status on GitHub", width="800", height="326", class="screenshot" %}
Bundlesize check status on GitHub

You may have a fast app today, but adding new code can often change this. Checking pull requests with bundlesize will help you avoid performance regressions. Bootstrap, Tinder, Trivago and many others use it to keep their budgets in check.

With bundlesize, it's possible to set thresholds for each file separately. This is especially useful if you are splitting a bundle in your application.

By default, it tests gzipped asset sizes. You can use the compression option to switch to brotli compression or turn it off completely.

Lighthouse Bot

{% Img src="image/admin/5zLlIEUm06wEbu3J1WjG.png", alt="Lighthouse Bot", width="800", height="267" %}

Lighthouse Bot integrates with Travis CI and enforces budgets based on any of the five Lighthouse audit categories. For example, a budget of 100 for your Lighthouse performance score. It's sometimes simpler to keep an eye on a single number than individual asset budgets and Lighthouse scores take many things into account.

{% Img src="image/admin/6yVLSYcr0eCytJixZ1bK.png", alt="Lighthouse scores 💯", width="800", height="211", class="screenshot" %}
Lighthouse scores 💯

Lighthouse Bot runs an audit after you deploy a site to staging server. In .travis.yml set budgets for particular Lighthouse categories with --perf, --a11y, --bp, --seo or --pwa options. Aim to stay in the green zone with scores of at least 90.

after_success:
  - ./deploy.sh # Deploy the PR changes to staging server
  - npm run lh -- --perf=96 https://staging.example.com # Run Lighthouse test

If the scores for a pull request on GitHub fall below the threshold you've set, Lighthouse Bot can prevent pull request from being merged. ⛔

{% Img src="image/admin/nVDqJAA1DZDFqpOaQA9J.png", alt="Lighthouse Bot check status on GitHub", width="800", height="175", class="screenshot" %}
Lighthouse Bot check status on GitHub

Lighthouse Bot then comments on your pull request with updated scores. This is a neat feature which encourages conversation about performance as code changes are happening.

{% Img src="image/admin/q1BUp838y2oSs70Suxyb.png", alt="Lighthouse reporting scores on pull request", width="800", height="383" %}
Lighthouse reporting scores on pull request 💬

If you find your pull request blocked by a poor Lighthouse score, run an audit with Lighthouse CLI or in Dev Tools. It generates a report with details about bottlenecks and hints for simple optimizations.

Summary

Tool CLI CI Summary
Lighthouse ✔️ ❌
  • Budgets for different types of resources based on their size or count.
webpack ✔️ ❌
  • Budgets based on sizes of assets generated by webpack.
  • Checks uncompressed sizes.
bundlesize ✔️ ✔️
  • Budgets based on sizes of specific resources.
  • Checks compressed or uncompressed sizes.
Lighthouse Bot ❌ ✔️
  • Budgets based on Lighthouse scores.

Use lazy loading to improve loading speed

The portion of images and video in the typical payload of a website can be significant. Unfortunately, project stakeholders may be unwilling to cut any media resources from their existing applications. Such impasses are frustrating, especially when all parties involved want to improve site performance, but can't agree on how to get there. Fortunately, lazy loading is a solution that lowers initial page payload and load time, but doesn't skimp on content.

What is lazy loading? {: #what }

Lazy loading is a technique that defers loading of non-critical resources at page load time. Instead, these non-critical resources are loaded at the moment of need. Where images are concerned, "non-critical" is often synonymous with "off-screen". If you've used Lighthouse and examined some opportunities for improvement, you may have seen some guidance in this realm in the form of the Defer offscreen images audit:

{% Img src="image/admin/63NnMISWUUWD3mvAliwe.png", alt="A screenshot of the Defer offscreen images audit in Lighthouse.", width="800", height="102" %}
One of Lighthouse's performance audits is to identify off screen images, which are candidates for lazy loading.

You've probably already seen lazy loading in action, and it goes something like this:

An example of image lazy loading can be found on the popular publishing platform Medium, which loads lightweight placeholder images at page load, and replaces them with lazily-loaded images as they're scrolled into the viewport.

{% Img src="image/admin/p5ahQ67QtZ20bgto7Kpy.jpg", alt="A screenshot of the website Medium in the browsing, demonstrating lazy loading in action. The blurry placeholder is on the left, and the loaded resource is on the right.", width="800", height="493" %}
An example of image lazy loading in action. A placeholder image is loaded at page load (left), and when scrolled into the viewport, the final image loads at the time of need.

If you're unfamiliar with lazy loading, you might be wondering just how useful the technique is, and what its benefits are. Read on to find out!

Why lazy-load images or video instead of just loading them? {: #why }

Because it's possible you're loading stuff the user may never see. This is problematic for a couple reasons:

Lazy loading images and video reduces initial page load time, initial page weight, and system resource usage, all of which have positive impacts on performance.

Implementing lazy loading {: #implementing }

There are a number of ways to implement lazy loading. Your choice of solution must take into account the browsers you support, and also what you are trying to lazy-load.

Modern browsers implement browser-level lazy loading, which can be enabled using the loading attribute on images and iframes. To provide compatibility with older browsers or to perform lazy loading on elements without built-in lazy loading you can implement a solution with your own JavaScript. There are also a number of existing libraries to help you to do this. See the posts on this site for full details of all of these approaches:

Also, we have compiled a list of potential issues with lazy loading, and things to watch out for in your implementation.

Conclusion

Used with care, lazy loading images and video can seriously lower the initial load time and page payloads on your site, including Core Web Vitals. Users won't incur unnecessary network activity—including network contention on slower connections—and processing costs of media resources they may never see, but they can still view those resources if they want.

As far as performance improvement techniques go, lazy loading is reasonably uncontroversial. If you have a lot of inline imagery in your site, it's a perfectly fine way to cut down on unnecessary downloads. Your site's users and project stakeholders will appreciate it!

Lazy loading best practices

While lazy loading images and video have positive and measurable performance benefits, it's not a task to be taken lightly. If you get it wrong, there could be unintended consequences. As such, it's important to keep the following concerns in mind.

Mind the fold {: #wrong-fold }

It may be tempting to lazy-load every single media resource on the page with JavaScript, but you need to resist this temptation. Anything resting above the fold shouldn't be lazy-loaded. Such resources should be considered critical assets, and thus should be loaded normally.

Lazy loading delays the loading of resources until after the DOM is interactive when scripts have finished loading and begin execution. For images below the fold, this is fine, but critical resources above the fold should be loaded with the standard <img> element so they're displayed as soon as possible.

Of course, where the fold lies is not so clear these days when websites are viewed on so many screens of varying sizes. What lies above the fold on a laptop may well lie below it on mobile devices. There's no bulletproof advice for addressing this optimally in every situation. You'll need to conduct an inventory of your page's critical assets, and load those images in typical fashion.

Additionally, you may not want to be so strict about the fold line as the threshold for triggering lazy loading. It may be more ideal for your purposes to establish a buffer zone some distance below the fold so that images begin loading well before the user scrolls them into the viewport. For example, the Intersection Observer API allows you to specify a rootMargin property in an options object when you create a new IntersectionObserver instance. This effectively gives elements a buffer, which triggers lazy loading behavior before the element is in the viewport:

let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
  // lazy-loading image code goes here
}, {
  rootMargin: "0px 0px 256px 0px"
});

If the value for rootMargin looks similar to values you'd specify for a CSS margin property, that's because it is! In this case, the bottom margin of the observed element (the browser viewport by default, but this can be changed to a specific element using the root property) is broadened by 256 pixels. That means the callback function will execute when an image element is within 256 pixels of the viewport and the image will begin to load before the user actually sees it.

To achieve this same effect in browsers that don't support Intersection Observe, use scroll event handling code and adjust your getBoundingClientRect check to include a buffer.

Layout shifting and placeholders {: #wrong-layout-shifting }

Lazy loading media can cause shifting in the layout if placeholders aren't used. These changes can be disorienting for users and trigger expensive DOM layout operations that consume system resources and contribute to jank. At a minimum, consider using a solid color placeholder occupying the same dimensions as the target image, or techniques such as LQIP or SQIP that hint at the content of a media item before it loads.

For <img> tags, src should initially point to a placeholder until that attribute is updated with the final image URL. Use the poster attribute in a <video> element to point to a placeholder image. Additionally, use width and height attributes on both <img> and <video> tags. This ensures that transitioning from placeholders to final images won't change the rendered size of the element as media loads.

Image decoding delays {: #wrong-decoding-delays }

Loading large images in JavaScript and dropping them into the DOM can tie up the main thread, causing the user interface to be unresponsive for a short period of time while decoding occurs. Asynchronously decoding images using the decode method prior to inserting them into the DOM can cut down on this sort of jank, but beware: It's not available everywhere yet, and it adds complexity to lazy loading logic. If you want to use it, you'll need to check for it. Below shows how you might use Image.decode() with a fallback:

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

if ("decode" in newImage) {
  // Fancy decoding logic
  newImage.decode().then(function() {
    imageContainer.appendChild(newImage);
  });
} else {
  // Regular image load
  imageContainer.appendChild(newImage);
}

Check out this CodePen link to see code similar to this example in action. If most of your images are fairly small, this may not do much for you, but it can certainly help cut down on jank when lazy loading large images and inserting them into the DOM.

When stuff doesn't load {: #wrong-loading-failure }

Sometimes media resources fail to load for one reason or another and errors occur. When might this happen? It depends, but here's one hypothetical scenario for you: You have an HTML caching policy for a short period of time (e.g., five minutes), and the user visits the site or a user has a left a stale tab open for a long period of time (e.g., several hours) and comes back to read your content. At some point in this process, a redeployment occurs. During this deployment, an image resource's name changes due to hash-based versioning, or is removed altogether. By the time the user lazy-loads the image, the resource is unavailable, and thus fails.

While these are relatively rare occurrences, it may behoove you to have a backup plan if lazy loading fails. For images, such a solution may look something like this:

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

newImage.onerror = function(){
  // Decide what to do on error
};
newImage.onload = function(){
  // Load the image
};

What you decide to do in the event of an error depends on your application. For example, you could replace the image placeholder area with a button that allows the user to attempt to load the image again, or simply display an error message in the image placeholder area.

Other scenarios could arise as well. Whatever you do, it's never a bad idea to signal to the user when an error has occurred, and possibly give them an action to take if something goes awry.

JavaScript availability {: #wrong-no-js }

It shouldn't be assumed that JavaScript is always available. If you're going to lazy-load images, consider offering <noscript> markup that will show images in case JavaScript is unavailable. The simplest possible fallback example involves using <noscript> elements to serve images if JavaScript is turned off:

I'm an image! I'm an image!

If JavaScript is turned off, users will see both the placeholder image and the image contained with the <noscript> elements. To get around this, place a class of no-js on the <html> tag like so:

<html class="no-js">

Then place one line of inline script in the <head> before any style sheets are requested via <link> tags that removes the no-js class from the <html> element if JavaScript is on:

<script>document.documentElement.classList.remove("no-js");</script>

Finally, use some CSS to hide elements with a class of lazy when JavaScript is unavailable:

.no-js .lazy {
  display: none;
}

This doesn't prevent placeholder images from loading, but the outcome is more desirable. People with JavaScript turned off get something more than placeholder images, which is better than placeholders and no meaningful image content at all.

Lazy loading images

Images can appear on a webpage due to being inline in the HTML as <img> elements or as CSS background images. In this post you will find out how to lazy-load both types of image.

Inline images {: #images-inline }

The most common lazy loading candidates are images as used in <img> elements. With inline images we have three options for lazy loading, which may be used in combination for the best compatibility across browsers:

Using browser-level lazy loading {: #images-inline-browser-level }

Chrome and Firefox both support lazy loading with the loading attribute. This attribute can be added to <img> elements, and also to <iframe> elements. A value of lazy tells the browser to load the image immediately if it is in the viewport, and to fetch other images when the user scrolls near them.

{% Aside %} Note <iframe loading="lazy"> is currently non-standard. While implemented in Chromium, it does not yet have a specification and is subject to future change when this does happen. We suggest not to lazy-load iframes using the loading attribute until it becomes part of the specification. {% endAside %}

See the loading field of MDN's browser compatibility table for details of browser support. If the browser does not support lazy loading then the attribute will be ignored and images will load immediately, as normal.

For most websites, adding this attribute to inline images will be a performance boost and save users loading images that they may not ever scroll to. If you have large numbers of images and want to be sure that users of browsers without support for lazy loading benefit you will need to combine this with one of the methods explained next.

To learn more, check out Browser-level lazy loading for the web.

Using Intersection Observer {: #images-inline-intersection-observer }

To polyfill lazy loading of <img> elements, we use JavaScript to check if they're in the viewport. If they are, their src (and sometimes srcset) attributes are populated with URLs to the desired image content.

If you've written lazy loading code before, you may have accomplished your task by using event handlers such as scroll or resize. While this approach is the most compatible across browsers, modern browsers offer a more performant and efficient way to do the work of checking element visibility via the Intersection Observer API.

Intersection Observer is easier to use and read than code relying on various event handlers, because you only need to register an observer to watch elements rather than writing tedious element visibility detection code. All that's left to do is to decide what to do when an element is visible. Let's assume this basic markup pattern for your lazily loaded <img> elements:

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">

There are three relevant pieces of this markup that you should focus on:

  1. The class attribute, which is what you'll select the element with in JavaScript.
  2. The src attribute, which references a placeholder image that will appear when the page first loads.
  3. The data-src and data-srcset attributes, which are placeholder attributes containing the URL for the image you'll load once the element is in the viewport.

Now let's see how to use Intersection Observer in JavaScript to lazy-load images using this markup pattern:

document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to event handlers here
  }
});

On the document's DOMContentLoaded event, this script queries the DOM for all <img> elements with a class of lazy. If Intersection Observer is available, create a new observer that runs a callback when img.lazy elements enter the viewport.

{% Glitch { id: 'lazy-intersection-observer', path: 'index.html', previewSize: 0 } %}

Intersection Observer is available in all modern browsers. Therefore using it as a polyfill for loading="lazy" will ensure that lazy loading is available for most visitors.

Images in CSS {: #images-css }

While <img> tags are the most common way of using images on web pages, images can also be invoked via the CSS background-image property (and other properties). Browser-level lazy loading does not apply to CSS background images, so you need to consider other methods if you have background images to lazy-load.

Unlike <img> elements which load regardless of their visibility, image loading behavior in CSS is done with more speculation. When the document and CSS object models and render tree are built, the browser examines how CSS is applied to a document before requesting external resources. If the browser has determined a CSS rule involving an external resource doesn't apply to the document as it's currently constructed, the browser doesn't request it.

This speculative behavior can be used to defer the loading of images in CSS by using JavaScript to determine when an element is within the viewport, and subsequently applying a class to that element that applies styling invoking a background image. This causes the image to be downloaded at the time of need instead of at initial load. For example, let's take an element that contains a large hero background image:

<div class="lazy-background">
  <h1>Here's a hero heading to get your attention!</h1>
  <p>Here's hero copy to convince you to buy a thing!</p>
  <a href="/buy-a-thing">Buy a thing!</a>
</div>

The div.lazy-background element would normally contain the hero background image invoked by some CSS. In this lazy loading example, however, you can isolate the div.lazy-background element's background-image property via a visible class added to the element when it's in the viewport:

.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}

.lazy-background.visible {
  background-image: url("hero.jpg"); /* The final image */
}

From here, use JavaScript to check if the element is in the viewport (with Intersection Observer!), and add the visible class to the div.lazy-background element at that time, which loads the image:

document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));

  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });

    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});

{% Glitch { id: 'lazy-background', path: 'index.html', previewSize: 0 } %}

Effects on Largest Contentful Paint (LCP)

Lazy loading is a great optimization that reduces both overall data usage and network contention during startup by deferring the loading of images to when they're actually needed. This can improve startup time, and reduce processing on the main thread by reducing time needed for image decodes.

However, lazy loading is a technique that can affect your website's Largest Contentful Paint LCP in a negative way if you're too eager with the technique. One thing you will want to avoid is lazy loading images that are in the viewport during startup.

When using JavaScript-based lazy loaders, you will want to avoid lazy loading in-viewport images because these solutions often use a data-src or data-srcset attribute as a placeholder for src and srcset attributes. The problem here is that the loading of these images will be delayed because the browser preload scanner can't find them during startup.

Even using browser-level lazy loading to lazy load an in-viewport image can backfire. When loading="lazy" is applied to an in-viewport image, that image will be delayed until the browser knows for sure it's in the viewport, which can affect a page's LCP.

Never lazy load images that are visible in the viewport during startup. It's a pattern that will affect your site's LCP negatively, and therefore the user experience. If you need an image at startup, load it at startup as quickly as possible by not lazy loading it!

Lazy loading libraries {: #libraries }

You should use browser-level lazy loading whenever possible, but if you find yourself in a situation where that isn't an option—such as a significant group of users still reliant on older browsers—the following libraries can be used to lazy-load images:

Lazy loading video

As with image elements, you can also lazy-load video. Videos are commonly loaded with the <video> element (although an alternate method using <img> has emerged with limited implementation). How to lazy-load <video> depends on the use case, though. Let's discuss a couple of scenarios that each require a different solution.

For video that doesn't autoplay {: #video-no-autoplay }

For videos where playback is initiated by the user (that is, videos that don't autoplay), specifying the preload attribute on the <video> element may be desirable:

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

{% Aside 'important' %} A video poster image can qualify as an LCP candidates. If your poster image is an LCP candidate, you should preload it with a fetchpriority attribute value of "high" so the user sees it as soon as possible. {% endAside %}

The example above uses a preload attribute with a value of none to prevent browsers from preloading any video data. The poster attribute gives the <video> element a placeholder that will occupy the space while the video loads. The reason for this is that default behaviors for loading video can vary from browser to browser:

Because browser default behaviors with regard to preload are not set in stone, being explicit is probably your best bet. In this cases where the user initiates playback, using preload="none" is the easiest way to defer loading of video on all platforms. The preload attribute isn't the only way to defer the loading of video content. Fast Playback with Video Preload may give you some ideas and insight into working with video playback in JavaScript.

Unfortunately, it doesn't prove useful when you want to use video in place of animated GIFs, which is covered next.

For video acting as an animated GIF replacement {: #video-gif-replacement }

While animated GIFs enjoy wide use, they're subpar to video equivalents in a number of ways, particularly in file size. Animated GIFs can stretch into the range of several megabytes of data. Videos of similar visual quality tend to be far smaller.

Using the <video> element as a replacement for animated GIF is not as straightforward as the <img> element. Animated GIFs have three characteristics:

  1. They play automatically when loaded.
  2. They loop continuously (though that's not always the case).
  3. They don't have an audio track.

Achieving this with the <video> element looks something like this:

<video autoplay muted loop playsinline>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

The autoplay, muted, and loop attributes are self-explanatory. playsinline is necessary for autoplaying to occur in iOS. Now you have a serviceable video-as-GIF replacement that works across platforms. But how to go about lazy loading it? To start, modify your <video> markup accordingly:

<video class="lazy" autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

You'll notice the addition of the poster attribute, which lets you specify a placeholder to occupy the <video> element's space until the video is lazy-loaded. As with the <img> lazy-loading examples, stash the video URL in the data-src attribute on each <source> element. From there, use JavaScript code similar to the Intersection Observer-based image lazy loading examples:

document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

When you lazy-load a <video> element, you need to iterate through all of the child <source> elements and flip their data-src attributes to src attributes. Once you've done that, you need to trigger loading of the video by calling the element's load method, after which the media will begin playing automatically per the autoplay attribute.

Using this method, you have a video solution that emulates animated GIF behavior, but doesn't incur the same intensive data usage as animated GIFs do, and you can lazy-load that content.

Lazy loading libraries {: #libraries }

The following libraries can help you to lazy-load video:

Each of these lazy loading libraries is well documented, with plenty of markup patterns for your various lazy loading endeavors.

Minify CSS

CSS files can contain unnecessary characters, such as comments, whitespaces, and indentation. In production, these characters can be safely removed, to reduce file size without affecting how the browser processes the styles. This technique is called minification.

Loading unminified CSS

Take a look at the following CSS block:

body {
  font-family: "Benton Sans", "Helvetica Neue", helvetica, arial, sans-serif;
  margin: 2em;
}

/* all titles need to have the same font, color and background */
h1 {
  font-style: italic;
  color: #373fff;
  background-color: #000000;
}

h2 {
  font-style: italic;
  color: #373fff;
  background-color: #000000;
}

This content is easy to read, at the cost of producing a larger than necessary file:

After making these changes, you would obtain a more compact version of the same styles:

body{font-family:"Benton Sans","Helvetica Neue",helvetica,arial,sans-serif;margin:2em}h1,h2{font-style:italic;color:#373fff;background-color:#000}

You probably don't want to write CSS like that. Instead, you can write CSS as usual, and add a minification step to your build process. In this guide, you'll learn how to do it by using a popular build tool: webpack.

Measure

You'll apply CSS minification to a site that has been used in other guides: Fav Kitties. This version of the site uses a cool CSS library: animate.css, to animate different page elements when a user votes for a cat 😺.

As a first step, you need to understand what would be the opportunity after minifying this file:

  1. Open the measure page.
  2. Enter the URL: https://fav-kitties-animated.glitch.me and click Run Audit.
  3. Click View report.
  4. Click on Performance and go the Opportunities section.

The resulting report shows that up to 16 KB can be saved from the animate.css file:

{% Img src="image/admin/RFMk5OMAIvOlkUZJTsh4.png", alt="Lighthouse: Minify CSS opportunity.", width="800", height="172", class="screenshot" %}

Now inspect the content of the CSS:

  1. Open the Fav Kitties site in Chrome. (It might take a while for Glitch servers to respond the first time.) {% Instruction 'devtools-network', 'ol' %}
  2. Click the CSS filter.
  3. Select the Disable cache checkbox. {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/WgneNAyftk8jneyXxMih.png", alt="DevTools CSS unoptimized trace", width="800", height="138" %}

The page is requesting two CSS files, of 1.9KB and 76.2KB respectively.

  1. Click animate.css.
  2. Click the Response tab, to see the file contents.

Note that the stylesheet contains characters for whitespaces and indentation:

{% Img src="image/admin/UEB5Xxe5IHhGtMx3XfKD.png", alt="DevTools CSS unoptimized response", width="800", height="286" %}

Next, you'll add some webpack plugins to your build process to minify these files.

{% Aside 'note' %} Note: the previous Lighthouse report only lists animate.css as an opportunity for minification. Minifying style.css will also save some bytes, but not enough for Lighthouse to consider it a significant savings. However, minifying CSS is a general best practice; so it makes sense to minify all of your CSS files. {% endAside %}

CSS Minification with webpack

Before jumping into the optimizations, take some time understanding how build process for the Fav Kitties site works:

{% Glitch { id: 'fav-kitties-animated', path: 'webpack.config.js', previewSize: 0 } %}

By default, the resulting JS bundle that webpack produces would contain the content of the CSS files inlined. Since we want to maintain separate CSS files, we are using two complementary plugins:

You will now make some changes in the project:

  1. Open the Fav Kitties project in Glitch. {% Instruction 'source', 'ol' %} {% Instruction 'remix', 'ol' %} {% Instruction 'console', 'ol' %}

To minify the resulting CSS, you'll use the optimize-css-assets-webpack-plugin:

  1. In Glitch console, run npm install --save-dev optimize-css-assets-webpack-plugin.
  2. Run refresh, so the changes are synchronized with the Glitch editor.

Next, go back to the Glitch editor, open the webpack.config.js file, and make the following modifications:

Load the module at the beginning of the file:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

Then, pass an instance of the plugin to the plugins array:

  plugins: [
    new HtmlWebpackPlugin({template: "./src/index.html"}),
    new MiniCssExtractPlugin({filename: "[name].css"}),
    new FixStyleOnlyEntriesPlugin(),
    new OptimizeCSSAssetsPlugin({})
  ]

After making the changes a rebuild of the project will be triggered. This is how the resulting webpack.config.js will look like:

{% Glitch { id: 'fav-kitties-animated-min', path: 'webpack.config.js', previewSize: 0 } %}

Next, you'll check the result of this optimization with performance tools.

Verify

{% Instruction 'preview' %}

If you got lost in any previous step, you can click here, to open an optimized version of the site.

To inspect the size and content of the files:

{% Instruction 'devtools-network', 'ol' %}

  1. Click the CSS filter.
  2. Select the Disable cache checkbox if it isn't already. {% Instruction 'reload-app', 'ol' %}

{% Img src="image/admin/id5kWwB3NilmVPWPTM59.png", alt="DevTools CSS unoptimized response", width="800", height="130" %}

You can inspect these files, and see that the new versions don't contain any whitespaces. Both files are much smaller, in particular, the animate.css has been reduced in ~26%, saving ~20KB!

As a final step:

  1. Open the measure page.
  2. Enter the URL of the optimized site.
  3. Click View report.
  4. Click on Performance and find the Opportunities section.

The report doesn't show "Minify CSS" as "Opportunity" anymore, and has now moved to "Passed Audits" section:

{% Img src="image/admin/zegn2qIHYYK58w1GhgYd.png", alt="Lighthouse Passed Audits for optimized page.", width="800", height="163" %}

Since CSS files are render-blocking resources, if you apply minification on sites that use large CSS files, you can see improvements on metrics like First Contentful Paint.

Next steps and resources

In this guide, we've covered CSS Minification with webpack, but the same approach can be followed with other build tools, like gulp-clean-css for Gulp, or grunt-contrib-cssmin for Grunt.

Minification can also be applied to other types of files. Check out the Minify and compress network payloads guide to learn more about tools to minify JS, and some complementary techniques, like compression.

Assessing loading performance in the field with Navigation Timing and Resource Timing

If you've used connection throttling in the network panel in a browser's developer tools (or Lighthouse in Chrome) to assess loading performance, you know how convenient those tools are for performance tuning. You can quickly measure the impact of performance optimizations with a consistent and stable baseline connection speed. The only problem is that this is synthetic testing, which yields lab data, not field data.

Synthetic testing isn't inherently bad, but it's not representative of how fast your website is loading for real users. That requires field data, which you can collect from the Navigation Timing and Resource Timing APIs.

APIs to help you assess loading performance in the field

Navigation Timing and Resource Timing are two similar APIs with significant overlap that measure two distinct things:

These APIs expose their data in a performance entry buffer, which can be accessed in the browser with JavaScript. There are multiple ways to query a performance buffer, but a common way is by using performance.getEntriesByType:

// Get Navigation Timing entries:
performance.getEntriesByType('navigation');

// Get Resource Timing entries:
performance.getEntriesByType('resource');

performance.getEntriesByType accepts a string describing the type of entries you want to retrieve from the performance entry buffer. 'navigation' and 'resource' retrieve timings for the Navigation Timing and Resource Timing APIs, respectively.

{% Aside %} Try loading a website and then enter either of the commands in the above code snippet in your browser's console to see actual timings captured by your browser. {% endAside %}

The amount of information these APIs provide can be overwhelming, but they're your key to measuring loading performance in the field, as you can gather these timings from users as they visit your website.

The life and timings of a network request

Gathering and analyzing navigation and resource timings is sort of like archeology in that you're reconstructing the fleeting life of a network request after the fact. Sometimes it helps to visualize concepts, and where network requests are concerned, your browser's developer tools can help.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/BxgZyzAEyFnk846wvzNa.png", alt="A diagram of network timings as shown in Chrome's DevTools. The timings depicted are for request queueing, connection negotiation, the request itself, and the response in color-coded bars.", width="800", height="583" %}
A visualization of a network request in the network panel of Chrome's DevTools

The life of a network request has distinct phases, such as DNS lookup, connection establishment, TLS negotiation, and so on. These timings are represented as a DOMHighResTimestamp. Depending on your browser, the granularity of timings may be down to the microsecond, or be rounded up to milliseconds. Let's examine these phases in detail, and how they relate to Navigation Timing and Resource Timing.

{% Aside %} As you read this guide, this diagram for both Navigation Timing and Resource Timing may help you to visualize the order of the timings they provide. {% endAside %}

DNS lookup

When a user goes to a URL, the Domain Name System (DNS) is queried to translate a domain to an IP address. This process may take significant time—time you'll want to measure in the field, even. Navigation Timing and Resource Timing expose two DNS-related timings:

Calculating total DNS lookup time can be done by subtracting the start metric from the end metric:

// Measuring DNS lookup time
const [pageNav] = performance.getEntriesByType('navigation');
const totalLookupTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

{% Aside 'caution' %} You can't always rely on timings to be populated. Timings provided in both APIs will have a value of 0 in some cases. For example, a DNS lookup may be served by a local cache. Additionally, any timings for cross-origin requests may be unavailable if those origins don't set a Timing-Allow-Origin response header. {% endAside %}

Connection negotiation

Another contributing factor to loading performance is connection negotiation, which is latency incurred when connecting to a web server. If HTTPS is involved, this process will also include TLS negotiation time. The connection phase consists of three timings:

Measuring total connection time is similar to measuring total DNS lookup time: you subtract the start timing from the end timing. However, there's an additional secureConnectionStart property that may be 0 if HTTPS isn't used or if the connection is persistent. If you want to measure TLS negotiation time, you'll need to keep that in mind:

// Quantifying total connection time
const [pageNav] = performance.getEntriesByType('navigation');
const connectionTime = pageNav.connectEnd - pageNav.connectStart;
let tlsTime = 0; // <-- Assume 0 to start with

// Was there TLS negotiation?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

Once DNS lookup and connection negotiation ends, timings related to fetching documents and their dependent resources come into play.

Requests and responses

Loading performance is affected by two types of factors:

Both types of factors affect loading performance. Timings related to these factors are vital, as they describe how long resources take to download. Both Navigation Timing and Resource Timing describe loading performance with the following metrics:

These timings allow you to measure multiple aspects of loading performance, such as cache lookup within a service worker and download time:

// Cache seek plus response time of the current document
const [pageNav] = performance.getEntriesByType('navigation');
const fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
let workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

You can also measure other aspects of request/response latency:

const [pageNav] = performance.getEntriesByType('navigation');

// Request time only (excluding redirects, DNS, and connection/TLS time)
const requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
const responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
const requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

Other measurements you can make

Navigation Timing and Resource Timing is useful for more than what the examples above outline. Here are some other situations with relevant timings that may be worth exploring:

{% Aside 'warning' %} Timings related to document unloading and processing are available only in Navigation Timing, as they only apply to navigation requests. {% endAside %}

Acquiring timings in application code

All of the examples shown so far use performance.getEntriesByType, but there are other ways to query the performance entry buffer, such as performance.getEntriesByName and performance.getEntries. These methods are fine when only light analysis is needed. In other situations, though, they can introduce excessive main thread work by iterating over a large number of entries, or even repeatedly polling the performance buffer to find new entries.

// Create the performance observer:
const perfObserver = new PerformanceObserver((observedEntries) => {
  // Get all resource entries collected so far:
  const entries = observedEntries.getEntries();

  // Iterate over entries:
  for (let i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer for Navigation Timing entries:
perfObserver.observe({
  type: 'navigation',
  buffered: true
});

// Run the observer for Resource Timing entries:
perfObserver.observe({
  type: 'resource',
  buffered: true
});

{% Aside %} Adding the buffered option to a performance observer's observe event ensures that performance entries added to the buffer prior to the instantiation of the performance observer are observable. {% endAside %}

This method of collecting timings may feel awkward when compared to directly accessing the performance entry buffer, but it's preferable to tying up the main thread with work that doesn't serve a critical and user-facing purpose.

Phoning home

Once you've collected all the timings you need, you can send them to an endpoint for further analysis. Two ways to do this are with either navigator.sendBeacon or a fetch with the keepalive option set. Both methods will send a request to a specified endpoint in a non-blocking way, and the request will be queued in a way that outlives the current page session if need be:

// Caution: If you have lots of performance entries, don't
// do this. This is an example for illustrative purposes.
const data = JSON.stringify(performance.getEntries()));

// The endpoint to transmit the encoded data to
const endpoint = '/analytics';

// Check for fetch keepalive support
if ('keepalive' in Request.prototype) {
  fetch(endpoint, {
    method: 'POST',
    body: data,
    keepalive: true,
    headers: {
      'Content-Type': 'application/json'
    }
  });
} else if ('sendBeacon' in navigator) {
  // Use sendBeacon as a fallback
  navigator.sendBeacon(endpoint, data);
}

In this example, the JSON string will arrive in a POST payload that you can decode and process/store in an application backend as needed.

Wrapping up

Once you have metrics collected, it's up to you to figure out how to analyze that field data. When analyzing field data, there are a few general rules to follow to ensure you're drawing meaningful conclusions:

This guide isn't meant to be an exhaustive resource on Navigation or Resource Timing, but a starting point. Below are some additional resources you may find helpful:

With these APIs and the data they provide, you'll be better equipped to understand how loading performance is experienced by real users, which will give you more confidence in diagnosing and addressing loading performance problems in the field.

Optimize CSS background images with media queries

Many sites request heavy resources, like images, that are not optimized for certain screens, and send large CSS files containing styles that some devices will never use. Using media queries is a popular technique for delivering tailored stylesheets and assets to different screens to reduce the amount of data transferred to users and improve page load performance. This guide shows you how to use media queries to send images that are only as large as they need to be, a technique commonly known as responsive images.

Prerequisites

This guide assumes that you're familiar with Chrome DevTools. You can use another browser's DevTools instead if you prefer. You'll just need to map the Chrome DevTools screenshots in this guide back to the relevant features in your browser of choice.

Understand responsive background images

First, analyze the network traffic of the unoptimized demo:

  1. Open the unoptimized demo in a new Chrome tab. {% Instruction 'devtools-network', 'ol' %} {% Instruction 'reload-page', 'ol' %}

{% Aside %} Check out Inspect Network Activity With Chrome DevTools if you need more help with DevTools. {% endAside %}

You'll see that the only image that's being requested is background-desktop.jpg, which has a size of 1006KB:

{% Img src="image/admin/K8P4MHp2FSnZYTw3ZVkG.png", alt="DevTools network trace for the unoptimized background image.", width="800", height="126" %}

Resize the browser window and notice that the Network Log isn't showing any new requests being made by the page. This means that the same image background is being used for all screen sizes.

You can see the styles that control the background image in style.css:

body {
  background-position: center center;
  background-attachment: fixed;
  background-repeat: no-repeat; background-size: cover;
  background-image: url(images/background-desktop.jpg);
}

Here's the meaning of each of the properties used:

When combined, these styles tell the browser to adapt the background image to different screen heights and widths. This is the first step towards achieving a responsive background.

Using a single background image for all screen sizes has some limitations:

In the next section, you'll learn how to apply an optimization to load different background images, according to the user's device.

Use media queries

Using media queries is a common technique to declare stylesheets that will only apply to certain media or device types. They are implemented by using @media rules, which let you define a set of breakpoints, where specific styles are defined. When the conditions defined by the @media rule are met (for example, a certain screen width), the group of styles defined inside the breakpoint will be applied.

The following steps can be used to apply media queries to the site so that different images are used, depending on the maximum width of the device requesting the page.

body {
  background-position: center center;
  background-attachment: fixed;
  background-repeat: no-repeat; background-size: cover;
  background-image: url(images/background-desktop.jpg);
}

For mobile:

@media (max-width: 480px) {
    body {
        background-image: url(images/background-mobile.jpg);
    }
}

For tablets:

@media (min-width: 481px) and (max-width: 1024px) {
    body {
        background-image: url(images/background-tablet.jpg);
    }
}

For desktop devices:

@media (min-width: 1025px) {
    body {
	    background-image: url(images/background-desktop.jpg);
   }
}

Open the optimized version of style.css in your browser to see the changes made.

{% Aside %} The images used in the optimized demo are already resized to fit into different screen sizes. Showing how to resize images is out of the scope of this guide, but if you want to know more about this, the Serve responsive images guide covers some useful tools, like the sharp npm package and the ImageMagick CLI. {% endAside %}

Measure for different devices

Next visualize the resulting site in different screen sizes and in simulated mobile devices:

  1. Open the optimized site in a new Chrome tab.
  2. Make your viewport narrow (less than 480px). {% Instruction 'devtools-network', 'ol' %} {% Instruction 'reload-page', 'ol' %} Notice how the background-mobile.jpg image was requested.
  3. Make your viewport wider. Once it's wider than 480px notice how background-tablet.jpg is requested. Once it's wider than 1025px notice how background-desktop.jpg is requested.

When the width of the browser screen is changed, new images are requested.

In particular when the width is below the value defined in the mobile breakpoint (480px), you see the following Network Log:

{% Img src="image/admin/jd2kHIefYf91udpFEmvx.png", alt="DevTools network trace for the optimized background image.", width="800", height="125" %}

The size of the new mobile background is 67% smaller than the desktop one.

Effects on Largest Contentful Paint (LCP)

Elements with CSS background images are considered a candidate for Largest Contentful Paint (LCP), however, CSS background images are not discoverable by the browser preload scanner, which means you could be delaying your site's LCP if you're not careful.

The first option you should consider is whether your LCP candidate image could work in an <img> element with srcset and sizes attributes for responsiveness. The browser preload scanner will discover <img> elements, and send requests for them while the parser is blocked on rendering.

If you can't (or don't want to) avoid using a CSS background image, the second option would be to preload responsive images to ensure you preload the right image for the proper viewport size. The <link> elements media, imagesrcset, and imagesizes attributes hint to the browser that a given resource hint only applies in certain viewport conditions, avoiding multiple wasted preloads when you only want to load the one resource that's a fit for the current viewport.

Summary

In this guide you've learned to apply media queries to request background images tailored to specific screen sizes and save bytes when accessing the site on smaller devices, like mobile phones. You used the @media rule to implement a responsive background. This technique is widely supported by all browsers. A new CSS feature: image-set(), can be used for the same purpose with fewer lines of code. At the time of this writing, this feature is not supported in all browsers, but you might want to keep an eye on how adoption evolves, as it can represent an interesting alternative to this technique.

Optimize WebFont loading and rendering

A "full" WebFont that includes all stylistic variants, which you may not need, plus all the glyphs, which may go unused, can easily result in a multi-megabyte download. In this post you will find out how to optimize loading of WebFonts so visitors only download what they will use.

To address the problem of large files containing all variants, the @font-face CSS rule is specifically designed to allow you to split the font family into a collection of resources. For example unicode subsets, and distinct style variants.

Given these declarations, the browser figures out the required subsets and variants and downloads the minimal set required to render the text, which is very convenient. However, if you're not careful, it can also create a performance bottleneck in the critical rendering path and delay text rendering.

The default behavior

Lazy loading of fonts carries an important hidden implication that may delay text rendering. The browser must construct the render tree, which is dependent on the DOM and CSSOM trees, before it knows which font resources it needs to render the text. As a result, font requests are delayed well after other critical resources, and the browser may be blocked from rendering text until the resource is fetched.

{% Img src="image/admin/NgSTa9SirmikQAq1G5fN.png", alt="Font critical rendering path", width="800", height="303" %}

  1. The browser requests the HTML document.
  2. The browser begins parsing the HTML response and constructing the DOM.
  3. The browser discovers CSS, JS, and other resources and dispatches requests.
  4. The browser constructs the CSSOM after all of the CSS content is received and combines it with the DOM tree to construct the render tree.
    • Font requests are dispatched after the render tree indicates which font variants are needed to render the specified text on the page.
  5. The browser performs layout and paints content to the screen.
    • If the font is not yet available, the browser may not render any text pixels.
    • After the font is available, the browser paints the text pixels.

The "race" between the first paint of page content, which can be done shortly after the render tree is built, and the request for the font resource is what creates the "blank text problem" where the browser might render page layout but omits any text.

By preloading WebFonts and using font-display to control how browsers behave with unavailable fonts, you can prevent blank pages and layout shifts due to font loading.

Preload your WebFont resources

If there's a high probability that your page will need a specific WebFont hosted at a URL you know in advance, you can take advantage of resource prioritization. Using <link rel="preload"> will trigger a request for the WebFont early in the critical rendering path, without having to wait for the CSSOM to be created.

Customize the text rendering delay

While preloading makes it more likely that a WebFont will be available when a page's content is rendered, it offers no guarantees. You still need to consider how browsers behave when rendering text that uses a font-family which is not yet available.

In the post Avoid invisible text during font loading you can see that default browser behavior is not consistent. However, you can tell modern browsers how you want them to behave by using font-display.

{% BrowserCompat 'css.at-rules.font-face.font-display' %}

Similar to the existing font timeout behaviors that some browsers implement, font-display segments the lifetime of a font download into three major periods:

  1. The first period is the font block period. During this period, if the font face is not loaded, any element attempting to use it must instead render with an invisible fallback font face. If the font face successfully loads during the block period, the font face is then used normally.
  2. The font swap period occurs immediately after the font block period. During this period, if the font face is not loaded, any element attempting to use it must instead render with a fallback font face. If the font face successfully loads during the swap period, the font face is then used normally.
  3. The font failure period occurs immediately after the font swap period. If the font face is not yet loaded when this period starts, it's marked as a failed load, causing normal font fallback. Otherwise, the font face is used normally.

Understanding these periods means you can use font-display to decide how your font should render depending on whether or when it was downloaded.

To work with the font-display property, add it to your @font-face rules:

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  font-display: auto; /* or block, swap, fallback, optional */
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2'), /* will be preloaded */
       url('/fonts/awesome-l.woff') format('woff'),
       url('/fonts/awesome-l.ttf') format('truetype'),
       url('/fonts/awesome-l.eot') format('embedded-opentype');
  unicode-range: U+000-5FF; /* Latin glyphs */
}

font-display currently supports the following range of values:

For more information on preloading fonts, and the font-display property, see the following posts:

The Font Loading API

The Font Loading API provides a scripting interface to define and manipulate CSS font faces, track their download progress, and override their default lazyload behavior. For example, if you're sure that a particular font variant is required, you can define it and tell the browser to initiate an immediate fetch of the font resource:

{% BrowserCompat 'api.FontFace' %}

var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
  style: 'normal', unicodeRange: 'U+000-5FF', weight: '400'
});

// don't wait for the render tree, initiate an immediate fetch!
font.load().then(function() {
  // apply the font (which may re-render text and cause a page reflow)
  // after the font has finished downloading
  document.fonts.add(font);
  document.body.style.fontFamily = "Awesome Font, serif";

  // OR... by default the content is hidden,
  // and it's rendered after the font is available
  var content = document.getElementById("content");
  content.style.visibility = "visible";

  // OR... apply your own render strategy here...
});

Further, because you can check the font status (via the check()) method and track its download progress, you can also define a custom strategy for rendering text on your pages:

Best of all, you can also mix and match the above strategies for different content on the page. For example, you can delay text rendering on some sections until the font is available, use a fallback font, and then re-render after the font download has finished.

{% Aside %} The Font Loading API is not available in older browsers. Consider using the FontLoader polyfill or the WebFontloader library to deliver similar functionality, albeit with even more overhead from an additional JavaScript dependency. {% endAside %}

Proper caching is a must

Font resources are, typically, static resources that don't see frequent updates. As a result, they are ideally suited for a long max-age expiry— ensure that you specify both a conditional ETag header, and an optimal Cache-Control policy for all font resources.

If your web application uses a service worker, serving font resources with a cache-first strategy is appropriate for most use cases.

You should not store fonts using localStorage or IndexedDB; each of those has its own set of performance issues. The browser's HTTP cache provides the best and most robust mechanism to deliver font resources to the browser.

WebFont loading checklist

Automated testing for WebFont loading behavior with Lighthouse

Lighthouse can help automate the process of making sure that you're following web font optimization best practices.

The following audits can help you make sure that your pages are continuing to follow web font optimization best practices over time:

Performance budgets 101

Performance is an important part of the user experience and it affects business metrics. It's tempting to think that if you are a good developer you'll end up with a performant site, but the truth is that good performance is rarely a side effect. As with most other things—to reach a goal you have to define it clearly. Start the journey by setting a performance budget.

Definition

A performance budget is a set of limits imposed on metrics that affect site performance. This could be the total size of a page, the time it takes to load on a mobile network, or even the number of HTTP requests that are sent. Defining a budget helps get the web performance conversation started. It serves as a point of reference for making decisions about design, technology, and adding features.

Having a budget enables designers to think about the effects of high-resolution images and the number of web fonts they pick. It also helps developers compare different approaches to a problem and evaluate frameworks and libraries based on their size and parsing cost.

Choose metrics

Quantity-based metrics ⚖️

These metrics are useful in the early stages of development because they highlight the impact of including heavy images and scripts. They are also easy to communicate to both designers and developers.

We've already mentioned a few things you can include in a performance budget such as page weight and the number of HTTP requests, but you can split these up into more granular limits like:

However, these numbers don't tell you much about the user experience. Two pages with the same number of requests or same weight can render differently depending on the order in which resources get requested. If a critical resource like a hero image or a stylesheet on one of the pages is loaded late in the process, the users will wait longer to see something useful and perceive the page as slower. If on the other page the most important parts load quickly, they may not even notice if the rest of the page doesn't.

{% Img src="image/admin/U0QhA82KFyED4r1y3tAq.png", alt="Image of progressive page rendering based on the critical-path", width="611", height="300" %}

This is why it's important to keep track of another type of metric.

Milestone timings ⏱️

Milestone timings mark events that happen during page load, such as DOMContentLoaded or load event. The most useful timings are user-centric performance metrics that tell you something about the experience of loading a page. These metrics are available through browser APIs and as part of Lighthouse reports.

First Contentful Paint (FCP) measures when the browser displays the first bit of content from the DOM, like text or images.

Time to Interactive (TTI) measures how long it takes for a page to become fully interactive and reliably respond to user input. It's a very important metric to track if you expect any kind of user interaction on the page like clicking links, buttons, typing or using form elements.

Rule-based metrics 💯

Lighthouse and WebPageTest calculate performance scores based on general best practice rules, that you can use as guidelines. As a bonus, Lighthouse also offers you hints for simple optimizations.

You'll get the best results if you keep track of a combination of quantity-based and user-centric performance metrics. Focus on asset sizes in the early phases of a project and start tracking FCP and TTI as soon as possible.

Establish a baseline

The only way to really know what works best for your site is to try it—research and then test your findings. Analyze the competition to see how you stack up. 🕵️

If you don't have time for that, here are good default numbers to get you started:

These numbers are calculated based on real-world baseline devices and 3G network speed. Over half of the internet traffic today happens on mobile networks, so you should use 3G network speed as a starting point.

Examples of budgets

You should have a budget in place for different types of pages on your site since the content will vary. For example:

Add performance budgets to your build process

{% Img src="image/admin/YKJcgI9Yd8qEZM0nzPuv.png", alt="Webpack, bundlesize and Lighthouse logos", width="800", height="267" %}

Choosing a tool for this will depend a lot on the scale of your project and resources that you can dedicate to the task. There are a few open-source tools that can help you add budgeting to your build process:

If something goes over a defined threshold, you can either:

Track performance

Making sure your site is fast enough means you have to keep measuring after the initial launch. Monitoring these metrics over time and getting data from real users will show you how changes in performance impact key business metrics.

Wrap up

The purpose of a performance budget is to make sure you focus on performance throughout a project and setting it early will help prevent backtracking later. It should be the point of reference for helping you figure out what to include on your website. The main idea is to set goals so that you can better balance performance without harming functionality or user experience.🎯

The next guide will walk you through defining your first performance budget in a few simple steps.

Preload critical assets to improve loading speed

When you open a web page, the browser requests the HTML document from a server, parses its contents, and submits separate requests for any referenced resources. As a developer, you already know about all the resources your page needs and which of them are the most important. You can use that knowledge to request the critical resources ahead of time and speed up the loading process. This post explains how to achieve that with <link rel="preload">.

How preloading works

Preloading is best suited for resources typically discovered late by the browser.

{% Img src="image/admin/Ad9PLq3DcQt9Ycp63z6O.png", alt="Screenshot of Chrome DevTools Network panel.", width="701", height="509" %}
In this example, Pacifico font is defined in the stylesheet with a @font-face rule. The browser loads the font file only after it has finished downloading and parsing the stylesheet.

By preloading a certain resource, you are telling the browser that you would like to fetch it sooner than the browser would otherwise discover it because you are certain that it is important for the current page.

{% Img src="image/admin/PgRbERrxLGfF439yBMeY.png", alt="Screenshot of Chrome DevTools Network panel after applying preloading.", width="701", height="509" %}
In this example, Pacifico font is preloaded, so the download happens in parallel with the stylesheet.

The critical request chain represents the order of resources that are prioritized and fetched by the browser. Lighthouse identifies assets that are on the third level of this chain as late-discovered. You can use the Preload key requests audit to identify which resources to preload.

{% Img src="image/admin/BPUTHBNZFbeXqb0dVx2f.png", alt="Lighthouse's preload key requests audit.", width="745", height="97" %}

You can preload resources by adding a <link> tag with rel="preload" to the head of your HTML document:

The browser caches preloaded resources so they are available immediately when needed. (It doesn't execute the scripts or apply the stylesheets.)

{% Aside %} After implementing preloading, many sites, including Shopify, Financial Times and Treebo, saw 1-second improvements in user-centric metrics such as Time to Interactive and First Contentful Paint. {% endAside %}

Resource hints, for example, preconnectand prefetch, are executed as the browser sees fit. The preload, on the other hand, is mandatory for the browser. Modern browsers are already pretty good at prioritizing resources, that's why it's important to use preload sparingly and only preload the most critical resources.

Unused preloads trigger a Console warning in Chrome, approximately 3 seconds after the load event.

{% Img src="image/admin/z4FbCezjXHxaIhq188TU.png", alt="Chrome DevTools Console warning about unused preloaded resources.", width="800", height="228" %}

{% Aside %} preload is supported in all modern browsers. {% BrowserCompat 'html.elements.link.rel.preload' %} {% endAside %}

Use cases

{% Aside 'caution' %} At the time of writing, Chrome has an open bug for preloaded requests that are fetched sooner than other higher priority resources. Until this is resolved, be wary of how preloaded resources can "jump the queue" and be requested sooner than they should. {% endAside %}

Preloading resources defined in CSS

Fonts defined with @font-face rules or background images defined in CSS files aren't discovered until the browser downloads and parses those CSS files. Preloading these resources ensures they are fetched before the CSS files have downloaded.

Preloading CSS files

If you are using the critical CSS approach, you split your CSS into two parts. The critical CSS required for rendering the above-the-fold content is inlined in the <head> of the document and non-critical CSS is usually lazy-loaded with JavaScript. Waiting for JavaScript to execute before loading non-critical CSS can cause delays in rendering when users scroll, so it's a good idea to use <link rel="preload"> to initiate the download sooner.

Preloading JavaScript files

Because browsers don't execute preloaded files, preloading is useful to separate fetching from execution which can improve metrics such as Time to Interactive. Preloading works best if you split your JavaScript bundles and only preload critical chunks.

How to implement rel=preload

The simplest way to implement preload is to add a <link> tag to the <head> of the document:

Supplying the as attribute helps the browser set the priority of the prefetched resource according to its type, set the right headers, and determine whether the resource already exists in the cache. Accepted values for this attribute include: script, style, font, image, and others.

{% Aside %} Take a look at the Chrome Resource Priorities and Scheduling document to learn more about how the browser prioritizes different types of resources. {% endAside %}

{% Aside 'caution' %} Omitting the as attribute, or having an invalid value is equivalent to an XHR request, where the browser doesn't know what it is fetching so it can't determine the correct priority. It can also cause some resources, such as scripts, to be fetched twice. {% endAside %}

Some types of resources, such as fonts, are loaded in anonymous mode. For those you must set the crossorigin attribute with preload:

{% Aside 'caution' %} Fonts preloaded without the crossorigin attribute will be fetched twice! {% endAside %}

<link> elements also accept a type attribute, which contains the MIME type of the linked resource. The browsers use the value of the type attribute to make sure that resources get preloaded only if their file type is supported. If a browser doesn't support the specified resource type, it will ignore the <link rel="preload">.

{% Aside 'codelab' %} Improve the performance of a page by preloading web fonts. {% endAside %}

You can also preload any type of resource via the Link HTTP header:

Link: </css/style.css>; rel="preload"; as="style"

A benefit of specifying preload in the HTTP Header is that the browser doesn't need to parse the document to discover it, which can offer small improvements in some cases.

Preloading JavaScript modules with webpack

If you are using a module bundler that creates build files of your application, you need to check if it supports the injection of preload tags. With webpack version 4.6.0 or later, preloading is supported through the use of magic comments inside import():

import(_/* webpackPreload: true */_ "CriticalChunk")

If you are using an older version of webpack, use a third-party plugin such as preload-webpack-plugin.

Effects of preloading on Core Web Vitals

Preloading is a powerful performance optimization that has an effect on loading speed. Such optimizations can lead to changes in your site's Core Web Vitals, and it's important to be aware them.

Largest Contentful Paint (LCP)

Preloading has a powerful effect on Largest Contentful Paint (LCP) when it comes to fonts and images, as both images and text nodes can be LCP candidates. Hero images and large runs of text that are rendered using web fonts can benefit significantly from a well-placed preload hint, and should be used when there are opportunities to deliver these important bits of content to the user faster.

However, you want to be careful when it comes to preloading—and other optimizations! In particular, avoid preloading too many resources. If too many resources are prioritized, effectively none of them are. The effects of excessive preload hints will be especially detrimental to those on slower networks where bandwidth contention will be more evident.

Instead, focus on a few high-value resources that you know will benefit from a well-placed preload. When preloading fonts, ensure that you're serving fonts in WOFF 2.0 format to reduce resource load time as much as possible. Since WOFF 2.0 has excellent browser support, using older formats such as WOFF 1.0 or TrueType (TTF) will delay your LCP if the LCP candidate is a text node.

When it comes to LCP and JavaScript, you'll want to ensure that you're sending complete markup from the server in order for the browser's preload scanner to work properly. If you're serving up an experience that relies entirely on JavaScript to render markup and can't send server-rendered HTML, it would be advantageous to step in where the browser preload scanner can't and preload resources that would only otherwise be discoverable when the JavaScript finishes loading and executing.

Cumulative Layout Shift (CLS)

Cumulative Layout Shift (CLS) is an especially important metric where web fonts are concerned, and CLS has significant interplay with web fonts that use the font-display CSS property to manage how fonts are loaded. To minimize web font-related layout shifts, consider the following strategies:

  1. Preload fonts while using the default block value for font-display. This is a delicate balance. Blocking the display of fonts without a fallback can be considered a user experience problem. On one hand, loading fonts with font-display: block; eliminates web font-related layout shifts. On the other hand, you still want to get those web fonts loaded as soon as possible if they're crucial to the user experience. Combining a preload with font-display: block; may be an acceptable compromise.
  2. Preload fonts while using the fallback value for font-display. fallback is a compromise between swap and block, in that it has an extremely short blocking period.
  3. Use the optional value for font-display without a preload. If a web font isn't crucial to the user experience, but it is still used to render a significant amount of page text, consider using the optional value. In adverse conditions, optional will display page text in a fallback font while it loads the font in the background for the next navigation. The net result in these conditions is improved CLS, as system fonts will render immediately, while subsequent page loads will load the font immediately without layout shifts.

CLS is a difficult metric to optimize for when it comes to web fonts. As always, experiment in the lab, but trust your field data to determine if your font loading strategies are improving CLS or making it worse.

Responsiveness metrics

First Input Delay (FID) and Interaction to Next Paint are two metrics that gauge responsiveness to user input. Since the lion's share of interactivity on the web is driven by JavaScript, preloading JavaScript that powers important interactions may help to keep your FID and INP metrics as low as they can possibly be. However, be aware that FID is a load responsiveness metric and INP observes interactions throughout the entire page lifecycle—including during startup. Preloading too much JavaScript during startup can carry unintended negative consequences if too many resources are contending for bandwidth.

You'll also want to be careful about how you go about code splitting. Code splitting is an excellent optimization for reducing the amount of JavaScript loaded during startup, but interactions can be delayed if they rely on JavaScript loaded right at the start of the interaction. To compensate for this, you'll need to examine the user's intent, and inject a preload for the necessary chunk(s) of JavaScript before the interaction takes place. One example could be preloading JavaScript required for validating a form's contents when any of the fields in the form are focused.

Conclusion

To improve page speed, preload important resources that are discovered late by the browser. Preloading everything would be counterproductive so use preload sparingly and measure the impact in the real-world.

Prioritize resources

Not every byte that is sent down the wire to the browser has the same degree of importance, and the browser knows this. Browsers have heuristics that attempt to make a best-guess at the most important resources to load first—such as CSS before scripts and images.

That said, as with any heuristic, it doesn't always work out; the browser might make the wrong decision, usually because it doesn't have enough information at that time. There are ways that you can influence this decision-making using <link rel="preload">, <link rel="preconnect">, and <link rel="prefetch">.

Default priorities in the browser

The browser assigns different relative priorities to different types of resources based on how critical they might be. So, for example, a <script> tag in your page's <head> would be loaded in Chrome at a High priority (below CSS, at Highest), but that priority would change to Low if it has the async attribute (meaning it can be loaded and run asynchronously).

Priorities become important when investigating loading performance in your site. Beyond the usual techniques of measuring and analyzing the critical rendering path, it's useful to know Chrome's priority for each resource. You can find that in the Network panel in Chrome DevTools. Here's what it looks like:

{% Img src="image/admin/oqcdfVAVj2RajVWfs6ap.png", alt="An example of how priorities are displayed in Chrome Developer Tools", width="800", height="249" %}
The Priority column, which is hidden by default (see Add or remove columns .

These priorities give you an idea of how much relative importance the browser attributes to each resource. And remember that subtle differences are enough for the browser to assign a different priority; for example, an image that is part of the initial render is prioritized higher than an image that starts offscreen. If you're curious about priorities, Preload, Prefetch And Priorities in Chrome digs a lot deeper into the current state of priorities in Chrome.

So what can you do if you find any resources that are marked with a different priority than the one you'd want?

There are three different declarative solutions, which are all relatively new <link> types.

Publish, ship, and install modern JavaScript for faster applications

Over 90% of browsers are capable of running modern JavaScript, but the prevalence of legacy JavaScript remains a large source of performance problems on the web today.

Modern JavaScript

Modern JavaScript is not characterized as code written in a specific ECMAScript specification version, but rather in syntax that is supported by all modern browsers. Modern web browsers like Chrome, Edge, Firefox, and Safari make up more than 90% of the browser market, and different browsers that rely on the same underlying rendering engines make up an additional 5%. This means that 95% of global web traffic comes from browsers that support the most widely used JavaScript language features from the past 10 years, including:

Features in newer versions of the language specification generally have less consistent support across modern browsers. For example, many ES2020 and ES2021 features are only supported in 70% of the browser market—still the majority of browsers, but not enough that it's safe to rely on those features directly. This means that although "modern" JavaScript is a moving target, ES2017 has the widest range of browser compatibility while including most of the commonly used modern syntax features. In other words, ES2017 is the closest to modern syntax today.

Legacy JavaScript

Legacy JavaScript is code that specifically avoids using all the above language features. Most developers write their source code using modern syntax, but compile everything to legacy syntax for increased browser support. Compiling to legacy syntax does increase browser support, however the effect is often smaller than we realize. In many cases the support increases from around 95% to 98% while incurring a significant cost:

Modern JavaScript on npm

Recently, Node.js has standardized an "exports" field to define entry points for a package:

{
  "exports": "./index.js"
}

Modules referenced by the "exports" field imply a Node version of at least 12.8, which supports ES2019. This means that any module referenced using the "exports" field can be written in modern JavaScript. Package consumers must assume modules with an "exports" field contain modern code and transpile if necessary.

Modern-only

If you want to publish a package with modern code and leave it up to the consumer to handle transpiling it when they use it as a dependency—use only the "exports" field.

{
  "name": "foo",
  "exports": "./modern.js"
}

{% Aside 'caution' %} This approach is not recommended. In a perfect world, every developer would have already configured their build system to transpile all dependencies (node_modules) to their required syntax. However, this is not currently the case, and publishing your package using only modern syntax would prevent its usage in applications that would be accessed through legacy browsers. {% endAside %}

Modern with legacy fallback

Use the "exports" field along with "main" in order to publish your package using modern code but also include an ES5 + CommonJS fallback for legacy browsers.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Modern with legacy fallback and ESM bundler optimizations

In addition to defining a fallback CommonJS entrypoint, the "module" field can be used to point to a similar legacy fallback bundle, but one that uses JavaScript module syntax (import and export).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Many bundlers, such as webpack and Rollup, rely on this field to take advantage of module features and enable tree shaking. This is still a legacy bundle that does not contain any modern code aside from import/export syntax, so use this approach to ship modern code with a legacy fallback that is still optimized for bundling.

Modern JavaScript in applications

Third-party dependencies make up the vast majority of typical production JavaScript code in web applications. While npm dependencies have historically been published as legacy ES5 syntax, this is no longer a safe assumption and risks dependency updates breaking browser support in your application.

With an increasing number of npm packages moving to modern JavaScript, it's important to ensure that the build tooling is set up to handle them. There's a good chance some of the npm packages you depend on are already using modern language features. There are a number of options available to use modern code from npm without breaking your application in older browsers, but the general idea is to have the build system transpile dependencies to the same syntax target as your source code.

webpack

As of webpack 5, it is now possible to configure what syntax webpack will use when generating code for bundles and modules. This doesn't transpile your code or dependencies, it only affects the "glue" code generated by webpack. To specify the browser support target, add a browserslist configuration to your project, or do it directly in your webpack configuration:

module.exports = {
  target: ['web', 'es2017'],
};

It is also possible to configure webpack to generate optimized bundles that omit unnecessary wrapper functions when targeting a modern ES Modules environment. This also configures webpack to load code-split bundles using <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

There are a number of webpack plugins available that make it possible to compile and ship modern JavaScript while still supporting legacy browsers, such as Optimize Plugin and BabelEsmPlugin.

Optimize Plugin

Optimize Plugin is a webpack plugin that transforms final bundled code from modern to legacy JavaScript instead of each individual source file. It's a self-contained setup that allows your webpack configuration to assume everything is modern JavaScript with no special branching for multiple outputs or syntaxes.

Since Optimize Plugin operates on bundles instead of individual modules, it processes your application's code and your dependencies equally. This makes it safe to use modern JavaScript dependencies from npm, because their code will be bundled and transpiled to the correct syntax. It can also be faster than traditional solutions involving two compilation steps, while still generating separate bundles for modern and legacy browsers. The two sets of bundles are designed to be loaded using the module/nomodule pattern.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin can be faster and more efficient than custom webpack configurations, which typically bundle modern and legacy code separately. It also handles running Babel for you, and minifies bundles using Terser with separate optimal settings for the modern and legacy outputs. Finally, polyfills needed by the generated legacy bundles are extracted into a dedicated script so they are never duplicated or unnecessarily loaded in newer browsers.

Comparison: transpiling source modules twice versus transpiling generated bundles.

BabelEsmPlugin

BabelEsmPlugin is a webpack plugin that works along with @babel/preset-env to generate modern versions of existing bundles to ship less transpiled code to modern browsers. It is the most popular off-the-shelf solution for module/nomodule, used by Next.js and Preact CLI.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin supports a wide array of webpack configurations, because it runs two largely separate builds of your application. Compiling twice can take a little bit of extra time for large applications, however this technique allows BabelEsmPlugin to integrate seamlessly into existing webpack configurations and makes it one of the most convenient options available.

Configure babel-loader to transpile node_modules

If you are using babel-loader without one of the previous two plugins, there's an important step required in order to consume modern JavaScript npm modules. Defining two separate babel-loader configurations makes it possible to automatically compile modern language features found in node_modules to ES2017, while still transpiling your own first-party code with the Babel plugins and presets defined in your project's configuration. This doesn't generate modern and legacy bundles for a module/nomodule setup, but it does make it possible to install and use npm packages that contain modern JavaScript without breaking older browsers.

webpack-plugin-modern-npm uses this technique to compile npm dependencies that have an "exports" field in their package.json, since these may contain modern syntax:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Alternatively, you can implement the technique manually in your webpack configuration by checking for an "exports" field in the package.json of modules as they are resolved. Omitting caching for brevity, a custom implementation might look like this:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

When using this approach, you'll need to ensure modern syntax is supported by your minifier. Both Terser and uglify-es have an option to specify {ecma: 2017} in order to preserve and in some cases generate ES2017 syntax during compression and formatting.

Rollup

Rollup has built-in support for generating multiple sets of bundles as part of a single build, and generates modern code by default. As a result, Rollup can be configured to generate modern and legacy bundles with the official plugins you're likely already using.

@rollup/plugin-babel

If you use Rollup, the getBabelOutputPlugin() method (provided by Rollup's official Babel plugin) transforms the code in generated bundles rather than individual source modules. Rollup has built-in support for generating multiple sets of bundles as part of a single build, each with their own plugins. You can use this to produce different bundles for modern and legacy by passing each through a different Babel output plugin configuration:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Additional build tools

Rollup and webpack are highly-configurable, which generally means each project must update its configuration enable modern JavaScript syntax in dependencies. There are also higher-level build tools that favor convention and defaults over configuration, like Parcel, Snowpack, Vite and WMR. Most of these tools assume npm dependencies may contain modern syntax, and will transpile them to the appropriate syntax level(s) when building for production.

In addition to dedicated plugins for webpack and Rollup, modern JavaScript bundles with legacy fallbacks can be added to any project using devolution. Devolution is a standalone tool that transforms the output from a build system to produce legacy JavaScript variants, allowing bundling and transformations to assume a modern output target.

Measure performance with the RAIL model

RAIL is a user-centric performance model that provides a structure for thinking about performance. The model breaks down the user's experience into key actions (for example, tap, scroll, load) and helps you define performance goals for each of them.

{% Aside 'important' %} Core Web Vitals is a newer initiative by Google to provide unified guidance for quality signals that are essential to delivering a great user experience on the web. It is the recommended approach for defining performance goals over RAIL, and has different thresholds than those detailed here. {% endAside %}

RAIL stands for four distinct aspects of web app life cycle: response, animation, idle, and load. Users have different performance expectations for each of these contexts, so performance goals are defined based on the context and UX research on how users perceive delays.

{% Img src="image/admin/uc1IWVOW2wEhIY6z4KjJ.png", alt="The 4 parts of the RAIL performance model: response, animation, idle, and load.", width="800", height="290" %}
The 4 parts of the RAIL performance model

Focus on the user

Make users the focal point of your performance effort. The table below describes key metrics of how users perceive performance delays:

0 to 16 ms Users are exceptionally good at tracking motion, and they dislike it when animations aren't smooth. They perceive animations as smooth so long as 60 new frames are rendered every second. That's 16 ms per frame, including the time it takes for the browser to paint the new frame to the screen, leaving an app about 10 ms to produce a frame.
0 to 100 ms Respond to user actions within this time window and users feel like the result is immediate. Any longer, and the connection between action and reaction is broken.
100 to 1000 ms Within this window, things feel part of a natural and continuous progression of tasks. For most users on the web, loading pages or changing views represents a task.
1000 ms or more Beyond 1000 milliseconds (1 second), users lose focus on the task they are performing.
10000 ms or more Beyond 10000 milliseconds (10 seconds), users are frustrated and are likely to abandon tasks. They may or may not come back later.

{% Aside %} Users perceive performance delays differently, depending on network conditions and hardware. For example, loading sites on a powerful desktop machine over a fast Wi-Fi connection commonly happens in under 1 s and users have grown accustomed to that. Loading sites on mobile devices with slow 3G connections takes more time, so mobile users are generally more patient and loading in 5 s on mobile is a more realistic goal. {% endAside %}

Goals and guidelines

In the context of RAIL, the terms goals and guidelines have specific meanings:

Response: process events in under 50ms

Goal: Complete a transition initiated by user input within 100 ms, so users feel like the interactions are instantaneous.

Guidelines:

50 ms or 100 ms?

The goal is to respond to input in under 100 ms, so why is our budget only 50 ms? This is because there is generally other work being done in addition to input handling, and that work takes up part of the time available for acceptable input response. If an application is performing work in the recommended 50 ms chunks during idle time, that means input can be queued for up to 50 ms if it occurs during one of those chunks of work. Accounting for this, it's safe to assume only the remaining 50 ms is available for actual input handling. This effect is visualized in the diagram below which shows how input received during an idle task is queued, reducing the available processing time:

{% Img src="image/admin/I7HDZ9qGxe0jAzz6PxNq.png", alt="Diagram showing how input received during an idle task is queued, reducing available input processing time to 50ms", width="800", height="400" %}
How idle tasks affect input response budget.

Animation: produce a frame in 10 ms

Goals:

Guidelines:

{% Aside %} Recognize all the types of animations. Animations aren't just fancy UI effects. Each of these interactions are considered animations:

Idle: maximize idle time

Goal: Maximize idle time to increase the odds that the page responds to user input within 50 ms.

Guidelines:

Load: deliver content and become interactive in under 5 seconds

When pages load slowly, user attention wanders, and users perceive the task as broken. Sites that load quickly have longer average sessions, lower bounce rates, and higher ad viewability.

Goals:

{% Aside %}

Be aware that these targets may change over time.

{% endAside %}

Guidelines:

{% Aside %}

Recognize the factors that affect page load performance:

{% endAside %}

Tools for measuring RAIL

There are a few tools to help you automate your RAIL measurements. Which one you use depends on what type of information you need, and what type of workflow you prefer.

Chrome DevTools

Chrome DevTools provides in-depth analysis on everything that happens while your page loads or runs. See Get Started With Analyzing Runtime Performance to get familiar with the Performance panel UI.

The following DevTools features are especially relevant:

Lighthouse

Lighthouse is available in Chrome DevTools, at PageSpeed Insights, as a Chrome Extension, as a Node.js module, and within WebPageTest. You give it a URL, it simulates a mid-range device with a slow 3G connection, runs a series of audits on the page, and then gives you a report on load performance, as well as suggestions on how to improve.

The following audits are especially relevant:

Response

Load

WebPageTest

WebPageTest is a web performance tool that uses real browsers to access web pages and collect timing metrics. Enter a URL at webpagetest.org/easy to get a detailed report on the page's load performance on a real Moto G4 device with a slow 3G connection. You can also configure it to include a Lighthouse audit.

Summary

RAIL is a lens for looking at a website's user experience as a journey composed of distinct interactions. Understand how users perceive your site in order to set performance goals with the greatest impact on user experience.

Reduce JavaScript payloads with code splitting

Nobody likes waiting. Over 50% of users abandon a website if it takes longer than 3 seconds to load.

Sending large JavaScript payloads impacts the speed of your site significantly. Instead of shipping all the JavaScript to your user as soon as the first page of your application is loaded, split your bundle into multiple pieces and only send what's necessary at the very beginning.

Why is code splitting beneficial?

Code splitting is a technique that seeks to minimize startup time. When we ship less JavaScript at startup, we can get applications to be interactive faster by minimizing main thread work during this critical period.

When it comes to Core Web Vitals, reducing JavaScript payloads downloaded at startup will contribute to better First Input Delay (FID) and Interaction to Next Paint (INP) times. The reasoning behind this is that, by freeing up the main thread, the application is able to respond to user inputs more quickly by reducing JavaScript parse, compile, and execution-related startup costs.

Depending on your website's architecture—particularly if your website relies heavily on client-side rendering—reducing the size of JavaScript payloads responsible for rendering markup may lead to improved Largest Contentful Paint (LCP) times. This can occur when the LCP resource is delayed in being discovered by the browser until after client-side markup is completed, or when the main thread is too busy to render that LCP element. Both scenarios can delay the LCP time for the page.

{% Aside 'important' %} If your website relies on client-side rendering exclusively, you should seek to use Server-Side Rendering (SSR) to ensure that the server is sending meaningful markup to the client in response to the navigation request. This can help the browser preload scanner to opportunistically fetch resources more effectively. {% endAside %}

Measure

Lighthouse displays a failed audit when a significant amount of time is taken to execute all the JavaScript on a page.

{% Img src="image/admin/p0Ahh3pzXog3jPdDp6La.png", alt="A failing Lighthouse audit showing scripts taking too long to execute.", width="797", height="100" %}

Split the JavaScript bundle to only send the code needed for the initial route when the user loads an application. This minimizes the amount of script that needs to be parsed and compiled, which results in faster page load times.

import moduleA from "library";

form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});

const someFunction = () => {
  // uses moduleA
}

In here, someFunction uses a module imported from a particular library. If this module is not being used elsewhere, the code block can be modified to use a dynamic import to fetch it only when the form is submitted by the user.

form.addEventListener("submit", e => {
  e.preventDefault();
  import('library.moduleA')
    .then(module => module.default) // using the default export
    .then(() => someFunction())
    .catch(handleError());
});

const someFunction = () => {
    // uses moduleA
}

The code that makes up the module does not get included into the initial bundle and is now lazy loaded, or provided to the user only when it is needed after the form submission. To further improve page performance, preload critical chunks to prioritize and fetch them sooner.

Although the previous code snippet is a simple example, lazy loading third party dependencies is not a common pattern in larger applications. Usually, third party dependencies are split into a separate vendor bundle that can be cached since they don't update as often. You can read more about how the SplitChunksPlugin can help you do this.

Splitting on the route or component level when using a client-side framework is a simpler approach to lazy loading different parts of your application. Many popular frameworks that use webpack provide abstractions to make lazy loading easier than diving into the configurations yourself.

Reduce JavaScript payloads with tree shaking

Today's web applications can get pretty big, especially the JavaScript part of them. As of mid-2018, HTTP Archive puts the median transfer size of JavaScript on mobile devices at approximately 350 KB. And this is just transfer size! JavaScript is often compressed when sent over the network, meaning that the actual amount of JavaScript is quite a bit more after the browser decompresses it. That's important to point out, because as far as resource processing is concerned, compression is irrelevant. 900 KB of decompressed JavaScript is still 900 KB to the parser and compiler, even though it may be roughly 300 KB when compressed.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/VytWruOnBN3nHJnSm18w.svg", alt="A diagram illustrating the process of downloading, decompressing, parsing, compiling, and executing JavaScript.", width="600", height="106" %}
The process of downloading and running JavaScript. Note that even though the transfer size of the script is 300 KB compressed, it is still 900 KB worth of JavaScript that must be parsed, compiled, and executed.

JavaScript is an expensive resource to process. Unlike images which only incur relatively trivial decode time once downloaded, JavaScript must be parsed, compiled, and then finally executed. Byte for byte, this makes JavaScript more expensive than other types of resources.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/rEdnN5fuzBC6DOjUqgEL.png", alt="A diagram comparing the processing time of 170 KB of JavaScript versus an equivalently sized JPEG image. The JavaScript resource is far more resource-intensive byte for byte than the JPEG.", width="800", height="504" %}
The processing cost of parsing/compiling 170 KB of JavaScript vs decode time of an equivalently sized JPEG. (source).

While improvements are continually being made to improve the efficiency of JavaScript engines, improving JavaScript performance is—as always—a task for developers.

To that end, there are techniques to improve JavaScript performance. Code splitting, is one such technique that improves performance by partitioning application JavaScript into chunks, and serving those chunks to only the routes of an application that need them.

While this technique works, it doesn't address a common problem of JavaScript-heavy applications, which is the inclusion of code that's never used. Tree shaking attempts to solve this problem.

What is tree shaking?

Tree shaking is a form of dead code elimination. The term was popularized by Rollup, but the concept of dead code elimination has existed for some time. The concept has also found purchase in webpack, which is demonstrated in this article by way of a sample app.

The term "tree shaking" comes from the mental model of your application and its dependencies as a tree-like structure. Each node in the tree represents a dependency that provides distinct functionality for your app. In modern apps, these dependencies are brought in via static import statements like so:

// Import all the array utilities!
import arrayUtils from "array-utils";

{% Aside %} Note: If you're not sure what ES6 modules are, this explainer at Pony Foo is worth a look. This guide assumes you have working knowledge of how ES6 modules work, so if you don't know anything about them, give that article a read! {% endAside %}

When an app is young—a sapling, if you will—it may have few dependencies. It's also using most—if not all—the dependencies you add. As your app matures, however, more dependencies can get added. To compound matters, older dependencies fall out of use, but may not get pruned from your codebase. The end result is that an app ends up shipping with a lot of unused JavaScript. Tree shaking addresses this by taking advantage of how static import statements pull in specific parts of ES6 modules:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

The difference between this import example and the previous one is that rather than importing everything from the "array-utils" module—which could be a lot of code)—this example imports only specific parts of it. In dev builds, this doesn't change anything, as the entire module gets imported regardless. In production builds, webpack can be configured to "shake" off exports from ES6 modules that weren't explicitly imported, making those production builds smaller. In this guide, you'll learn how to do just that!

Finding opportunities to shake a tree

For illustrative purposes, a sample one-page app is available that demonstrates how tree shaking works. You can clone it and follow along if you like, but we'll cover every step of the way together in this guide, so cloning isn't necessary (unless hands-on learning is your thing).

The sample app is a searchable database of guitar effect pedals. You enter a query and a list of effect pedals will appear.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/bBmp460zNYmTVeAgQhw5.png", alt="A screenshot of a sample one page application for searching a database of guitar effect pedals.", width="800", height="673" %}
A screenshot of the sample app.

The behavior that drives this app is separated into vendor (i.e., Preact and Emotion) and app-specific code bundles (or "chunks", as webpack calls them):

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/oreVh5rQcXxIDenvQxE7.png", alt="A screenshot of two application code bundles (or chunks) shown in the network panel of Chrome's DevTools.", width="800", height="282" %}
The app's two JavaScript bundles. These are uncompressed sizes.

The JavaScript bundles shown in the figure above are production builds, meaning they're optimized through uglification. 21.1 KB for an app-specific bundle isn't bad, but it should be noted that no tree shaking is occurring whatsoever. Let's look at the app code and see what can be done to fix that.

{% Aside %} If you don't care for a long-winded explanation, and want to dive into code, go ahead and check out the tree-shake branch in the app's GitHub repo. You can also diff this branch to see exactly what was changed to make tree shaking work! {% endAside %}

In any application, finding tree shaking opportunities are going to involve looking for static import statements. Near the top of the main component file, you'll see a line like this:

import * as utils from "../../utils/utils";

You can import ES6 modules in a variety of ways, but ones like this should get your attention. This specific line says "import everything from the utils module, and put it in a namespace called utils." The big question to ask here is, "just how much stuff is in that module?"

If you look at the utils module source code, you'll find there's about 1,300 lines of code.

Do you need all that stuff? Let's double check by searching the main component file that imports the utils module to see how many instances of that namespace come up.

{% Img src="image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/Grk6jcj6IiMHIiJPqiEM.png", alt="A screenshot of a search in a text editor for 'utils.', returning only 3 results.", width="800", height="117" %}
The utils namespace we've imported tons of modules from is only invoked three times within the main component file.

As it turns out, the utils namespace appears in only three spots in our application—but for what functions? If you take a look at the main component file again, it appears to be only one function, which is utils.simpleSort, which is used to sort the search results list by a number of criteria when the sorting dropdowns are changed:

if (this.state.sortBy === "model") {
  // `simpleSort` gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

Out of a 1,300 line file with a bunch of exports, only one of them is used. This results in shipping a lot of unused JavaScript.

{% Aside %} Note: This project is purposefully kept simple, so it's a bit easier in this case to find out where the bloat is coming from. In large projects with many modules, however, things get more complicated. Tools such as Webpack Bundle Analyzer and source-map-explorer may provide further insight. {% endAside %}

While this example app is admittedly a bit contrived, it doesn't change the fact that this synthetic sort of scenario resembles actual optimization opportunities you may encounter in a production web app. Now that you've identified an opportunity for tree shaking to be useful, how is it actually done?

Keeping Babel from transpiling ES6 modules to CommonJS modules

Babel is an indispensable tool, but it may make the effects of tree shaking a bit more difficult to observe. If you're using @babel/preset-env, Babel may transform ES6 modules into more widely compatible CommonJS modules—that is, modules you require instead of import.

Because tree shaking is more difficult to do for CommonJS modules, webpack won't know what to prune from bundles if you decide to use them. The solution is to configure @babel/preset-env to explicitly leave ES6 modules alone. Wherever you configure Babel—be it in babel.config.js or package.json—this involves adding a little something extra:

// babel.config.js
export default {
  presets: [
    [
      "@babel/preset-env", {
        modules: false
      }
    ]
  ]
}

Specifying modules: false in your @babel/preset-env config gets Babel to behave as desired, which allows webpack to analyze your dependency tree and shake off unused dependencies.

Keeping side effects in mind

Another aspect to consider when shaking dependencies from your app is whether your project's modules have side effects. An example of a side effect is when a function modifies something outside of its own scope, which is a side effect of its execution:

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]

In this example, addFruit produces a side effect when it modifies the fruits array, which is outside its scope.

Side effects also apply to ES6 modules, and that matters in the context of tree shaking. Modules that take predictable inputs and produce equally predictable outputs without modifying anything outside of their own scope are dependencies that can be safely dropped if we're not using them. They're self-contained, modular pieces of code. Hence, "modules".

Where webpack is concerned, a hint can be used to specify that a package and its dependencies are free of side effects by specifying "sideEffects": false in a project's package.json file:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

Alternatively, you can tell webpack which specific files are not side effect-free:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}

In the latter example, any file that isn't specified will be assumed to be free of side effects. If you don't want to add this to your package.json file, you can also specify this flag in your webpack config via module.rules.

Importing only what's needed

After instructing Babel to leave ES6 modules alone, a slight adjustment to our import syntax is required to bring in only the functions needed from the utils module. In this guide's example, all that's needed is the simpleSort function:

import { simpleSort } from "../../utils/utils";

Because only simpleSort is being imported instead of the entire utils module, every instance of utils.simpleSort will need to changed to simpleSort:

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

This should be all that's needed for tree shaking to work in this example. This is the webpack output before shaking the dependency tree:

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

This is the output after tree shaking is successful:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

While both bundles shrank, it's really the main bundle that benefits most. By shaking off the unused parts of the utils module, the main bundle shrinks by about 60%. This not only lowers the amount of time the script takes to the download, but processing time as well.

Go shake some trees!

Whatever mileage you get out of tree shaking depends on your app and its dependencies and architecture. Try it! If you know for a fact you haven't set up your module bundler to perform this optimization, there's no harm trying and seeing how it benefits your application.

You may realize a significant performance gain from tree shaking, or not much at all. But by configuring your build system to take advantage of this optimization in production builds and selectively importing only what your application needs, you'll be proactively keeping your application bundles as small as possible.

Special thanks to Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone, and Philip Walton for their valuable feedback, which significantly improved the quality of this article.

Minify and compress network payloads

There are two useful techniques that can be used to improve the performance of your web page:

Incorporating both of these techniques reduces payload sizes and in turn improves page load times.

Measure

Lighthouse displays a failed audit if it detects any CSS or JS resources on your page that can be minified.

{% Img src="image/admin/ZT9ESeCStegt0SklYbni.png", alt="Lighthouse Minify CSS Audit", width="800", height="90" %}

{% Img src="image/admin/vDaAnUSvQxmGcoasQj1k.png", alt="Lighthouse Minify JS Audit", width="800", height="112" %}

It also audits for any uncompressed assets.

{% Img src="image/admin/xfqzdLuu3w3lanxo5Ggc.png", alt="Lighthouse: Enable text compression", width="800", height="123" %}

Minification

Minification is the process of removing whitespace and any code that is not necessary to create a smaller but perfectly valid code file. Terser is a popular JavaScript compression tool and webpack v4 includes a plugin for this library by default to create minified build files.

Data compression

Compression is the process of modifying data using a compression algorithm. Gzip is the most widely used compression format for server and client interactions. Brotli is a newer compression algorithm which can provide even better compression results than Gzip.

{% Aside %} Compressing files can significantly improve the performance of a webpage, but you rarely need to do this yourself. Many hosting platforms, CDNs and reverse proxy servers either encode assets with compression by default or allow you to easily configure them. Read the documentation for the tool that you are using to see if compression is already supported before attempting to roll out your own solution. {% endAside %}

There are two different ways to compress files sent to a browser:

Both approaches have their own advantages and disadvantages which is covered in the next section. Use whichever works best for your application.

Dynamic compression

This process involves compressing assets on-the-fly as they get requested by the browser. This can be simpler than compressing files manually or with a build process, but can cause delays if high compression levels are used.

const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
	console.log('Your app is listening on port ' + listener.address().port);
});

This compresses your assets using gzip. If your web server supports it, consider using a separate module like shrink-ray to compress via Brotli to achieve better compression ratios.

{% Aside 'codelab' %} Use express.js to compress assets with gzip and Brotli. {% endAside %}

Static compression

Static compression involves compressing and saving assets ahead of time. This can make the build process take longer, especially if high compression levels are used, but ensures that no delays happen when the browser fetches the compressed resource.

If your web server supports Brotli, use a plugin like BrotliWebpackPlugin with webpack to compress your assets as part of your build step. Otherwise, use CompressionPlugin to compress your assets with gzip. It can be included just like any other plugin in the webpack configurations file:

module.exports = {
	//...
	plugins: [
		//...
		new CompressionPlugin()
	]
}

Once compressed files are part of the build folder, create a route in your server to handle all JS endpoints to serve the compressed files. Here is an example of how this can be done with Node and Express for gzipped assets.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
	req.url = req.url + '.gz';
	res.set('Content-Encoding', 'gzip');
	next();
});

app.use(express.static('public'));

Reduce web font size

Typography is fundamental to good design, branding, readability, and accessibility. Web fonts enable all of the above and more: the text is selectable, searchable, zoomable, and high-DPI friendly, providing consistent and sharp text rendering regardless of the screen size and resolution. WebFonts are critical to good design, UX, and performance.

Web font optimization is a critical piece of the overall performance strategy. Each font is an additional resource, and some fonts may block rendering of the text, but just because the page is using WebFonts doesn't mean that it has to render slower. On the contrary, optimized fonts, combined with a judicious strategy for how they are loaded and applied on the page, can help reduce the total page size and improve page rendering times.

Anatomy of a web font

A web font is a collection of glyphs, and each glyph is a vector shape that describes a letter or symbol. As a result, two simple variables determine the size of a particular font file: the complexity of the vector paths of each glyph and the number of glyphs in a particular font. For example, Open Sans, which is one of the most popular WebFonts, contains 897 glyphs, which include Latin, Greek, and Cyrillic characters.

{% Img src="image/admin/B92rhiBJD9sx88a5CvVy.png", alt="Font glyph table", width="800", height="309" %}

When picking a font, it's important to consider which character sets are supported. If you need to localize your page content to multiple languages, you should use a font that can deliver a consistent look and experience to your users. For example, Google's Noto font family aims to support all the world's languages. Note, however, that the total size of Noto, with all languages included, results in a 1.1GB+ ZIP download.

In this post you will find out how to reduce the delivered filesize of your web fonts.

Web font formats

Today there are two recommeded font container formats in use on the web:

WOFF and WOFF 2.0 enjoy wide support and are supported by all modern browsers.

Web fonts and compression

Both WOFF and WOFF 2.0 have built-in compression. WOFF 2.0's internal compression uses Brotli, and offers up to 30% better compression than WOFF. For more information, see the WOFF 2.0 evaluation report.

Finally, it's worth noting that some font formats contain additional metadata, such as font hinting and kerning information that may not be necessary on some platforms, which allows for further file-size optimization. For example, Google Fonts maintains 30+ optimized variants for each font and automatically detects and delivers the optimal variant for each platform and browser.

Define a font family with @font-face

The @font-face CSS at-rule allows you to define the location of a particular font resource, its style characteristics, and the Unicode codepoints for which it should be used. A combination of such @font-face declarations can be used to construct a "font family," which the browser will use to evaluate which font resources need to be downloaded and applied to the current page.

Consider a variable font

Variable fonts can significantly reduce the filesize of your fonts in cases where you need multiple variants of a font. Instead of needing to load the regular and bold styles plus their italic versions, you can load a single file that contains all of the information. However, variable font file sizes will be larger than an individual font variant—though smaller than the combination of many variants. Rather than one large variable font, it may be better to serve critical font variants first, with other variants downloaded later.

Variable fonts are now supported by all modern browsers, find out more in the Introduction to variable fonts on the web.

Select the right format

Each @font-face declaration provides the name of the font family, which acts as a logical group of multiple declarations, font properties such as style, weight, and stretch, and the src descriptor, which specifies a prioritized list of locations for the font resource.

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome.woff2') format('woff2'),
       /* Only serve WOFF if necessary. Otherwise,
          WOFF 2.0 is fine by itself. */
       url('/fonts/awesome.woff') format('woff');
}

@font-face {
  font-family: 'Awesome Font';
  font-style: italic;
  font-weight: 400;
  src: local('Awesome Font Italic'),
       url('/fonts/awesome-i.woff2') format('woff2'),
       url('/fonts/awesome-i.woff') format('woff');
}

First, note that the above examples define a single Awesome Font family with two styles (normal and italic), each of which points to a different set of font resources. In turn, each src descriptor contains a prioritized, comma-separated list of resource variants:

{% Aside %} Unless you're referencing one of the default system fonts, it is rare for the user to have it locally installed, especially on mobile devices, where it is effectively impossible to "install" additional fonts. Even if you start with a local() entry, you should always provide a list of url() entries for those that do not have it downloaded. {% endAside %}

When the browser determines that the font is needed, it iterates through the provided resource list in the specified order and tries to load the appropriate resource. For example, following the example above:

  1. The browser performs page layout and determines which font variants are required to render the specified text on the page. Fonts that aren't part of the page's CSS Object Model (CSSOM) are not downloaded by the browser, as they are not required.
  2. For each required font, the browser checks if the font is available locally.
  3. If the font is not available locally, the browser iterates over external definitions:

The combination of local and external directives with appropriate format hints allows you to specify all of the available font formats and let the browser handle the rest. The browser determines which resources are required and selects the optimal format.

{% Aside 'important' %} If you're shipping more formats than just WOFF 2.0, the order in which the font variants are specified matters. The browser picks the first format it supports. Therefore, if you want the newer browsers to use WOFF 2.0, then you should place the WOFF 2.0 declaration above WOFF. {% endAside %}

Unicode-range subsetting

In addition to font properties such as style, weight, and stretch, the @font-face rule allows you to define a set of Unicode codepoints supported by each resource. This enables you to split a large Unicode font into smaller subsets (for example, Latin, Cyrillic, and Greek subsets) and only download the glyphs required to render the text on a particular page.

The unicode-range descriptor allows you to specify a comma-delimited list of range values, each of which can be in one of three different forms:

For example, you can split your Awesome Font family into Latin and Japanese subsets, each of which the browser downloads on an as-needed basis:

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2');
  /* Latin glyphs */
  unicode-range: U+000-5FF;
}

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome-jp.woff2') format('woff2');
  /* Japanese glyphs */
  unicode-range: U+3000-9FFF, U+ff??;
}

{% Aside 'important' %} Unicode-range subsetting is particularly important for Asian languages, where the number of glyphs is much larger than in Western languages and a typical "full" font is often measured in megabytes instead of tens of kilobytes. {% endAside %}

The use of Unicode range subsets and separate files for each stylistic variant of the font allows you to define a composite font family that is both faster and more efficient to download. Visitors only download the variants and subsets they need, and they aren't forced to download subsets that they may never see or use on the page.

Nearly all browsers support unicode-range. For compatibility with older browsers you may need to fall back to "manual subsetting". In this case you have to fall back to providing a single font resource that contains all the necessary subsets and hide the rest from the browser. For example, if the page is only using Latin characters, then you can strip other glyphs and serve that particular subset as a standalone resource.

  1. Determine which subsets are needed:
    • If the browser supports unicode-range subsetting, then it will automatically select the right subset. The page just needs to provide the subset files and specify appropriate unicode-ranges in the @font-face rules.
    • If the browser doesn't support unicode-range subsetting, then the page needs to hide all unnecessary subsets; that is, the developer must specify the required subsets.
  2. Generate font subsets:
    • Use the open-source pyftsubset tool to subset and optimize your fonts.
    • Some font servers—such as Google Font—will automatically subset by default.
    • Some font services allow manual subsetting via custom query parameters, which you can use to manually specify the required subset for your page. Consult the documentation from your font provider.

Font selection and synthesis

Each font family may be composed of multiple stylistic variants (regular, bold, italic) and multiple weights for each style. Each of which, in turn, may contain very different glyph shapes—for example, different spacing, sizing, or a different shape altogether.

{% Img src="image/admin/FNtAc2xRmx2MuUt2MADj.png", alt="Font weights", width="697", height="127" %}

The above diagram illustrates a font family that offers three different bold weights:

All other in-between variants (indicated in gray) are automatically mapped to the closest variant by the browser.

When a weight is specified for which no face exists, a face with a nearby weight is used. In general, bold weights map to faces with heavier weights and light weights map to faces with lighter weights.

CSS font matching algorithm

Similar logic applies to italic variants. The font designer controls which variants they will produce, and you control which variants you'll use on the page. Because each variant is a separate download, it's a good idea to keep the number of variants small. For example, you can define two bold variants for the Awesome Font family:

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2');
  /* Latin glyphs */
  unicode-range: U+000-5FF;
}

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 700;
  src: local('Awesome Font'),
       url('/fonts/awesome-l-700.woff2') format('woff2');
  /* Latin glyphs */
  unicode-range: U+000-5FF;
}

The above example declares the Awesome Font family that is composed of two resources that cover the same set of Latin glyphs (U+000-5FF) but offer two different "weights": normal (400) and bold (700). However, what happens if one of your CSS rules specifies a different font weight, or sets the font-style property to italic?

{% Img src="image/admin/a8Jo2cIO1tPsj71AzftS.png", alt="Font synthesis", width="800", height="356" %}

{% Aside 'warning' %} Be aware that synthesized approaches may not be suitable for scripts like Cyrillic, where italic forms are very different in shape. For proper fidelity in those scripts, use an actual italic font. {% endAside %}

The example above illustrates the difference between the actual vs. synthesized font results for Open Sans. All synthesized variants are generated from a single 400-weight font. As you can see, there's a noticeable difference in the results. The details of how to generate the bold and oblique variants are not specified. Therefore, the results vary from browser to browser, and are highly dependent on the font.

{% Aside %} For best consistency and visual results, don't rely on font synthesis. Instead, minimize the number of used font variants and specify their locations, such that the browser can download them when they are used on the page. Or, choose to use a variable font. That said, in some cases a synthesized variant may be a viable option, but be cautious in using synthesized variants. {% endAside %}

Web font size optimization checklist

Effects on Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS)

Depending on your page's content, text nodes can be considered candidates for Largest Contentful Paint (LCP). It's therefore vital to ensure your web fonts are as small as possible by following the advice in this article so that your users will see the text on your page as soon as they possibly can.

If you're concerned that, despite your optimization efforts, page text might take too long to appear because of large web font resource, the font-display property has a number of settings that can help you to avoid invisible text while a font is downloading. However, using the swap value may cause significant layout shifts that affect your site's Cumulative Layout Shift (CLS). Consider using the optional or fallback values if possible.

If your web fonts are crucial to your branding—and by extension, the user experience— consider preloading your fonts so that the browser has a head start on requesting them. This can reduce both the swap period if you use font-display: swap, or the blocking period if you're not using font-display.

Remove unused code

Registries like npm have transformed the JavaScript world for the better by allowing anyone to easily download and use over half a million public packages. But we often include libraries we're not fully utilizing. To fix this issue, analyze your bundle to detect unused code. Then remove unused and unnecessary libraries.

Impact on Core Web Vitals

By removing unused code, you can improve your website's Core Web Vitals. Largest Contentful Paint, for example, can be affected by unused code by increased bandwidth contention caused by larger than necessary assets. LCP can also be affected if large JavaScript assets that render markup solely on the client contain references to LCP candidates by delaying when these resources can load.

Other metrics such as First Input Delay (FID) and Interaction to Next Paint (INP) can also be affected by unused code, because even unused JavaScript must be downloaded, parsed, compiled, and then executed. The unused code can introduce unnecessary delays in resource load time, memory usage, and main thread activity that contribute to poor page responsiveness.

This guide will help you get a handle on your project's unused code by showing you how to analyze your project's codebase, and offer strategies for pruning unused code from the JavaScript assets you ship to your users in production.

Analyze your bundle

DevTools makes it easy to see the size of all network requests: {% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %} {% Instruction 'reload-page', 'ol' %}

{% Img src="image/admin/aq6QZj5p4KTuaWnUJnLC.png", alt="Network panel with bundle request", width="800", height="169" %}

The Coverage tab in DevTools will also tell you how much CSS and JS code in your application is unused.

{% Img src="image/admin/xlPdOMaeykJhYqGcaMJr.png", alt="Code Coverage in DevTools", width="800", height="562" %}

By specifying a full Lighthouse configuration through its Node CLI, an "Unused JavaScript" audit can also be used to trace how much unused code is being shipped with your application.

{% Img src="image/admin/tdC0d65gEIiHZy6eyo82.png", alt="Lighthouse Unused JS Audit", width="800", height="347" %}

If you happen to be using webpack as your bundler, Webpack Bundle Analyzer will help you investigate what makes up the bundle. Include the plugin in your webpack configurations file like any other plugin:

module.exports = {
  //...
  plugins: [
    //...
    new BundleAnalyzerPlugin()
  ]
}

Although webpack is commonly used to build single-page applications, other bundlers, such as Parcel and Rollup, also have visualization tools that you can use to analyze your bundle.

Reloading the application with this plugin included shows a zoomable treemap of your entire bundle.

{% Img src="image/admin/pLAHEtl5C011wTk2IJij.png", alt="Webpack Bundle Analyzer", width="800", height="468" %}

Using this visualization allows you to inspect which parts of your bundle are larger than others, as well as get a better idea of all the libraries that you're importing. This can help identify if you are using any unused or unnecessary libraries.

Remove unused libraries

In the previous treemap image, there are quite a few packages within a single @firebase domain. If your website only needs the firebase database component, update the imports to fetch that library:

import firebase from 'firebase';
import firebase from 'firebase/app';
import 'firebase/database';

It is important to emphasize that this process is significantly more complex for larger applications.

For the mysterious looking package that you're quite sure is not being used anywhere, take a step back and see which of your top-level dependencies are using it. Try to find a way to only import the components that you need from it. If you aren't using a library, remove it. If the library isn't required for the initial page load, consider if it can be lazy loaded.

And in case you're using webpack, check out the list of plugins that automatically remove unused code from popular libraries.

{% Aside 'codelab' %} Remove unused code. {% endAside %}

Remove unnecessary libraries

Not all libraries can be easily broken down into parts and selectively imported. In these scenarios, consider if the library could be removed entirely. Building a custom solution or leveraging a lighter alternative should always be options worth considering. However, it is important to weigh the complexity and effort required for either of these efforts before removing a library entirely from an application.

Replace animated GIFs with video for faster page loads

Have you ever seen an animated GIF on a service like Imgur or Gfycat, inspected it in your dev tools, only to find out that GIF was really a video? There's a good reason for that. Animated GIFs can be downright huge.

{% Img src="image/admin/3UZ0b9dDotVIXWQT5Auk.png", alt="DevTools network panel showing a 13.7 MB gif.", width="800", height="155" %}

Thankfully, this is one of those areas of loading performance where you can do relatively little work to realize huge gains! By converting large GIFs to videos, you can save big on users' bandwidth.

Measure first

Use Lighthouse to check your site for GIFs that can be converted to videos. In DevTools, click on the Audits tab and check the Performance checkbox. Then run Lighthouse and check the report. If you have any GIFs that can be converted, you should see a suggestion to "Use video formats for animated content":

{% Img src="image/admin/KOSr9IivnkyaFk6RJ5u1.png", alt="A failing Lighthouse audit, use video formats for animated content.", width="800", height="173" %}

Create MPEG videos

There are a number of ways to convert GIFs to video, FFmpeg is the tool used in this guide. To use FFmpeg to convert the GIF, my-animation.gif to an MP4 video, run the following command in your console:

ffmpeg -i my-animation.gif -b:v 0 -crf 25 -f mp4 -vcodec libx264 -pix_fmt yuv420p my-animation.mp4

This tells FFmpeg to take my-animation.gif as the input, signified by the -i flag, and to convert it to a video called my-animation.mp4.

The libx264 encoder only works with files that have even dimensions, like 320px by 240px. If the input GIF has odd dimensions you can include a crop filter to avoid FFmpeg throwing a 'height/width not divisible by 2' error:

ffmpeg -i my-animation.gif -vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" -b:v 0 -crf 25 -f mp4 -vcodec libx264 -pix_fmt yuv420p my-animation.mp4

Create WebM videos

While MP4 has been around since 1999, WebM is a relatively new file format initially released in 2010. WebM videos are much smaller than MP4 videos, but not all browsers support WebM so it makes sense to generate both.

To use FFmpeg to convert my-animation.gif to a WebM video, run the following command in your console:

ffmpeg -i my-animation.gif -c vp9 -b:v 0 -crf 41 my-animation.webm

Compare the difference

The cost savings between a GIF and a video can be pretty significant.

{% Img src="image/admin/LWzvOWaOdMnNLTPWjayt.png", alt="File size comparison showing 3.7 MB for the gif, 551 KB for the mp4 and 341 KB for the webm.", width="800", height="188" %}

In this example, the initial GIF is 3.7 MB, compared to the MP4 version, which is 551 KB, and the WebM version, which is only 341 KB!

Replace the GIF img with a video

Animated GIFs have three key traits that a video needs to replicate:

Luckily, you can recreate these behaviors using the <video> element.

<video autoplay loop muted playsinline></video>

A <video> element with these attributes plays automatically, loops endlessly, plays no audio, and plays inline (that is, not full screen), all the hallmark behaviors expected of animated GIFs! 🎉

Finally, the <video> element requires one or more <source> child elements pointing to different video files that the browser can choose from, depending on the browser's format support. Provide both WebM and MP4, so that if a browser doesn't support WebM, it can fall back to MP4.

<video autoplay loop muted playsinline>
  <source src="my-animation.webm" type="video/webm">
  <source src="my-animation.mp4" type="video/mp4">
</video>

{% Aside 'codelab' %} Replace an animated GIF with a video. {% endAside %}

{% Aside %} Browsers don't speculate about which <source> is optimal, so the order of <source>'s matters. For example, if you specify an MP4 video first and the browser supports WebM, browsers will skip the WebM <source> and use the MPEG-4 instead. If you prefer a WebM <source> be used first, specify it first! {% endAside %}

Effect on Largest Contentful Paint (LCP)

It should be noted that while <img> elements are candidates for LCP, <video> elements without a poster image are not LCP candidates. The solution in the case of emulating animated GIFs is not to add poster attribute to your <video> elements, because that image will go unused.

What does this mean for your website? The recommendation is to stick with using a <video> instead of an animated GIF, but with the understanding that such media will not be a candidate for LCP, and the next largest candidate will be used instead. As GIFs and <video>s are typically larger and so slower to download, moving to a different LCP candidate will likely even improve the site's LCP.

Use WebP images

WebP images are smaller than their JPEG and PNG counterparts—usually on the magnitude of a 25–35% reduction in filesize. This decreases page sizes and improves performance.

WebP is an excellent replacement for JPEG, PNG, and GIF images. In addition, WebP offers both lossless and lossy compression. In lossless compression no data is lost. Lossy compression reduces file size, but at the expense of possibly reducing image quality.

Convert images to WebP

People generally use one of the following approaches for converting their images to WebP: the cwebp command-line tool or the Imagemin WebP plugin (npm package). The Imagemin WebP plugin is generally the best choice if your project uses build scripts or build tools (e.g. Webpack or Gulp), whereas the CLI is a good choice for simple projects or if you'll only need to convert images once.

When you convert images to WebP, you have the option to set a wide variety of compression settings—but for most people the only thing you'll ever need to care about is the quality setting. You can specify a quality level from 0 (worst) to 100 (best). It's worth playing around with this, find which level is the right tradeoff between image quality and filesize for your needs.

Use cwebp

Convert a single file, using cwebp's default compression settings:

cwebp images/flower.jpg -o images/flower.webp

Convert a single file, using a quality level of 50:

cwebp -q 50 images/flower.jpg -o images/flower.webp

Convert all files in a directory:

for file in images/*; do cwebp "$file" -o "${file%.*}.webp"; done

Use Imagemin

The Imagemin WebP plugin can be used by itself or with your favorite build tool (Webpack/Gulp/Grunt/etc.). This usually involves adding ~10 lines of code to a build script or the configuration file for your build tool. Here are examples of how to do that for Webpack, Gulp, and Grunt.

If you are not using one of those build tools, you can use Imagemin by itself as a Node script. This script will convert the files in the images directory and save them in the compressed_images directory.

const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');

imagemin(['images/*'], {
  destination: 'compressed_images',
  plugins: [imageminWebp({quality: 50})]
}).then(() => {
  console.log('Done!');
});

Serve WebP images

If your site only supports WebP compatible browsers, you can stop reading. Otherwise, serve WebP to newer browsers and a fallback image to older browsers:

Before:

<img src="flower.jpg" alt="">

After:

<picture>
  <source type="image/webp" srcset="flower.webp">
  <source type="image/jpeg" srcset="flower.jpg">
  <img src="flower.jpg" alt="">
</picture>

The <picture>, <source>, and <img> tags, including how they are ordered relative to each other, all interact to achieve this end result.

<picture>

The <picture> tag provides a wrapper for zero or more <source> tags and one <img> tag.

<source>

The <source> tag specifies a media resource.

The browser uses the first listed source that's in a format it supports. If the browser does not support any of the formats listed in the <source> tags, it falls back to loading the image specified by the <img> tag.

{% Aside 'gotchas' %}

{% endAside %}

<img>

The <img> tag is what makes this code work on browsers that don't support the <picture> tag. If a browser does not support the <picture> tag, it will ignore the tags it doesn't support. Thus, it only "sees" the <img src="flower.jpg" alt=""> tag and loads that image.

{% Aside 'gotchas' %}

{% endAside %}

Reading the HTTP Accept header

If you have an application back end or web server that allows you to rewrite requests, you can read the value of the HTTP Accept header, which will advertise what alternative image formats are supported:

Accept: image/webp,image/svg+xml,image/*,*/*;q=0.8

Reading this request header and rewriting the response based on its contents has the benefit of simplifying your image markup. <picture> markup can get rather long with many sources. Below is an Apache mod_rewrite rule that can serve WebP alternates:

RewriteEngine On
RewriteCond %{HTTP:Accept} image/webp [NC]
RewriteCond %{HTTP:Content-Disposition} !attachment [NC]
RewriteCond %{DOCUMENT_ROOT}/$1.webp -f [NC]
RewriteRule (.+)\.(png|jpe?g)$ $1.webp [T=image/webp,L]

If you go this route, you'll need to set the HTTP Vary response header to ensure caches will understand that the image may be served with varying content types:

<FilesMatch ".(jpe?g|png)$">
  <IfModule mod_headers.c>
    Header set Vary "Content-Type"
  </IfModule>
</FilesMatch>

The rewrite rule above will look for a WebP version of any requested JPEG or PNG image. If a WebP alternate is found, it will be served with the proper Content-Type header. This will allow you to use image markup similar to the following with automatic WebP support:

<img src="flower-320w.jpg" srcset="flower-320w.jpg 320w, flower-640w.jpg 640w, flower-960w.jpg 960w">

Verify WebP usage

Lighthouse can be used to verify that all images on your site are being served using WebP. Run the Lighthouse Performance Audit (Lighthouse > Options > Performance) and look for the results of the Serve images in next-gen formats audit. Lighthouse will list any images that are not being served in WebP.

Serve images with correct dimensions

We've all been there: you forgot to scale down an image before adding it to the page. The image looks fine, but it is wasting users' data and hurting page performance.

Identify incorrectly sized images

Lighthouse makes it easy to identify incorrectly-sized images. Run the Performance Audit (Lighthouse > Options > Performance) and look for the results of the Properly size images audit. The audit lists any images that need to be resized.

Determine the correct image size

Image sizing can be deceptively complicated. For this reason, we've provided two approaches: the "good" and the "better." Both will improve performance, but the "better" approach may take a bit longer to understand and implement. However, it will also reward you with bigger performance improvements. The best choice for you is the one that you feel comfortable implementing.

A quick note on CSS units

There are two types of CSS units for specifying the size of HTML elements, including images:

The "Good" Approach

For images with sizing based on…

You may find it helpful to check your analytics data (e.g. Google Analytics) to see which display sizes are commonly used by your users. Alternatively, screensiz.es provides information about the displays of many common devices.

The DevTools Elements panel can be used to determine what size an image is displayed at.

{% Img src="image/admin/pKQa0Huu0KGInOekdz6M.png", alt="DevTools element's panel", width="800", height="364" %}

The "Better" approach

For images with both absolute and relative sizing, use srcset and sizes attributes to serve different images to different display densities. Read the guide on responsive images.

"Display density" refers to the fact that different displays have different densities of pixels. All other things being equal, a high pixel density display will look sharper than a low pixel density display.

As a result, multiple image versions are necessary if you want users to experience the crispest possible images, regardless of the pixel density of their device.

{% Aside %} Some sites find that this difference in image quality matters, some find that it does not. {% endAside %}

Responsive image techniques make this possible by allowing you to list multiple image versions and for the device to choose the image that works best for it.

An image that works across all devices will be unnecessarily large for smaller devices. Responsive image techniques, specifically srcset and sizes, allow you to specify multiple image versions and for the device to choose the size that works best for it.

Resize images

Regardless of the approach that you choose, you may find it helpful to use ImageMagick to resize your images. ImageMagick is the most popular command line tool for creating and editing images. Most people can resize images far more quickly when using the CLI than a GUI-based image editor.

Resize image to 25% the size of the original:

convert flower.jpg -resize 25% flower_small.jpg

Scale image to fit within "200px wide by 100px tall":

# macOS/Linux
convert flower.jpg -resize 200x100 flower_small.jpg

# Windows
magick convert flower.jpg -resize 200x100 flower_small.jpg

If you'll be resizing many images, you may find it more convenient to use a script or service to automate the process. You can learn more about this in the Responsive Images guide.

Avoid layout shifts by specifying dimensions

While this guide discusses image dimensions in the context of reducing the amount of unnecessary bytes downloaded, it's important to note that reserving the correct space for images in the layout is another crucial component of minimizing a page's Cumulative Layout Shift metric. When serving images in HTML, be sure to use proper width and height attributes so that the browser knows how much space to allocate in the layout for the image:

<img src="flower.jpg" width="640" height="480" alt="A picture of a siberian iris.">

Without these attributes, or the equivalent CSS sizing, the browser has no idea how much space the image will take up until it has loaded. This will cause layout shifts in the document, which can be frustrating to users when content moves after they have started consuming it. This can result in users losing their place when reading, or to "miss" their intended hit target and end up clicking on something else they didn't intend to during page load.

An alternative to providing the width and height explicitly, is to use the CSS aspect-ratio property on the image. This has a similar effect on an element's size that width and height attributes do in the sense that the container will maintain a consistent aspect ratio. However, the difference is that this may result in a different aspect-ratio being used than the image is provided in, so you will likely want to use an object-fit setting to ensure the image is not distorted in this explicit 16/9 view:

img {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

Verify

Once you've resized all your images, re-run Lighthouse to verify that you didn't miss anything.

Serve modern code to modern browsers for faster page loads

Building websites that work well on all major browsers is a core tenet of an open web ecosystem. However, this means additional work of ensuring that all of the code you write is supported in each browser that you plan to target. If you want to use new JavaScript language features, you need to transpile these features to backwards-compatible formats for browsers that do not yet support them.

Babel is the most widely used tool to compile code that contains newer syntax into code that different browsers and environments (such as Node) can understand. This guide assumes you are using Babel, so you need to follow the setup instructions to include it into your application if you haven't already. Select webpack in Build Systems if you are using webpack as the module bundler in your app.

{% Aside %} Fun fact: Lebab is a separate library that does the opposite of what Babel does. It converts older code into newer syntax. {% endAside %}

To use Babel to only transpile what is needed for your users, you need to:

  1. Identify which browsers you want to target.
  2. Use @babel/preset-env with appropriate browser targets.
  3. Use <script type="module"> to stop sending transpiled code to browsers that don't need it.

Identify which browsers you want to target

Before you begin to modify how the code in your application is transpiled, you need to identify which browsers access your application. Analyze which browsers your users are currently using as well as those that you plan to target to make an informed decision.

Use @babel/preset-env

Transpiling code usually results in a file that is larger in file size than their original forms. By minimizing the amount of compilation that you do you can reduce the size of your bundles to improve the performance of a web page.

Instead of including specific plugins to selectively compile certain language features you are using, Babel provides a number of presets that bundles plugins together. Use @babel/preset-env to only include the transforms and polyfills needed for the browsers you plan on targeting.

Include @babel/preset-env within the presets array in your Babel configurations file, .babelrc:

{
 "presets": [
   [
     "@babel/preset-env",
     {
       "targets": ">0.25%"
     }
   ]
 ]
}

Use the targets field to specify which browser versions you want to include by adding an appropriate query to the browsers field. @babel/preset-env integrates with browserslist, an open-source configuration shared between different tools for targeting browsers. A full list of compatible queries is in the browserslist documentation. Another option is to use a .browserslistrc file to list the environments you wish to target.

The ">0.25%" value tells Babel to only include the transforms needed to support browsers that make up more than 0.25% of global usage. This ensures your bundle does not contain unnecessary transpiled code for browsers that are used by a very small percentage of users.

In most cases, this is a better approach than using the following configuration:

  "targets": "last 2 versions"

The "last 2 versions" value transpiles your code for the last two versions of every browser, which means support is provided for discontinued browsers such as Internet Explorer. This can unnecessarily increase the size of your bundle if you do not expect these browsers to be used to access your application.

Ultimately, you should select the appropriate combination of queries to only target browsers that fit your needs.

Enable modern bugfixes

@babel/preset-env groups multiple JavaScript syntax features into collections and enables/disables them based on the target browsers specified. Although this works well, an entire collection of syntax features is transformed when a targeted browser contains a bug with just a single feature. This often results in more transformed code than is necessary.

Originally developed as a separate preset, the bugfixes option in @babel/preset-env solves this problem by converting modern syntax that is broken in some browsers to the closest equivalent syntax that is not broken in those browsers. The result is nearly identical modern code with a few small syntax tweaks that guarantee compatibility in all target browsers. To use this optimization, make sure you have @babel/preset-env 7.10 or later installed, then set the bugfixes property to true:

{
 "presets": [
   [
     "@babel/preset-env",
     {
       "bugfixes": true
     }
   ]
 ]
}

In Babel 8, the bugfixes option will be enabled by default.

Use <script type="module">

JavaScript modules, or ES modules, are a relatively new feature supported in all major browsers. You can use modules to create scripts that can import and export from other modules, but you can also use them with @babel/preset-env to only target browsers that support them.

Instead of querying for specific browser versions or market share, consider specifying "esmodules" : true inside your .babelrc file's targets field.

{
   "presets":[
      [
         "@babel/preset-env",
         {
            "targets":{
               "esmodules": true
            }
         }
      ]
   ]
}

Many newer ECMAScript features compiled with Babel are already supported in environments that support JavaScript modules. So by doing this, you simplify the process of making sure that only transpiled code is used for browsers that actually need it.

Browsers that support modules ignore scripts with a nomodule attribute. Conversely, browsers that do not support modules ignore script elements with type="module". This means you can include a module as well as a compiled fallback.

Ideally, the two version scripts of an application are included like this:

  <script type="module" src="main.mjs"></script>
  <script nomodule src="compiled.js" defer></script>

Browsers that support modules fetch and execute main.mjs and ignore compiled.js. The browsers that do not support modules do the opposite.

{% Aside %} Module scripts are deferred by default. The defer attribute is added to the nomodule script for the same behavior. {% endAside %}

If you use webpack, you can set different targets in your configurations for two separate versions of your application:

{% Aside %} Although this HTML approach can provide performance benefits, certain browsers have been found to double-fetch when specifying both module and nomodule scripts. Jason Miller's Modern Script Loading explains this in more detail and covers a few options that can be used to circumvent this. {% endAside %}

With thanks to Connor Clark and Jason Miller for their reviews.

Serve responsive images

Serving desktop-sized images to mobile devices can use 2–4x more data than needed. Instead of a "one-size-fits-all" approach to images, serve different image sizes to different devices.

Responsive images and Core Web Vitals

When you serve responsive images, you're evaluating the display capabilities of the user's device and choosing one of a set of image candidates that are optimal for display based on those criteria. The result—as stated previously—is rather than delivering too much image data to devices that won't benefit from it, you're serving an appropriately sized image for the device. For smaller devices such as phones and tablets, this equates to reduced data usage as compared to devices with larger screens, such as laptops.

The effects of faster image loading can also extend to your page's Largest Contentful Paint (LCP). For example, if your page's LCP element is an image, you're reducing that LCP candidate's resource load time.

Lower resource load times will lower the load time for an LCP image, which will improve the page's LCP score. A lower LCP means users will perceive that your site is loading faster, particularly the largest piece of content visible in the viewport during page load. Serving responsive images can also reduce bandwidth contention for other resources on the page, which can improve how fast your page loads in general.

Resize images

Two of the most popular image resizing tools are the sharp npm package and the ImageMagick CLI tool.

The sharp package is a good choice for automating image resizing (for example, generating multiple sizes of thumbnails for all the videos on your website). It is fast and easily integrated with build scripts and tools. On the other hand, ImageMagick is convenient for one-off image resizing because it is used entirely from the command line.

sharp

To use sharp as a Node script, save this code as a separate script in your project, and then run it to convert your images:

const sharp = require('sharp');
const fs = require('fs');
const directory = './images';

fs.readdirSync(directory).forEach(file => {
  sharp(`${directory}/${file}`)
    .resize(200, 100) // width, height
    .toFile(`${directory}/${file}-small.jpg`);
  });

ImageMagick

To resize an image to 33% of its original size, run the following command in your terminal:

convert -resize 33% flower.jpg flower-small.jpg

To resize an image to fit within 300px wide by 200px high, run the following command:

# macOS/Linux
convert flower.jpg -resize 300x200 flower-small.jpg

# Windows
magick convert flower.jpg -resize 300x200 flower-small.jpg

How many image versions should you create?

There is no single "correct" answer to this question. However, it's common to serve 3-5 different sizes of an image. Serving more image sizes is better for performance, but will take up more space on your servers and require writing a tiny bit more HTML.

Other options

Image services like Thumbor (open-source) and Cloudinary are also worth checking out. Image services provide responsive images (and image manipulation) on-demand. Thumbor is setup by installing it on a server; Cloudinary takes care of these details for you and requires no server setup. Both are easy ways to create responsive images.

Serve multiple image versions

Specify multiple image versions and the browser will choose the best one to use:

Before After
<img src="flower-large.jpg"> <img src="flower-large.jpg" srcset="flower-small.jpg 480w, flower-large.jpg 1080w" sizes="50vw">

The <img> tag's src, srcset, and sizes attributes all interact to achieve this end result.

The "src" attribute

The src attribute makes this code work for browsers that don't support the srcset and sizes attributes. If a browser does not support these attributes, it will fall back to loading the resource specified by the src attribute.

{% Aside 'gotchas' %} The resource specified by the src attribute should be large enough to work well on all device sizes. {% endAside %}

The "srcset" attribute

The srcset attribute is a comma-separated list of image filenames and their width or density descriptors.

This example uses width descriptors. 480w is a width descriptor tells the browser that flower-small.jpg is 480px wide; 1080w is a width descriptor tells the browser that flower-large.jpg is 1080px wide.

"Width descriptor" sounds fancy but is just a way to tell the browser the width of an image. This saves the browser from needing to download the image to determine its size.

{% Aside 'gotchas' %} Use the w unit (instead of px) to write width descriptors. For example, a 1024px wide image would be written as 1024w. {% endAside %}

Extra Credit: You don't need to know about density descriptors to serve different image sizes. However, if you're curious about how density descriptors work, check out the Resolution Switching code lab. Density descriptors are used to serve different images based on the device's pixel density.

The "sizes" attribute

The sizes attribute tells the browser how wide the image will be when it is displayed. However, the sizes attribute has no effect on display size; you still need CSS for that.

The browser uses this information, along with what it knows about the user's device (i.e. its dimensions and pixel density), to determine which image to load.

If a browser does not recognize the "sizes" attribute, it will fallback to loading the image specified by the "src" attribute. (Browsers shipped support for the "sizes" and "srcset" attributes at the same time, so a browser will either support both attributes or neither.)

{% Aside 'gotchas' %} Slot width can be specified using a variety of units. The following are all valid sizes:

The following is not a valid size:

Extra Credit: If you want to be fancy, you can also use the sizes attribute to specify multiple slot sizes. This accommodates websites that use different layouts for different viewport sizes. Check out this multiple slot code sample to learn how to do this.

(Even more) Extra Credit

In addition to all the extra credit already listed (images are complex!), you can also use these same concepts for art direction. Art direction is the practice of serving completely different looking images (rather than different versions of the same image) to different viewports. You can learn more in the Art Direction code lab.

Verify

Once you've implemented responsive images, you can use Lighthouse to make sure that you didn't miss any images. Run the Lighthouse Performance Audit (Lighthouse > Options > Performance) and look for the results of the Properly size images audit. These results will list the images that need to be resized.

Signed Exchanges (SXGs)

Signed exchanges (SXG) are a delivery mechanism that makes it possible to authenticate the origin of a resource independently of how it was delivered. Implementing SXG can improve Largest Contentful Paint (LCP) by enabling privacy-preserving cross-origin prefetch. Additionally, this decoupling advances a variety of use cases such as offline internet experiences and serving from third-party caches.

This article provides a comprehensive overview of SXG: how it works, use cases, and tooling.

Browser compatibility {: #browser-compatibility }

SXG is supported by Chromium-based browsers (starting with versions: Chrome 73, Edge 79, and Opera 64).

Overview

As its primary use case, SXG uses a cache to prefetch and serve content that has been cryptographically signed by the origin. This helps speed cross origin navigations from referer sites while also ensuring that pages remain unaltered and properly attributed to their origin. Any potentially identifying information is hidden until after the user navigates to a site thereby protecting the user’s privacy. Google Search is an early adopter of SXG prefetching capabilities and for sites that receive a large portion of their traffic from Google Search, SXG can be an important tool for delivering faster page loads to users. Over time, we hope this impact will expand to additional referrers.

How it Works

A site signs a request/response pair (an "HTTP exchange") in a way that makes it possible for the browser to verify the origin and integrity of the content independently of how the content was distributed. As a result, the browser can display the URL of the origin site in the address bar, rather than the URL of the server that delivered the content.

{% Img src="image/GiC3si2rdySvEXRggAJBPhgTHFr2/kGUL8NalZ2OaC81a5q8g.PNG", alt="Diagram explaining how Signed Exchanges Works. Browser communicating with the cache which communicates with the destination site", width="800", height="392" %}

Historically, the only way for a site to use a third-party to distribute its content while maintaining attribution has been for the site to share its SSL certificates with the distributor. This has security drawbacks; moreover, it is a far stretch from making content truly portable.

The SXG format

An SXG is encapsulated in a binary-encoded file that has two primary components: an HTTP exchange and a signature that covers the exchange. The HTTP exchange consists of a request URL, content negotiation information, and an HTTP response.

{% Details %} {% DetailsSummary %}

Here's an example of a decoded SXG file. {% endDetailsSummary %}

format version: 1b3
request:
  method: GET
  uri: https://example.org/
  headers:
response:
  status: 200
  headers:
    Cache-Control: max-age=604800
    Digest: mi-sha256-03=kcwVP6aOwYmA/j9JbUU0GbuiZdnjaBVB/1ag6miNUMY=
    Expires: Mon, 24 Aug 2020 16:08:24 GMT
    Content-Type: text/html; charset=UTF-8
    Content-Encoding: mi-sha256-03
    Date: Mon, 17 Aug 2020 16:08:24 GMT
    Vary: Accept-Encoding
signature:
    label;cert-sha256=*ViFgi0WfQ+NotPJf8PBo2T5dEuZ13NdZefPybXq/HhE=*;
    cert-url="https://test.web.app/ViFgi0WfQ-NotPJf8PBo2T5dEuZ13NdZefPybXq_HhE";
    date=1597680503;expires=1598285303;integrity="digest/mi-sha256-03";sig=*MEUCIQD5VqojZ1ujXXQaBt1CPKgJxuJTvFlIGLgkyNkC6d7LdAIgQUQ8lC4eaoxBjcVNKLrbS9kRMoCHKG67MweqNXy6wJg=*;
    validity-url="https://example.org/webpkg/validity"
header integrity: sha256-Gl9bFHnNvHppKsv+bFEZwlYbbJ4vyf4MnaMMvTitTGQ=

The exchange has a valid signature.
payload [1256 bytes]:
<!doctype html>
<html>
<head>
    <title>SXG example</title>
    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
    }
    </style>
</head>
<body>
<div>
    <h1>Hello</h1>
</div>
</body>
</html>

{% endDetails %}

The expires parameter in the signature indicates a SXG's expiration date. A SXG may be valid for at most 7 days. Find more information on the signature header in the Signed HTTP Exchanges spec.

Support for server-side personalization

An SXG containing a Vary: Cookie header will be shown only to users who don't have cookies for the signed request URL. If your site presents different HTML to its logged-in users, you can use this feature to take advantage of SXGs without altering that experience. See details on server-side personalization with Dynamic SXG.

Web Packaging

SXG is part of the broader Web Packaging spec proposal family. In addition to SXGs, the other major component of the Web Packaging spec is Web Bundles ("bundled HTTP exchanges"). Web Bundles are a collection of HTTP resources and the metadata necessary to interpret the bundle.

The relationship between SXGs and Web Bundles is a common point of confusion. SXG and Web Bundles are two distinct technologies that don't depend on each other—Web Bundles can be used with both signed and unsigned exchanges. A common goal advanced by both SXGs and Web Bundles is the creation of a "web packaging" format that allows sites to be shared in their entirety for offline consumption.

Speeding up Page Loads with Signed Exchanges

Enabling Signed Exchanges can help speed up web page performance and thereby impact your site’s Core Web Vitals, in Particular Largest Contentful Paint (LCP). As an early adopter, Google Search uses SXG to provide users with a faster page load experience for pages loaded from the search results page.

Google Search crawls and caches SXGs when available and prefetches SXG that the user is likely to visit—for example, the page corresponding to the first search result.

SXG works best in tandem with other performance optimizations such as use of CDNs and reduction of render-blocking subresources. After implementing, follow these recommendations to maximize the LCP benefit from prefetching SXGs. In many cases, such optimization can result in nearly instant page loads coming from Google Search:

{% Video src="video/rULxC7pPw3PFS4o9xr7v8isFmCv1/MQwtXWQD41XWNTzRHLie.mp4", controls=true, poster="image/rULxC7pPw3PFS4o9xr7v8isFmCv1/cdP5lEt76GS8N3Bix2X2.jpg" %}

Impact of Signed Exchanges

From past experiments we have observed an average of 300ms to 400ms reduction in LCP from SXG-enabled prefetches. This helps sites make a better first-impression on users and often has a positive impact on business metrics.

Several global brands and sites have already benefited from Signed Exchanges. As a case study, lets look at how implementing Signed Exchanges helped RebelMouse, a prominent Content Management System (CMS), improve their customers' performance and business metrics:

Cloudflare found that SXG improved TTFB for 98% of sites it tested, and improved LCP for 85% of sites, with a median improvement of over 20% in SXG-eligible page loads.

{% Aside %} SXG enables cross-origin prefetching because it allows prefetch requests via an SXG cache server operated by the referrer, ensuring that the user's potentially identifying information is not revealed to the origin server until they navigate. This technique is available to any site that wishes to make its outlinks faster or more resilient to limited network access. {% endAside %}

Indexing

The SXG and non-SXG representations of a page are not ranked or indexed differently by Google Search. SXG is ultimately a delivery mechanism—it does not change the underlying content.

AMP

AMP content can be delivered using SXG. SXG allows AMP content to be prefetched and displayed using its canonical URL, rather than its AMP URL.AMP has its own separate tooling for generating SXGs.Learn how to serve AMP using signed exchanges on amp.dev.

Debugging SXGs with Chrome DevTools {: #debugging }

To see a SXG firsthand, use a Chromium browser, open DevTools, open the Network panel, and visit this example search page. Signed Exchanges can be identified by looking for signed-exchange in the Type column.

{% Img src="image/admin/cNdohSaeXqGHFBwD7L3B.png", alt="Screenshot showing a SXG request within the 'Network' panel in DevTools", width="696", height="201" %}
The Network panel in DevTools

The Preview tab provides more information about the contents of a SXG.

{% Img src="image/admin/E0rBwuxk4BxFmLJ3gXhP.png", alt="Screenshot of the 'Preview' tab for a SXG", width="800", height="561" %}
The Preview tab in DevTools

Tooling

Implementing SXGs consists of generating the SXG corresponding to a given URL and then serving that SXG to requestors (usually crawlers).

Certificates

To generate a SXG you will need a certificate that can sign SXGs, although some tools acquire these automatically. This page lists the certificate authorities that can issue this type of certificate. Certificates can be obtained automatically from the Google certificate authority using any ACME client. Web Packager Server has a built-in ACME client, and sxg-rs will soon.

{% Aside %} Production use of SXGs requires a certificate that supports the CanSignHttpExchanges extension. Per spec, certificates with this extension must have a validity period no longer than 90 days and require that the requesting domain have a DNS CAA record configured. {% endAside %}

Platform-specific SXG tooling

These tools support specific technology stacks. If you are already using a platform supported by one of these tools, you may find it easier to set up than a general-purpose tool.

General-purpose SXG tooling

sxg-rs HTTP server

The sxg-rs http_server acts as a reverse proxy for serving SXGs. For requests from SXG crawlers, http_server will sign the responses from the backend and respond with an SXG. For installation instructions, see the README.

Web Packager Server

The Web Packager Server, webpkgserver, is an alternative to sxg-rs http_server, written in Go. For instructions on setting up the Web Packager server, see How to set up signed exchanges using Web Packager.

Web Packager CLI

The Web Packager CLI generates a SXG corresponding to a given URL.

webpackager \
    --private\_key=private.key \
    --cert\_url=https://example.com/certificate.cbor \
    --url=https://example.com

Once the SXG file has been generated, upload it to your server and serve it with the application/signed-exchange;v=b3 MIME type. In addition, you will need to serve the SXG certificate as application/cert-chain+cbor.

SXG libraries

These libraries could be used to build your own SXG generator:

Content negotiation

Servers should serve SXG when the Accept header indicates that the q-value for application/signed-exchange is greater than or equal to the q-value for text/html. In practice, this means that an origin server will serve SXG to crawlers, but not browsers. Many of the above tools do this by default, but for other tools, the following regular expression can be used to match the Accept header of requests that should be served as SXG:

Accept: /(^|,)\s\*application\/signed-exchange\s\*;\s\*v=[[:alnum:]\_-]+\s\*(,|$)/

This recommendation includes examples for Apache and nginx.

{% Aside %} SXG can deliver superior performance when used with caching or prefetching. However, for content that is loaded directly from the origin server without the benefit of these optimizations, text/html delivers better performance than SXG. Serving content as SXG allows crawlers and other intermediaries to cache SXGs for faster delivery to users. {% endAside %}

Update cache API

The Google SXG Cache has an API that site owners can use to remove SXGs from the cache before they've expired due to Cache-Control: max-age. See the update cache API reference for details.

Linking to SXG

Any site can cache, serve, and prefetch SXGs of the pages that it links to, where available, using the and tags:

<a href="https://example.com/article.html.sxg">
<link rel="prefetch" as="document" href="https://example.com/article.html.sxg">

This article illustrates how to use nginx to distribute SXGs.

Unique Advantages

SXG is one of many possible technologies to enable cross-origin prefetching. When deciding which technology to use, you may need to trade off between optimizing different aspects. The following sections illustrate a few of the unique values that SXG provides in the space of possible solutions. These factors may change over time as the space of available solutions evolves.

Fewer requests to serve

With cross-site prefetching, your server may need to serve additional requests. This corresponds to cases where a page was prefetched, but either the user didn't visit the page, or the prefetched bytes couldn't be shown to the user. For SXG, these additional unused requests can be significantly reduced:

Page speed improvement

You may see additional page speed improvement due to the prefetch surfaces and capabilities it currently supports:

Conclusion

Signed Exchanges are a delivery mechanism that make it possible to verify the origin and validity of a resource independently of how the resource was delivered. As a result, SXGs can be distributed by third-parties while maintaining full publisher attribution.

Further reading

How to set up Signed Exchanges using Web Packager

A signed exchange (SXG) is a delivery mechanism that makes it possible to authenticate the origin of a resource independently of how it was delivered. The following instructions explain how to set up Signed Exchanges using Web Packager. Instructions are included for both self-signed and CanSignHttpExchanges certificates.

Serve SXGs using a self-signed certificate

Using a self-signed certificate to serve SXGs is primarily used for demonstration and testing purposes. SXGs signed with a self-signed certificate will generate error messages in the browser when used outside of testing environments and should not be served to crawlers.

Prerequisites

To follow these instructions you will need to have openssl and Go installed in your development environment.

Generate a self-signed certificate

This section explains how to generate a self-signed certificate that can be used with signed exchanges.

Instructions

  1. Generate a private key.

    openssl ecparam -out priv.key -name prime256v1 -genkey
    

    The private key will be saved as a file named priv.key.

  2. Create a certificate signing request (CSR).

    openssl req -new -sha256 -key priv.key -out cert.csr -subj '/O=Web Packager Demo/CN=example.com'
    

    A certificate signing request is a block of encoded text that conveys the information necessary to request a certificate from a certificate authority(CA). Although you will not be requesting a certificate from a CA, it is still necessary to create a certificate signing request.

    The command above creates a certificate signing request for an organization named Web Packager Demo that has the common name example.com. The common name should be the fully qualified domain name of the site that contains the content that you want to package as SXG.

    In a production SXG setup, this would be a site that you own. However, in a testing environment like the one described in these instructions, it can be any site.

  3. Create a certificate that has the CanSignHttpExchanges extension.

    openssl x509 -req -days 90 -in cert.csr -signkey priv.key -out cert.pem -extfile <(echo -e "1.3.6.1.4.1.11129.2.1.22 = ASN1:NULL\nsubjectAltName=DNS:example.com")
    

    This command uses the private key and the CSR created in steps 1 and 2 to create the certificate file cert.pem. The -extfile flag associates the certificate with the CanSignHttpExchanges certificate extension (1.3.6.1.4.1.11129.2.1.22 is the object identifier for the CanSignHttpExchanges extension). In addition, the -extfile flag also defines example.com as a Subject Alternative Name.

    If you are curious about the contents of cert.pem, you can view them using the following command:

    openssl x509 -in cert.pem -noout -text
    

    You are done creating private keys and certificates. You will need the priv.key and cert.pem files in the next section.

Setup the Web Packager server for testing

Prerequisites

  1. Install Web Packager.

    git clone https://github.com/google/webpackager.git
    
  2. Build webpkgserver.

    cd webpackager/cmd/webpkgserver
    go build .
    

    webpkgserver is a specific binary within the Web Packager project.

  3. Verify that webpkgserver has been installed correctly.

    ./webpkgserver --help
    

    This command should return information about the usage of webpkgserver. If this does not work, a good first troubleshooting step is to verify that your GOPATH is configured correctly.

Instructions

  1. Navigate to the webpkgserver directory (you might already be in this directory).

    cd /path/to/cmd/webpkgserver
    
  2. Create a webpkgsever.toml file by copying the example.

    cp ./webpkgserver.example.toml ./webpkgserver.toml
    

    This file contains the configuration options for webpkgserver.

  3. Open webpkgserver.toml with an editor of your choice and make the following changes:

    • Change the line #AllowTestCert = false to AllowTestCert = true.
    • Change the line PEMFile = 'path/to/your.pem' to reflect the path to the PEM certificate, cert.pem, that you created. Do not change the line mentioning TLS.PEMFile—this is a different configuration option.
    • Change the line KeyFile = 'priv.key' to reflect the path of the private key, priv.key, that you created. Do not change the line mentioning TLS.KeyFile—this is a different configuration option.
    • Change the line #CertURLBase = '/webpkg/cert' to CertURLBase = 'data:'. CertURLBase indicates the serving location of the SXG certificate. This information is used to set the cert-url parameter in the Signature header of the SXG. In production environments, CertURLBase is used like this: CertURLBase = 'https://mysite.com/'. However, for local testing, CertURLBase = 'data:' can be used to instruct webpkgserver to use a data URL to inline the certificate in the cert-url field. For local testing, this is the most conveninent way to serve the SXG certificate.
    • Change the line Domain = 'example.org' to reflect the domain that you created a certificate for. If you have followed the instructions in this article verbatim, this should be changed to example.com. webpkgserver will only fetch content from the domain indicated by webpkgserver.toml. If you try to fetch pages from a different domain without updating webpkgserver.toml, the webpkgserver logs will show the error message URL doesn't match the fetch targets.

    Optional

    If you want to enable or disable subresource preloading, the following webpkgserver.toml configuration options are relevant:

    • To have webpkgserver insert directives for preloading stylesheet and script subresources as SXGs, change the line #PreloadCSS = false to PreloadCSS = true. In addition, change the line #PreloadJS = false to PreloadJS = true.

      As an alternative to using this configuration option, you can manually add Link: rel="preload" headers and <link rel="preload"> tags to a page's HTML.

    • By default, webpkgserver replaces existing <link rel="preload"> tags with the equivalent <link> tags necessary for fetching this content as SXG. In doing so, webpkgserver will set the allowed-alt-sxg and header-integrity directives as needed—HTML authors do not need to add these by hand. To override this behavior and keep existing non-SXG preloads, change #KeepNonSXGPreloads (default = false) to KeepNonSXGPreloads = true. Keep in mind that enabling this option may make the SXG ineligible for the Google SXG cache per these requirements.

  4. Start webpkgserver.

    ./webpkgserver
    

    If the server has started successfully, you should see the following log messages:

    Listening at 127.0.0.1:8080
    Successfully retrieved valid OCSP.
    Writing to cache in /private/tmp/webpkg
    

    Your log messages may look slightly different. In particular, the directory that webpkgserver uses to cache certificates varies by operating system.

    If you do not see these messages, a good first troubleshooting step is to double-check webpkgserver.toml.

    If you update webpkgserver.toml you should restart webpkgserver.

  5. Launch Chrome using the following command:

    /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
    --user-data-dir=/tmp/udd \
    --ignore-certificate-errors-spki-list=`openssl x509 -noout -pubkey -in cert.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64`
    

    This command instructs Chrome to ignore the certificate errors associated with cert.pem. This makes it possible to test SXGs using a test certificate. If Chrome is launched without this command, inspecting the SXG in DevTools will display the error Certificate verification error: ERR_CERT_INVALID.

    Note:

    You may need to adjust this command to reflect the location of Chrome on your machine, as well as the location of cert.pem. If you've done this correctly, you should see a warning displayed below the address bar. The warning should be similar to this: You are using an unsupported command-line flag: --ignore-certificate-errors-spki-list=9uxADcgc6/ho0mJLRMBcOjfBaN21k0sOInoMchr9CMY=.

    If the warning does not include a hash string, you have not correctly indicated the location of the SXG certificate.

  6. Open the DevTools Network tab, then visit the following URL: http://localhost:8080/priv/doc/https://example.com.

    This makes a request to the webpackager instance running at http://localhost:8080 for a SXG containing the contents of https://example.com. /priv/doc/ is the default API endpoint used by webpackager.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/DPw05KcHLdWOgHYiDsgf.png", alt="Screenshot of the DevTools Network tab showing a SXG and its certificate.", width="800", height="236" %}

The following resources are listed in the Network tab:

If you don't see these resources, try clearing the browser cache, then reloading http://localhost:8080/priv/doc/https://example.com.

Click on the Preview tab to see more information about the Signed Exchange and its signature.

{% Img src="image/j2RDdG43oidUy6AL6LovThjeX9c2/x9hfT6ZJ8OuB4M1ImWic.png", alt="Screenshot of the Preview tab showing a SXG", width="800", height="541" %}

Serve signed exchanges using a CanSignHttpExchanges certificate

The instructions in this section explain how to serve SXGs using a CanSignHttpExchanges certificate. Production use of SXGs requires a CanSignHttpExchanges certificate.

For the sake of brevity, these instructions are written with the assumption that you understand the concepts discussed in the Setup Signed Exchanges using a self-signed certificate section.

Prerequisites

Instructions

  1. Create a PEM file by concatenating your site's SXG certificate followed by your site's CA certificate. More instructions on this can be found here.

    PEM is a file format that is commonly used as a "container" for storing multiple certificates.

  2. Create a fresh webpkgsever.toml file by copying the example.

    cp ./webpkgserver.example.toml ./webpkgserver.toml
    
  3. Open webpkgserver.toml with the editor of your choice and make the following changes:

    • Change the line PEMFile = cert.pem to reflect the location of the PEM file containing your full certificate chain.
    • Change the line KeyFile = 'priv.key' to reflect the location of the private key corresponding to your PEM File.
    • Change the line Domain = 'example.org' to reflect your site.
    • (Optional) To have webpkgserver auto-renew the SXG certificate every 90 days (45 days for Google), configure the options in the [SXG.ACME] section of webpkgserver.toml. This option only applies to sites with a DigiCert or Google ACME account setup.
  4. Configure your edge server to forward traffic to the webpkgserver instance.

    There are two primary types of requests handled by webpkgserver: requests for SXGs (which are served by the /priv/doc/ endpoint) and requests for the SXG certificate (which are served by the /webpkg/cert/ endpoint). The URL rewriting rules for each of these request types varies slightly. For more information, see Running behind front end edge server.

    Note:

    By default, webpkgserver serves the SXG certificate at /webpkg/cert/$CERT_HASH—for example, /webpkg/cert/-0QmE0gvoedn92gtwI3s7On9zPevJGm5pn2RYhpZxgY. To generate $CERT_HASH, run the following command:

    openssl base64 -in cert.pem -d | openssl dgst -sha256 -binary | base64 | tr /+ _- | tr -d =
    

Third-party JavaScript performance

Third-party JavaScript generally refers to scripts embedded in your website that are:

Sites use these scripts for various purposes, including:

{% Video src="video/tcFciHGuF3MxnTr1y5ue01OGLBn2/uLXJ72jZAlzK56ctPwXd.mp4", autoplay=true, loop=true, muted=true, playsinline=true %}

Third-party scripts can provide powerful functionality, but that's not the whole story. They also affect privacy, security, and page behavior⁠—and they can be particularly problematic for performance.

Performance

Any significant amount of JavaScript can slow down performance. But because third-party JavaScript is usually outside your control, it can bring additional issues.

Network

Setting up connections takes time, and sending too many requests to multiple servers causes slowdowns. That time is even longer for secure connections, which may involve DNS lookups, redirects, and several round trips to the final server that handles the user's request.

Third-party scripts often add to network overhead with things such as:

Rendering

The way third-party JavaScript is loaded matters a lot. If it's done synchronously in the critical rendering path it delays parsing of the rest of the document.

{% Aside 'key-term' %} The critical rendering path includes all resources that the browser needs to display the first screen's worth of content. {% endAside %}

If a third party has server issues and fails to deliver a resource, rendering is blocked until the request times out, which can be anywhere from 10 to 80 seconds. You can test and simulate this problem with WebPageTest Single-Point-of-Failure tests.

{% Aside %} A/B testing scripts can also often delay rendering. Most of them block content display until they complete processing—which can be true even for asynchronous A/B testing scripts. {% endAside %}

What to do about it

Using third-party JavaScript is often unavoidable, but there are things you can do to minimize adverse effects:

To learn how to audit third-party content and load it efficiently for better performance and user experience, check out the other posts in the Optimize your third-party resources section.

Use Imagemin to compress images

Uncompressed images bloat your pages with unnecessary bytes. Because images can be candidates for Largest Contentful Paint (LCP), those unnecessary bytes can add unnecessary resource load time, which can result in longer LCP times.

The photo on the right is 40% smaller than the one on the left, yet would probably look identical to the average user.

{% Img src="image/admin/LRE2JJAuShXTjQF5ZSaR.jpg", alt="", width="376", height="250" %}

20 KB

{% Img src="image/admin/u9hncwN4TsT7zw2ObU10.jpg", alt="", width="376", height="250" %}

12 KB

Measure

Run Lighthouse to check for opportunities to improve page load by compressing images. These opportunities are listed under "Efficiently encode images":

{% Img src="image/admin/LnIukPEZHuVJwBtuJ7mc.png", alt="image", width="800", height="552" %}

{% Aside %} Lighthouse currently reports on opportunities to compress images in JPEG format only. {% endAside %}

Imagemin

Imagemin is an excellent choice for image compression because it supports a wide variety of image formats and is easily integrated with build scripts and build tools. Imagemin is available as both a CLI and an npm module. Generally, the npm module is the best choice because it offers more configuration options, but the CLI can be a decent alternative if you want to try Imagemin without touching any code.

Plugins

Imagemin is built around "plugins." A plugin is an npm package that compresses a particular image format (e.g. "mozjpeg" compresses JPEGs). Popular image formats may have multiple plugins to pick from.

The most important thing to consider when choosing a plugin is whether it is "lossy" or "lossless." In lossless compression, no data is lost. Lossy compression reduces file size, but at the expense of possibly reducing image quality. If a plugin doesn't mention whether it is "lossy" or "lossless," you can tell by its API: if you can specify the image quality of the output, then it is "lossy."

For most people, lossy plugins are the best choice. They offer significantly greater filesize savings, and you can customize the compression levels to meet your needs. The table below lists popular Imagemin plugins. These aren't the only plugins available, but they'd all be good choices for your project.

Image Format Lossy Plugin(s) Lossless Plugin(s)
JPEG imagemin-mozjpeg imagemin-jpegtran
PNG imagemin-pngquant imagemin-optipng
GIF imagemin-giflossy imagemin-gifsicle
SVG imagemin-svgo
WebP imagemin-webp imagemin-webp

Imagemin CLI

The Imagemin CLI works with 5 different plugins: imagemin-gifsicle, imagemin-jpegtran, imagemin-optipng, imagemin-pngquant, and imagemin-svgo. Imagemin uses the appropriate plugin based on the image format of the input.

To compress the images in the "images/" directory and save them to the same directory, run the following command (overwrites the original files):

$ imagemin images/* --out-dir=images

Imagemin npm module

If you use one of these build tools, checkout the codelabs for Imagemin with webpack, gulp, or grunt.

You can also use Imagemin by itself as a Node script. This code uses the "imagemin-mozjpeg" plugin to compress JPEG files to a quality of 50 ('0' being the worst; '100' being the best):

const imagemin = require('imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');

(async() => {
  const files = await imagemin(
      ['source_dir/*.jpg', 'another_dir/*.jpg'],
      {
        destination: 'destination_dir',
        plugins: [imageminMozjpeg({quality: 50})]
      }
  );
  console.log(files);
})();

Use Lighthouse for performance budgets

Lighthouse now supports performance budgets. This feature, known as LightWallet, can be set up in under five minutes and provides feedback on performance metrics and the size and quantity of page resources.

{% Aside 'important' %} While Lighthouse is an excellent tool for identifying performance improvement opportunities, it can't be stressed enough that Lighthouse is a tool that relies on lab data. While lab data is an integral part of any performance improvement effort, comparing and contrasting that data with field data collected from your website's actual users is key. Doing so will help you understand how the changes you make to your website affect real users. {% endAside %}

Install Lighthouse

LightWallet is available in the command line version of Lighthouse v5+.

To get started, install Lighthouse:

npm install -g lighthouse

Create a Budget

Create a file named budget.json. In this file add the following JSON:

[
  {
    "path": "/*",
    "timings": [
      {
        "metric": "interactive",
        "budget": 3000
      },
      {
        "metric": "first-meaningful-paint",
        "budget": 1000
      }
    ],
    "resourceSizes": [
      {
        "resourceType": "script",
        "budget": 125
      },
      {
        "resourceType": "total",
        "budget": 300
      }
    ],
    "resourceCounts": [
      {
        "resourceType": "third-party",
        "budget": 10
      }
    ]
  }
]

This example budget.json file sets five separate budgets:

For a complete list of supported performance metrics and resource types, refer to the Performance Budgets section of the Lighthouse docs.

Run Lighthouse

Run Lighthouse using the --budget-path flag. This flag tells Lighthouse the location of your budget file.

lighthouse https://example.com --budget-path=./budget.json

{% Aside %} Note: A budget file does not have to be named budget.json. {% endAside %}

View the Results

If LightWallet has been configured correctly, the Lighthouse report will contain a Budgets section within the Performance category.

{% Img src="image/admin/FdUeI8rKZtJB3Ol624S3.png", alt="'Budgets' section of the Lighthouse report", width="800", height="289" %}

In the JSON version of the Lighthouse report, Lightwallet results can be found within the audit findings for the performance-budget audit.

Using bundlesize with Travis CI

Using bundlesize with Travis CI lets you define performance budgets with minimal setup and enforce them as part of your development workflow. Travis CI is a service that runs tests for your app in the cloud every time you push code to GitHub. You can configure your repository so that it won't allow merging pull-requests unless the bundlesize tests have passed.

Bundlesize's GitHub checks include a size comparison to the main branch and a warning in case of a big jump in size.

{% Img src="image/admin/8Mm1WPga9dbeIzGrv2fQ.jpg", alt="Bundlesize check on GitHub", width="769", height="316" %}

{% Aside %} You can also use bundlesize with Circle CI, Wrecker and Drone. {% endAside %}

To see it in action, here's an app bundled with webpack that lets you vote for your favorite kitty.

{% Img src="image/admin/DGSSFfpAMIaFqX8MwWss.png", alt="Cat voting app", width="800", height="567" %}

Set the performance budget

This Glitch already contains bundlesize.

{% Instruction 'remix' %}

The main bundle of this app is in the public folder. To test its size, add the following section to the package.json file:

"bundlesize": [
  {
    "path": "./public/*.bundle.js",
    "maxSize": "170 kB"
  }
]

{% Aside %} You can also set different thresholds for different files. This is especially useful if you are splitting a bundle in your application. {% endAside %}

To keep the compressed JavaScript bundle size under the recommended limit, set the performance budget to 170KB in the maxSize field.

Bundlesize supports glob patterns and the * wildcard character in the file path will match all bundle names in the public folder.

{% Aside %} By default, bundlesize tests gzipped sizes. You can use the compression option to switch to brotli compression or turn it off completely. {% endAside %}

Create a test script

Since Travis needs a test to run, add a test script to package.json:

"scripts": {
  "start": "webpack && http-server -c-1",
  "test": "bundlesize"
}

Set up continuous integration

Integrate GitHub and Travis CI

First, create a new repository for this project on your GitHub account and initialize it with a README.md.

You'll need to register an account on Travis and activate GitHub Apps integration under the Settings section of your profile.

{% Img src="image/admin/kMgVmB5rzRJN3DlqP08R.png", alt="GitHub Apps integration on Travis CI", width="800", height="508" %}

Once you have an account, go to Settings under your profile, click the Sync account button, and make sure your new repo is listed on Travis.

{% Img src="image/admin/Zi9Oo5SCfM5P7IxejYd3.png", alt="Travis CI Sync button", width="160", height="54" %}

Authorize bundlesize to post on pull requests

Bundlesize needs authorization to be able to post on pull requests, so visit this link to get the bundlesize token that will be stored in the Travis configuration.

{% Img src="image/admin/GkEMv2VCb25oC9lDSARQ.jpg", alt="bundlesize token", width="619", height="330" %}

In your project's Travis dashboard, go to More options > Settings > Environment variables.

{% Img src="image/admin/gol14FsIsYPyPWwIeYfI.png", alt="Adding environment variables on Travis CI", width="789", height="233" %}

Add a new environment variable with the token as the value field and BUNDLESIZE_GITHUB_TOKEN as the name.

The last thing you need to kick-off continuous integration is a .travis.yml file, which tells Travis CI what to do. To speed things up, it is already included in the project and it specifies that the app is using NodeJS.

With this step, you're all set up and bundlesize will warn you if your JavaScript ever goes over the budget. Even when you start off great, over time, as you add new features, kilobytes can pile up. With automated performance budget monitoring, you can rest easy knowing that it won't go unnoticed.

Try it out

Trigger your first bundlesize test

To see how the app stacks up against the performance budget, add the code to the GitHub repo that you created in step 3.

  1. On Glitch, click Tools > Git, Import, and Export > Export to GitHub.

  2. In the pop-up, enter your GitHub username and the name of the repo as username/repo. Glitch will export your app to a new branch named "glitch".

  3. Create a new pull request by clicking the New pull request button on the homepage of the repository.

You'll now see status checks in progress on the pull request page.

{% Img src="image/admin/SrdHGr9z5QY1vEfBwNIY.png", alt="GitHub checks in progress", width="774", height="351" %}

It won't take long until all checks are done. Unfortunately, the cat voting app is a bit bloated and does not pass the performance budget check. The main bundle is 266 KB and the budget is 170 KB.

{% Img src="image/admin/Dt31nFkMvJjn1me6cir3.png", alt="Failed bundlesize check", width="774", height="347" %}

Optimize

Luckily, there are some easy performance wins you can make by removing unused code. There are two main imports in src/index.js:

import firebase from "firebase";
import * as moment from 'moment';

The app is using Firebase Realtime Database to store the data, but it's importing the entire firebase package which consists of a lot more than just a database (auth, storage, messaging etc.).

Fix this by importing only the package that the app needs in the src/index.js file:

import firebase from "firebase";
import firebase from 'firebase/app';
import 'firebase/database';

{% Aside %} The firebase/app import, which sets up the API surface for each of the different services, is always required. {% endAside %}

Re-run test

Since the source file has been updated, you need to run webpack to build the new bundle file.

  1. Click the Tools button.

  2. Then click the Console button. This will open the console in another tab.

  3. In the console, type webpack and wait for it to finish the build.

  4. Export the code to GitHub from Tools > Git, Import, and Export > Export to GitHub.

  5. Go to the pull request page on GitHub and wait for all checks to finish.

{% Img src="image/admin/3aKOqNvvavQ32gl15PmU.png", alt="Passed bundlesize check", width="778", height="355" %}

Success! The new size of the bundle is 125.5 KB and all the checks have passed. 🎉

Unlike Firebase, importing parts of the moment library cannot be done as easily, but it's worth a shot. Check out how you can further optimize the app in the Remove unused code codelab.

Monitor

The app is now under the budget and all is well. Travis CI and bundlesize will keep monitoring the performance budget for you, making sure your app stays fast.

Using Lighthouse Bot to set a performance budget

You've done hard work to get fast, now make sure you stay fast by automating performance testing with Lighthouse Bot.

Lighthouse grades your app in 5 categories, and one of those is performance. While you could try to remember to monitor performance changes with DevTools or Lighthouse CLI every time you edit your code, you don't have to do that. Tools can do the tedious stuff for you. Travis CI is a great service that automatically runs tests for your app in the cloud every time you push new code.

Lighthouse Bot integrates with Travis CI, and its performance budget feature ensures that you won't accidentally downgrade performance without noticing. You can configure your repository so that it won't allow merging pull-requests if the Lighthouse scores fall below the threshold you've set (e.g. < 96/100).

{% Img src="image/admin/LIEdWOuIGubFE0JgBM5Y.png", alt="Failing Lighthouse Bot checks on GitHub", width="800", height="360" %}
Lighthouse Bot checks on GitHub.

{% Aside %} Lighthouse Bot used to be called Lighthouse CI. {% endAside %}

Although you can test performance on localhost, your site will often perform differently on live servers. To get a more realistic picture, it's best to deploy your site to a staging server. You can use any hosting service; this guide will take Firebase hosting for a spin.

1. Setup

This simple app helps you sort three numbers.

Clone the example from GitHub, and make sure to add it as a repository on your GitHub account.

2. Deploy to Firebase

To get started, you'll need a Firebase account. Once you've taken care of that, create a new project in the Firebase console by clicking "Add project":

{% Img src="image/admin/SYTTK9CHxnTt1n7vONKU.png", alt="", width="517", height="552" %}

Deploying to Firebase

You'll need Firebase CLI to deploy the app. Even if you already have it installed, it's good practice to frequently update the CLI to the latest version with this command:

npm install -g firebase-tools

To authorize the Firebase CLI, run:

firebase login

Now initialize the project:

firebase init

The console will ask you a series of questions during setup:

This process creates a firebase.json configuration file in the root of your project directory.

Congrats, you're ready to deploy! Run:

firebase deploy

In a split second, you'll have a live app.

3. Setting up Travis

You'll need to register an account on Travis and then activate GitHub Apps integration under the Settings section of your profile.

{% Img src="image/admin/fNGAjcYM5Rw3e43PExoD.png", alt="GitHub Apps integration on Travis CI", width="800", height="508" %}

Once you have an account

Go to Settings under your profile, hit the Sync account button, and make sure your project repo is listed on Travis.

{% Img src="image/admin/dnXFBpBw4WvWbXQL1DQf.png", alt="", width="160", height="54" %}

To kick-off continuous integration, you need two things:

  1. To have a .travis.yml file in the root directory
  2. To trigger a build by doing a regular old git push

The lighthouse-bot-starter repo already has a .travis.yml YAML file:

language: node_js
node_js:
  - "8.1.3"
install:
  - npm install
before_script:
  - npm install -g firebase-tools
script:
  - webpack

The YAML file tells Travis to install all the dependencies and build your app. Now it's your turn to push the example app to your own GitHub repository. If you haven't already, run the following command:

git push origin main

Click on your repo under Settings in Travis to see your project's Travis dashboard. If everything is cool, you'll see your build go from yellow to green in a couple of minutes. 🎉

4. Automate Firebase deployment with Travis

In Step 2, you logged into your Firebase account and deployed the app from the command line with firebase deploy. In order for Travis to deploy your app to Firebase, you have to authorize it. How do you do that? With a Firebase token. 🗝️🔥

Authorize Firebase

To generate the token run this command:

firebase login:ci

It will open a new tab in a browser window so that Firebase can verify you. After that, look back at the console, and you'll see your freshly minted token. Copy it and go back to Travis.

In your project's Travis dashboard, go to More options > Settings > Environment variables.

{% Img src="image/admin/uU7MBc5NdBDZch3ZE3Zd.png", alt="", width="789", height="233" %}

Paste the token in the value field, name the variable FIREBASE_TOKEN, and add it.

Add deployment to your Travis setup

You need the following lines to tell Travis to deploy the app after every successful build. Add them to the end of the .travis.yml file. 🔚

after_success:
  - firebase deploy --token $FIREBASE_TOKEN --non-interactive

Push this change to GitHub and wait for your first automated deployment. If you take a look at your Travis log, it should soon say ✔️ Deploy complete!

Now whenever you make changes to your app, they will be automatically deployed to Firebase.

5. Setting up Lighthouse Bot

Friendly Lighthouse Bot updates you on your app's Lighthouse scores. It just needs an invitation to your repo.

On GitHub, go to your project's settings and add "lighthousebot" as a collaborator (Settings>Collaborators):

{% Img src="image/admin/H2aLCOr36UDwm5Yk1k9r.png", alt="Lighthouse bot collaborator status", width="800", height="298" %}

Approving these requests is a manual process so they don't always happen instantly. Before you start testing, make sure lighthousebot has approved its collaborator status. In the meantime, you also need to add another key to your project's environment variables on Travis. Leave your email here, and you'll get a Lighthouse Bot key in your inbox. 📬

On Travis, add this key as an environment variable and name it LIGHTHOUSE_API_KEY:

{% Img src="image/admin/0XCrRSbUg1Sdca8k9xK9.jpg", alt="", width="800", height="127" %}

{% Aside %} You can reuse this same key for other projects. {% endAside %}

Add Lighthouse Bot to your project

Next, add Lighthouse Bot to your project by running:

npm i --save-dev https://github.com/ebidel/lighthousebot

And add this bit to the package.json:

"scripts": {
  "lh": "lighthousebot"
}

Add Lighthouse Bot to your Travis configuration

For the final trick, test the performance of the app after every pull request!

In .travis.yml add another step in after_success:

after_success:
  - firebase deploy --token $FIREBASE_TOKEN --non-interactive
  - npm run lh -- https://staging.example.com

It will run a Lighthouse audit on the given URL, so replace https://staging.example.com with the URL of your app (that's your-app-123.firebaseapp.com).

Set your standards high and tweak the setup so you don't accept any changes to the app that bring the performance score below 95:

- npm run lh -- --perf=95 https://staging.example.com

Make a pull request to trigger Lighthouse Bot test on Travis

Lighthouse Bot will only test pull requests, so if you push to the main branch now, you'll just get "This script can only be run on Travis PR requests" in your Travis log.

To trigger the Lighthouse Bot test:

  1. Checkout a new branch
  2. Push it to GitHub
  3. Make a pull request

Hang tight on that pull request page and wait for Lighthouse Bot to sing! 🎤

{% Img src="image/admin/SmWHb70YqVfagXI3f03D.png", alt="Passing Lighthouse scores", width="586", height="329" %}

{% Img src="image/admin/ZrPGH5OGEY5Y4e9ntUBK.png", alt="Passing GitHub checks", width="462", height="189" %}

The performance score is great, the app is under budget, and the check has passed!

More Lighthouse options

Remember how Lighthouse tests 5 different categories? You can enforce scores for any of those with Lighthouse Bot flags:

--perf  # performance
--pwa   # progressive web app score
--a11y  # accessibility score
--bp    # best practices score
--seo   # SEO score

Example:

npm run lh -- --perf=93 --seo=100 https://staging.example.com

This will fail the PR if the performance score drops below 93 or the SEO score drops below 100.

You can also choose not to get Lighthouse Bot's comments with the --no-comment option.

The value of speed

After a lot of hard work, you've done it. You've made your company's site noticeably faster. Now it's time for the fun part: showing stakeholders how much extra revenue your work has generated!

In this post we'll walk through how to do that by calculating the relative mobile conversion rate. This metric is useful because it quantifies the effects of site improvements while excluding external factors like marketing campaigns, which can obscure your findings. Let's get started!

Relative Mobile Conversion Rate (Rel mCvR)

A site's conversion rate can be influenced by site characteristics— like speed and usability—and by external factors—like marketing campaigns, seasonal events, and the mix of marketing channels.

{% Aside %} A conversion happens when a site visitor becomes a customer. What counts as a conversion will vary depending on the nature of the site. Buying a product, subscribing to a service, registering as a user, or even just viewing a particular page could be considered a conversion. Check with your marketing team if you're not sure how conversions are counted in your company. {% endAside %}

Since you're interested in how site speed affects conversions, the mobile site is most relevant— that's where you're most likely to see the benefits of speed improvements. Rather than looking at the mobile conversion rate alone, though, you'll be analyzing the relative mobile conversion rate (Rel mCvR), which is calculated by dividing the mobile conversion rate by the desktop conversion rate. This approach reduces the noise from external factors, which tend to affect both desktop and mobile, and makes it easier to see whether any increases in the mobile site's effectiveness were actually caused by the speed improvements.

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/6q2BiNPameuAqeMOghoE.jpg", alt="Table showing comparison of mobile/desktop conversion rate and relative mobile conversion rate", width="800", height="450" %}

{% Aside 'caution' %} Caution: Rel mCvR is influenced not only by speed, but also by other site characteristics like usability. If other big changes were made to the site during the period you want to analyze, you won't be able to measure the effects of speed improvements on their own, but you can show the benefits of all the improvements as a group. {% endAside %}

Doing the analysis

Make sure you have access to your site's Google Analytics, or collaborate with the analytics team. If you don't have a Google Analytics account, you can learn how to set one up at Get started with Analytics.

Step 1: Go to Google Analytics and click Admin. Under View, choose View Settings. There, copy the View ID.

Step 2: Go to this spreadsheet and click File, and Make a copy.

Step 3: Insert the View ID from Google Analytics into field B3, C3 and D3 in the spreadsheet. If your Google Analytics has goals instead of Ecommerce Conversion Rate, change field B6 and C6 so that you remove ga:transactionsPerSession and instead type in ga:goalConversionRateAll in the two fields.

Step 4: In the spreadsheet, click Add-ons, Google Analytics, and choose Run reports. Then go to the spreadsheet page Rel mCvR and see the results.

You should now have a chart that looks something like this:

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/d7VY9rh0nPtrfyvctkeb.jpg", alt="Chart showing mobile load time vs relative mobile conversion rate.", width="800", height="495" %}

Step 6: Using the chart, identify a period before the speed optimization (when load times were high) and a period after the speed optimization (when load times should be lower) that you want to analyze. In this example, you would compare eight weeks in Jan–Feb to eight weeks in Aug–Sept.

Step 7: In a new sheet, calculate the average load time and rel mCvR for the two periods. Then add the revenue coming from mobile visitors during the period after the speed optimization (Aug–Sept in the example). You can find revenue data in Google Analytics under the section Audience > Mobile > Overview.

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/NH6HEdFAmI5IHXOwSPlf.jpg", alt="Screenshot: Image of table in showing revenue data", width="800", height="122" %}

Step 8: Now calculate what the revenue would have been if Rel mCvR had not improved. Do this by dividing the revenue (€1,835,962) by the current Rel mCvR (51%) and multiplying by the Rel mCvR for the period before the speed optimization (42%).

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/HNkshbjj8pYGbGlMoZQ1.jpg", alt="Screenshot: spreadsheet cells showing formula for revenue without Rel mCvR improvements", width="800", height="202" %}

Step 9: Subtract the revenue that the company earned from what it would have earned if Rel mCvR had not improved.

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/I0LBdqLJ7i9gaTZLqgQc.jpg", alt="Screenshot: spreadsheet cells showing extra revenue formula", width="800", height="242" %}

In this example, the company earned an additional €323,993 in eight weeks thanks to Rel mCvR improving —that is, thanks to the mobile site becoming faster.

{% Img src="image/ZDZVuXt6QqfXtxkpXcPGfnygYjd2/DhukJqLa37bBxJbkhmaH.jpg", alt="Screenshot: spreadsheet cells showing extra revenue due to Rel mCvR improvements", width="800", height="242" %}

Things to consider when analyzing Rel mCvR

As noted above, other changes to the site, such as UX improvements, can influence Rel mCvR.

Summing up

While it has some limitations, Rel mCvR is a great low-cost way to estimate how much a speed optimization increased revenue without, for example, having to run server-side or slow-down tests. And quantifying the relationship between performance and revenue can help you demonstrate the value of development projects whose benefits might not be immediately clear to non-technical stakeholders.

Next steps

Aircraft instrument panel image by Arie Wubben on Unsplash

What is speed?

So, speed matters, but what exactly do we mean by it? What does it mean to have a fast site?

It's common to hear people talk in terms of their website loading in x.xx seconds or similar, but a load is not a single moment in time; it's an experience that no single metric can fully capture. There are multiple moments during the load experience that can affect whether a user perceives it as 'fast', and if you just focus solely on one, you might miss bad experiences that happen during the rest of the time.

Rather than measuring load with just one metric, you should time each moment during the experience that affects the user's perception of load speed. When a user navigates to a web page, they're typically looking for certain types of feedback:

{% Img src="image/admin/NGX9WC2BXTRY6FP5TTGl.png", alt="Image of feedback user is typically looking for", width="800", height="185" %}

Traditional performance metrics like load time or DOMContentLoaded time are unreliable, since their occurrence may or may not correspond with these feedback milestones. So, additional metrics have emerged that could be used to understand when a page delivers this feedback to its users:

{% Img src="image/admin/tz1aubGGvefskjcPfjBQ.png", alt="Image of speed metrics", width="800", height="654" %}

It's important to understand the different insights offered by these metrics, then establish the ones that are important to your user experience. Some brands even define additional custom metrics specific to the expectations people have of their service. In the case of Pinterest, users want to see images, so they defined a custom metric, Pinner Wait Time, that combines Time to Interactive and Above the Fold Image load times.

Even though the load is more than one moment in time, it can still be useful to have a single metric for the purposes of simplified reporting or comparison: Speed Index and Lighthouse score can both be used in this way.

What should you measure to improve performance?

The different steps of a purchase funnel are prone to performance issues in different ways, and therefore need different measurement and optimizations:

{% Img src="image/admin/87cAGdJAnPggf8Yt58ID.png", alt="A conversion funnel going from discover to engage to convert to re-engage.", width="800", height="399" %}
A conversion funnel.

In this guide we'll address how the different steps can be measured. For this we recommend you to look at lab as well as field data.

Lab data is gathered by running tests locally, for example by using Lighthouse and other tools. This can make it possible to compare website performance over time and with competitors through a controlled, stable environment, but it might not be representative of the performance users experience in real life.

Field data is gathered via analytics from real users, and is therefore representative of their experience. However, it can't easily be compared over time or with competitors. Network connections and smartphone hardware evolve over time, and different target audiences might have different devices, thereby making comparisons with field data tough. See also Measure Performance In The Field.

To get a complete picture, both data sources are needed. The following sections show how to gather lab and field data for the different relevant performance marks across the funnel.

Discovery

Optimizing for discovery means optimizing for the first load, which is what new users will get, but also search and social crawlers. Lab data for a first load can be easily acquired through Lighthouse, while field data (at least for Chrome) is readily available through Chrome UX reports. A convenient combination of both can be found in PageSpeed Insights. You should also track relevant metrics from the field yourself: Measuring these metrics on real users' devices provides a good overview.

From a user perspective the most important metrics are:

There are more metrics you can look at, but these are a good baseline. Additionally also make sure to capture bounce rates, conversions and user engagement so that you can set these in relation.

Engagement And Conversion

After the first load of a landing page, a user will proceed through your site, hopefully towards a successful conversion.

In this phase it is important to have fast and responsive navigations and interactions. Unfortunately it is not trivial to measure the complete flow of navigation and interaction events in the field, as every user takes different paths through the page. We therefore recommend measuring the time needed towards conversion or conversion subgoals ("Time-to-Action") by scripting and measuring the flow in a lab test, to compare performance over time or with competitors.

There are two convenient ways of doing this:

WebPageTest

WebPageTest offers a very flexible scripting solution. The basic idea is to:

Such a script could look like this:

combineSteps
navigate	https://www.store.google.com/landingpage
navigate	https://www.store.google.com/productpage
clickAndWait	innerText=Buy Now
navigate	https://www.store.google.com/basket
navigate	https://www.store.google.com/checkout

With a script like this in place you can easily measure and compare performance over time. This can even be automated through the WebPageTest API.

Puppeteer

Another great option to script testing is via headless Chrome, which can be controlled through the Node API Puppeteer. The general idea is to start the browser through Puppeteer, navigate to the landing page through the goto function, inject JavaScript to fill fields or click buttons and proceed through the funnel through further goto calls as needed.

As a metric the duration of the flow can be measured directly, but you could also sum up the FCP, FMP or TTI values of the individual loads of the flow. Test website performance with Puppeteer provides an overview of how to get performance metrics via Puppeteer. A very simplified example Node script could look like this:

const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const start = performance.now();
  await page.goto('https://www.store.google.com/landingpage');
  await page.goto('https://www.store.google.com/productpage');
  // click the buy button, which triggers overlay basket
  await page.click('#buy_btn');
  // wait until basket overlay is shown
  await page.waitFor('#close_btn');
  await page.goto('https://www.store.google.com/basket');
  await page.goto('https://www.store.google.com/checkout');
  console.log('Flow took ' + parseInt((performance.now() - start)/1000) + ' seconds');
  await browser.close();
})();

This script can easily be automated, and even be made part of the build process or perf budgets, and be monitored regularly.

Re-Engagement

Users will return to your site in different time intervals. Depending on time passed, the browser may have less of the website's resources cached, needing more network requests. This makes it difficult to estimate performance differences across repeat visits in lab tests. It is still recommended to keep an eye on this though, and a great lab test tool for repeat visits is WebPageTest, which has a dedicated option for a direct repeat visit:

{% Img src="image/admin/6eHzqWl6gaKjL39n8Dwh.png", alt="The WebPageTest homepage form for auditing a site. The repeat view option is highlighted.", width="800", height="650" %}
Webpagetest offers options to test first load and repeat load as well

To get a better feeling for repeat visit performance in the field use your analytics package of choice to segment your performance metrics by user type. Here is an example of such a report in Google Analytics:

{% Img src="image/admin/pSxM9xuJKho7RjOn4aDK.png", alt="A Google Analytics dashboard shows a number of fields being added to a custom report.", width="800", height="444" %}
A Google Analytics custom report can be used to report speed metrics for new and returning users.

A report like this will give you page load times for new and returning users as well.

Recap

This guide showed you how to measure first load, flow and repeat load via field and lab tests. Make sure to optimize the different steps of the funnel accordingly to maximize discovery (first load), engagement (navigations and flow) and re-engagement (repeat load).

Why does speed matter?

Consumers increasingly rely on mobile to access digital content and services, and if you look at your site analytics, you'll probably see this story playing out in your own data. Consumers are also more demanding than they've ever been, and when they weigh the experience on your site, they aren't just comparing you with your competitors, they're rating you against the best-in-class services they use every day.

This post rounds up some of the research that has been done on the relationship between performance and business success.

Performance is about retaining users

Performance has directly impacted the company's bottom line.

Pinterest

Performance plays a major role in the success of any online venture. High-performing sites engage and retain users better than low-performing ones.

Pinterest reduced perceived wait times by 40% and this increased search engine traffic and sign-ups by 15%.

COOK reduced average page load time by 850 milliseconds which increased conversions by 7%, decreased bounce rates by 7%, and increased pages per session by 10%.

Studies have also shown the negative impact poor performance can have on business goals. For example, the BBC found they lost an additional 10% of users for every additional second their site took to load.

Performance is about improving conversions

Retaining users is crucial to improving conversions. Slow sites have a negative impact on revenue, and fast sites are shown to increase conversion rates.

For Mobify, every 100ms decrease in homepage load speed worked out to a 1.11% increase in session-based conversion, yielding an average annual revenue increase of nearly $380,000. Additionally, a 100ms decrease in checkout page load speed amounted to a 1.55% increase in session-based conversion, which in turn yielded an average annual revenue increase of nearly $530,000.

When AutoAnything reduced page load time by half, they saw a boost of 12% to 13% in sales.

Retailer Furniture Village audited their site speed and developed a plan to address the problems they found, leading to a 20% reduction in page load time and a 10% increase in conversion rate.

Performance is about user experience

When it comes to user experience, speed matters. A consumer study shows that the stress response to delays in mobile speed are similar to that of watching a horror movie or solving a mathematical problem, and greater than waiting in a checkout line at a retail store.

As a site begins to load, there's a period of time where users wait for content to appear. Until this happens, there's no user experience to speak of. This lack of an experience is fleeting on fast connections. On slower connections, however, users are forced to wait. Users may experience more problems as page resources slowly trickle in.

{% Img src="image/admin/W0ctiX3cMOfWnNF6AQMg.png", alt="A comparison of two filmstrip reels of a page loading. The first shows a page loading on a slow connection, while the second shows the same page loading on a fast connection.", width="800", height="264" %}
A comparison of page load on a very slow connection (top) versus a faster connection (bottom).

Performance is a foundational aspect of good user experiences. When sites ship a lot of code, browsers must use megabytes of the user's data plan to download it. Mobile devices have limited CPU power and memory. They often get overwhelmed with what we might consider a small amount of unoptimized code. This creates poor performance which leads to unresponsiveness. Knowing what we know about human behavior, users will only tolerate low performing applications for so long before abandoning them.

Performance is about people

Poorly performing sites and applications can also pose real costs for the people who use them.

As mobile users continue to make up a larger portion of internet users worldwide, it's important to bear in mind that many of these users access the web through mobile LTE, 4G, 3G, and even 2G networks. As Ben Schwarz of Calibre points out in this study of real world performance, the cost of prepaid data plans is decreasing, which in turn is making access to the internet more affordable in places where it once wasn't. Mobile devices and internet access are no longer luxuries. They are common tools necessary to navigate and function in an increasingly interconnected world.

Total page size has been steadily increasing since at least 2011, and the trend appears to be continuing. As the typical page sends more data, users must replenish their metered data plans more often, which costs them money.

In addition to saving your users money, fast and lightweight user experiences can also prove crucial for users in crisis. Public resources such as hospitals, clinics, and crisis centers have online resources that give users important and specific information that they need during a crisis. While design is pivotal in presenting important information efficiently in stressful moments, the importance of delivering this information fast can't be understated. It's part of our job.

Get started with improving your website speed {: #get-started }

Read up on the Core Web Vitals to learn about the metrics that Google believes all websites should focus on.

Today, we’re building on this work and providing an early look at an upcoming Search ranking change that incorporates these page experience metrics. We will introduce a new signal that combines Core Web Vitals with our existing signals for page experience to provide a holistic picture of the quality of a user’s experience on a web page.

Evaluating page experience for a better web , Official Google Webmaster Central Blog

Then check out Fast load times for lots of tips and tricks related to getting fast and staying fast.

Your first performance budget

When you set a personal, business or family budget, you are setting a limit to your spending and making sure you stay within it. Performance budgets work in the same way, but for metrics that affect website performance.

With a performance budget established and enforced you can be sure that your site will render as quickly as possible. This will provide a better experience for your visitors and positively impact business metrics.

Here's how to define your first performance budget in a few simple steps.

Preliminary analysis

If you are trying to improve the performance of an existing site, start by identifying the most important pages. For example, these could be pages that have the highest amount of user traffic or a product landing page.

After you identify your key pages, it's time to analyze them. First, we'll focus on the timing milestones that best measure the user experience.

Under the Audits panel in Chrome DevTools, you'll find Lighthouse. Run audits on each page in a Guest window to record these two times:

{% Aside %} Using a Guest window gives you a clean testing environment without any Chrome extensions that could interfere with the audit. {% endAside %}

{% Img src="image/admin/VUtkCadH9vjnKSzGzd0S.png", alt="Lighthouse panel in Chrome DevTools", width="800", height="637" %}

Let's take a highly specialized search engine, Doggos.com, as an example. Doggos.com aims to index all dog-related things on the internet, and its most important pages are the home and results pages. Here are the FCP and TTI numbers measured for the site on desktop and mobile.

Desktop analysis of Doggos.com
Desktop FCP TTI
Homepage 1,680 ms 5,550 ms
Results page 2,060 ms 6,690 ms
Mobile analysis of Doggos.com
Mobile FCP TTI
Homepage 1,800 ms 6,150 ms
Results page 1,100 ms 7,870 ms

Competitive analysis

Once you've analyzed your own site, it's time to analyze your competitors' sites. Comparing results from websites similar to yours is a great way to figure out a performance budget. Whether you are working on an established project or starting from scratch, this is an important step. You get competitive advantage when you are faster than your competitors.

If you are not sure which sites to look at, here are a few tools to try:

  1. Google search's "related:" keyword
  2. Alexa's similar sites feature
  3. SimilarWeb

{% Img src="image/admin/EzpGvSgVJYC2y3rsnHRk.png", alt="Screenshot of Google search with the related keyword", width="775", height="336" %}

For a realistic picture, try to find 10 or so competitors.

Budget for timing milestones

Our niche search engine in this example has a handful of competitors and we'll focus on optimizing the homepage for mobile devices. Over half of the internet traffic today happens on mobile networks and using mobile numbers as default will benefit not only your mobile users, but your desktop users as well.

Create a chart with FCP and TTI times for all the similar websites and highlight the fastest in the bunch. A chart like this one gives you a clearer picture of how your website is doing compared to the competition.

Competitive analysis of Doggos.com on 3G network
Site/Homepage FCP TTI
goggles.com 880 ms 3,150 ms
Doggos.com 1,800 ms 6,500 ms
quackquackgo.com 2,680 ms 4,740 ms
ding.xyz 2,420 ms 7,040 ms
{% Img src="image/admin/Mfzr0dmMxHij9KrJraHD.jpg", alt="Doggo at a computer", width="800", height="600" %}
Doggos.com seems to be doing okay on the FCP metric but seriously lagging behind in TTI

There's room for improvement and a good guideline for that is the 20% rule. Research states that users recognize a difference in response times when it's greater than 20%. This means that if you want to be noticeably better than the best comparable site, you have to be at least 20% faster.

Performance budget that would get Doggos.com ahead of the competition
Measure Current time Budget (20% faster than competition)
FCP 1,800 ms 704 ms
TTI 6,500 ms 2,520 ms

If you are trying to optimize an existing site that goal may seem impossible to reach. This is not a sign for you to give up. Start with small steps and set a budget at 20% faster than your current speed. Keep optimizing from there.

For Doggos.com, a revised budget could look like this.

Revised Doggos.com performance budget
Measure Current time Initial budget (20% faster than the current time) Long-term goal (20% faster than competition)
FCP 1,800 ms 1,440 ms 704 ms
TTI 6,500 ms 5,200 ms 2,520 ms

Combine different metrics

A solid performance budget combines different types of metrics. We've already defined the budget for milestone timings and now we'll add two more to the mix:

Budget for quantity-based metrics

Whatever total page weight number you come up with, try to deliver under 170 KB of critical-path resources (compressed/minified). This guarantees your website will be fast even on inexpensive devices and slow 3G.

You can have a bigger budget for the desktop experience, but don't go wild. The median page weight on both desktop and mobile is over 1MB according to the HTTP Archive data for the last year. To get a performant website you have to aim well below these median numbers.

Here are a few examples based on TTI budgets:

Network Device JS Images CSS HTML Fonts Total Time to Interactive budget
Slow 3G Moto G4 100 30 10 10 20 ~170 KB 5s
Slow 4G Moto G4 200 50 35 30 30 ~345 KB 3s
WiFi Desktop 300 250 50 50 100 ~750 KB 2s

{% Aside %} The recommended sizes are for the critical-path resources. {% endAside %}

Defining a budget based on quantity metrics is a tricky business. An e-commerce website with loads of product photos is very different from a news portal which is mostly text. If you have ads or analytics on your site, that increases the amount of JavaScript you're shipping.

Use the table above as a starting point and adjust based on the type of content you are working with. Define what your pages will include, review your research and take an educated guess for individual asset sizes. For example, if you are building a website with a lot of images, put stricter limits to JS size.

Once you have a working website, check how you are doing on user-centric performance metrics and adjust your budget.

Budget for rule-based metrics

Very effective rule-based metrics are Lighthouse scores. Lighthouse grades your app in 5 categories and one of those is performance. Performance scores are calculated based on 5 different metrics, including First Contentful Paint and Time to Interactive.

When you try to build a great site, set Lighthouse performance score budget to at least 85 (out of 100). Use Lighthouse CI to enforce it on pull-requests.

Prioritize

Ask yourself what level of interaction you expect on your site. If it's a news website, users' primary goal is to read content so you should focus on rendering quickly and keeping FCP low. Doggos.com visitors want to click on relevant links as soon as possible, so the top priority is low TTI.

Find out exactly what part of your audience browses on desktop vs. on mobile devices and prioritize accordingly. One way to figure this out is to check what your audience is doing on competitors' websites, through the Chrome User Experience report dashboard.

{% Img src="image/admin/ycZwOrFNzjdjquriM9rJ.png", alt="Device distribution data from Chrome User Experience report", width="800", height="530" %}
Device distribution data from Chrome User Experience report

Next steps

Make sure your performance budget is enforced throughout the project and incorporate it into your build process.