Fast
- index
- Adaptive serving based on network quality
- Apply instant loading with the PRPL pattern
- Avoid invisible text during font loading
- Browser-level image lazy loading for the web
- Choose the right image format
- Using the Chrome UX Report to look at performance in the field
- Adapt video to image serving based on network quality
- Art direction
- Avoid flash of invisible text
- Reduce JavaScript payloads with code splitting
- Use density descriptors
- Extract and inline critical CSS with Critical
- Using Imagemin with Grunt
- Using Imagemin with Gulp
- Using Imagemin with webpack
- Optimize third-party JavaScript
- Codelab: Preload critical assets to improve loading speed
- Preload web fonts to improve loading speed
- Remove unused code
- Replace GIFs with video
- Serve images with correct dimensions
- Creating WebP Images with the Command Line
- Serve modern code to modern browsers for faster page loads
- Setting performance budgets with webpack
- Specifying multiple slot widths
- Minify and compress network payloads with gzip
- Minify and compress network payloads with brotli
- Two ways to prefetch: `<link>` tags and HTTP headers
- Lazy load offscreen images with lazysizes
- Choose the correct level of compression
- Content delivery networks (CDNs)
- Best practices for cookie notices
- Debug layout shifts
- Defer non-critical CSS
- Efficiently load third-party JavaScript
- Extract critical CSS
- Optimizing resource loading with the Fetch Priority API
- Fixing website speed cross-functionally
- Best practices for fonts
- How can performance improve conversion?
- How to measure speed?
- How to report metrics and build a performance culture
- How to stay fast?
- Identify slow third-party JavaScript
- Use image CDNs to optimize images
- Incorporate performance budgets into your build process
- Use lazy loading to improve loading speed
- Lazy loading best practices
- Lazy loading images
- Lazy loading video
- Minify CSS
- Assessing loading performance in the field with Navigation Timing and Resource Timing
- Optimize CSS background images with media queries
- Optimize WebFont loading and rendering
- Performance budgets 101
- Preload critical assets to improve loading speed
- Prioritize resources
- Publish, ship, and install modern JavaScript for faster applications
- Measure performance with the RAIL model
- Reduce JavaScript payloads with code splitting
- Reduce JavaScript payloads with tree shaking
- Minify and compress network payloads
- Reduce web font size
- Remove unused code
- Replace animated GIFs with video for faster page loads
- Use WebP images
- Serve images with correct dimensions
- Serve modern code to modern browsers for faster page loads
- Serve responsive images
- Signed Exchanges (SXGs)
- How to set up Signed Exchanges using Web Packager
- Third-party JavaScript performance
- Use Imagemin to compress images
- Use Lighthouse for performance budgets
- Using bundlesize with Travis CI
- Using Lighthouse Bot to set a performance budget
- The value of speed
- What is speed?
- What should you measure to improve performance?
- Why does speed matter?
- Your first performance budget
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:
- Switch between serving high-definition and low-definition content based on the user's network.
- Decide whether to preload resources.
- Defer uploads and downloads when users are on a slow connection.
- Enable offline mode if the network quality is not good enough to load the app and use the features.
- Warn users that doing something (for example, watching video) over cellular could cost them money.
- Use it in your analytics to gather data on your users' network quality.
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)".
How it works
| 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:
- Preload the most important resources.
- Render the initial route as soon as possible.
- Pre-cache remaining assets.
- Lazy load other routes and non-critical assets.
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' %}
- Select the Performance and Progressive Web App checkboxes.
- 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:
<link rel="preload" as="style" href="css/style.css">
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.
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.
- Preload critical resources.
- Render the initial route as soon as possible.
- Pre-cache remaining assets.
- Lazy load other routes and non-critical assets.
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:
- Don't use a custom font on initial page load. This ensures that the browser displays text immediately using a system font.
- Detect when your custom font is loaded. This can be accomplished with a couple lines of JavaScript code, thanks to the FontFaceObserver library.
- Update page styling to use the custom font.
Here are the changes you can expect to make to implement this:
- Refactor your CSS to not use a custom font on initial page load.
- Add a script to your page. This script detects when the custom font is loaded and then will update the page styling.
{% 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:
- Using the Intersection Observer API
- Using
scroll,resize, ororientationchangeevent handlers
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:
lazy: Defer loading of the resource until it reaches a calculated distance from the viewport.eager: Default loading behavior of the browser, which is the same as not including the attribute and means the image is loaded regardless of where it's located on the page. While this is the default, it can be useful to explicitly set this if your tooling automatically addsloading="lazy"if there is no explicit value, or if your linter complains if it is not explicitly set.
{% 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:
- The type of image resource being fetched
- The effective connection type
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:
<img loading=lazy>behaves closer to the experience offered by JavaScript lazy loading libraries.- The new distance-from-viewport thresholds still allow us to guarantee images have probably loaded by the time a user has scrolled to them.
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:
and the new thresholds vs. LazySizes (a popular JS lazy loading library):
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.
It is recommended to only add loading=lazy to images which are positioned below the fold, if possible. Images that are eagerly loaded can be fetched right away, while images which are loaded lazily the browser currently needs to wait until it knows where the image is positioned on the page, which relies on the IntersectionObserver to be available.
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:
- Replace
<img src>with<img data-src>to avoid an eager load in unsupported browsers. If theloadingattribute is supported, swapdata-srcforsrc. - If
loadingis not supported, load a fallback (lazysizes) and initiate it. As per lazysizes docs, you use thelazyloadclass as a way to indicate to lazysizes which images to lazy-load.
<!-- 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:
- CSS effects (such as shadows or gradients) and CSS animations can be used to produce resolution-independent assets that always look sharp at every resolution and zoom level, often at a fraction of the bytes required by an image file.
- Web fonts enable use of beautiful typefaces while preserving the ability to select, search, and resize text—a significant improvement in usability.
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.
- Vector graphics use lines, points, and polygons to represent an image.
- Raster graphics represent an image by encoding the individual values of each pixel within a rectangular grid.
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.
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:
- Prefer vector images whenever possible as they are resolution-independent and always deliver sharp results.
- If a raster image is required, serve responsive images.
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:
- 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.
- What about GIF? GIF limits the color palette to at most 256 colors,
and creates significantly larger file sizes than
- 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.
- 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.
- In
script.js, add anifstatement that tests whether thenavigator.connectionobject exists and whether it has theeffectiveTypeproperty. - Add an
ifstatement to check theeffectiveTypeof 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' %}
- 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' %}
- 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' %}
- Reload the app using different browser sizes. Notice how different the images are: they're not only different sizes but also different croppings and aspect ratios.
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
- View
index.htmlto see the art direction code for this demo.
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' %}
- Be careful when ordering source tags. The browser uses the first
<source>tag with a matching media query, even if subsequent<source>tags also have matching media queries. - The value of the
srcsetattribute is an image filepath. - Use images that are appropriately sized. Just because art direction is used for aesthetic purposes, doesn't mean that it shouldn't be performant too. {% endAside %}
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' %}
- The
<img>tag should always be included, and it should always be listed last, after all<source>tags. - The resource specified by the
<img>tag should be a size that works on all devices, so it can be used as a fallback. {% endAside %}
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' %}
- Add a script tag for
fontfaceobserver.jstoindex.html:
<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.
- Add the following script after the
fontfaceobserver.jsscript. This creates observers for the "Pacifico" and "Roboto" font families:
<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.
- Add this promise to your script, right below the FontFaceObservers that you just declared:
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
- Replace the
/* Do things */comment in the script with this line:
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.
- Update the CSS:
.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:
- Write a custom sorting method instead of importing a third-party library
- Use the built in
Array.prototype.sort()method to sort numerically - Only import the
sortBymethod fromlodashand not the entire library - 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:
- One responsible for the code that makes up our initial route
- A secondary chunk that contains our sorting code
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" %}
{% 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
Many popular frameworks and libraries that use webpack provide abstractions to make lazy loading easier than using dynamic imports in the middle of your application.
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' %}
- Reload the page using different devices to see the browser load different images.
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 |
- Checkout
index.htmlfor the code that makes this work.
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 %}
-
Determine the pixel density of a device by typing
window.devicePixelRatioin the console. -
View this table to see the pixel ratios of common devices. Most are between 1.0 and 4.0.
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:
- The pixel dimensions listed in image editors, file directories, and other places are a measurement of logical pixels.
- For higher resolution screens and larger displays you'll need images with larger dimensions. Merely enlarging smaller images defeats the purpose of serving multiple image versions. The browser would have done this anyway if a high resolution image was not provided.
{% 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.
- Take a look at the
index.htmlfile and note the<img>element.
<img src="flower.jpg"
srcset="flower-1x.jpg 1x,
flower-2x.jpg 2x,
flower-3x.jpg 3x">
This example put into words:
1x,2x, and3xare all density descriptors that tell the browser the pixel density that an image is intended for. This saves the browser from needing to download an image to determine this information.- The browser can choose between three images:
flower-1x.jpg(intended for browsers with a1.0pixel density),flower-2x.jpg(intended for browsers with a2.0pixel density), andflower-3x.jpg(intended for browsers with a3.0pixel density). flower.jpgis the fallback image for browsers that do not supportsrcset.
How to use this:
- Use a devicePixelRatio and the
xunit to write density descriptors. For example, the density descriptor for many Retina screens (window.devicePixelRatio = 2) would be written as2x. - If a density descriptor isn't provided, it is assumed to be
1x. - Including density descriptors in filenames is a common convention (and will help you keep track of files) but is not necessary. Images can have any filename.
- There is no need to include a
sizesattribute. Thesizesattribute is only used with width descriptors.
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' %}
- Click Mobile.
- Select the Performance checkbox.
- Clear the rest of the checkboxes in the Audits section.
- Click Simulated Fast 3G, 4x CPU Slowdown.
- 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.
- 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
{% 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.
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' %}
- In
gruntfile.js, replace the//Add configuration herecomment with this code block:
imagemin: {
dynamic: {
files: [{
cwd: 'images/',
expand: true,
src: ['**/*.{png,jpg}'],
}]
}
}
This code block tells Grunt which files should be compressed with Imagemin.
-
dynamicindicates that the list of files to compress will be dynamically generated by matching the filenames against the specified file pattern. -
The file pattern
{cwd: 'images/', expand: true, src: ['**/*.{png,jpg}']}will match all the JPEG and PNG images in theimages/directory.
{% Aside %} Have more questions about this? You can read more about the Grunt file object here. {% endAside %}
- Load the Imagemin task by adding this line immediately before
grunt.registerTask(...):
grunt.loadNpmTasks('grunt-contrib-imagemin');
grunt.registerTask('default', [/* list plugins here */]);
- Lastly, add Imagemin as the default Grunt task by replacing the
/* list plugins here */comment with'imagemin'. That line should now look like this:
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.
- Declare the
imagemin-pngquantplugin by adding this line to the top of yourgruntfile.js:
const pngquant = require('imagemin-pngquant');
const grunt = require('grunt')
grunt.initConfig({
...
- Add settings for compressing PNG images by adding an
optionsproperty to theimageminobject. Thatoptionsproperty should look like this:
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.
- Declare the
imagemin-mozjpegplugin by putting this line at the top yourgruntfile.js.
const mozjpeg = require('imagemin-mozjpeg');
const pngquant = require('imagemin-pngquant');
const grunt = require('grunt');
- Next, add
mozjpeg({quality: 50})to the array in theoptionsobject. That array should now look like this:
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
- Click the Tools button.
- Then click the Console button.
- 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' %}
- Run the Lighthouse performance audit (Lighthouse > Options > Performance) on the live version of your Glitch and verify that the "Efficiently encode images" audit was passed.
{% 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' %}
- First, initialize the
gulp-imageminplugin by adding this code at the top ofgulpfile.js:
const imagemin = require('gulp-imagemin');
- Next, replace the
//Add tasks herecomment ingulpfile.jswith this code block:
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
- Click the Tools button.
- Then click the Console button.
- 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.
- Declare the
imagemin-pngquantplugin by adding this line to the top of yourgulpfile.js:
const pngquant = require('imagemin-pngquant');
- Add the
imagemin-pngquantplugin (and its settings) by passing the following array toImageminPlugin():
[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.
- Declare the
imagemin-mozjpegplugin by putting this line at the top yourgulpfile.js.
const mozjpeg = require('imagemin-mozjpeg');
- Next, add
mozjpeg({quality: 50})to the array that's passed toImageminPlugin():
[
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
- Re-run Gulp:
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' %}
- Run the Lighthouse performance audit (Lighthouse > Options > Performance) on the live version of your Glitch and verify that the "Efficiently encode images" audit was passed.
{% 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 %}
- First, declare the Imagemin plugin by adding this code at the top of
webpack.config.js:
const ImageminPlugin = require('imagemin-webpack-plugin').default;
- Next, add the following code as the last item in the
plugins[]array. This adds Imagemin to the list of plugins that webpack uses:
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' %}
- 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?
- Re-run webpack, but this time 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:
- Initialize the
imagemin-mozjpegplugin by adding the following line at the top of yourwebpack.config.jsfile:
const imageminMozjpeg = require('imagemin-mozjpeg');
- Add a
pluginsproperty to the object passed toImageminPlugin(), such that the object now looks like this:
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
- In the console, re-run webpack:
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' %}
- Run the Lighthouse performance audit (Lighthouse > Options > Performance) on the live version of your Glitch and verify that the Efficiently encode images audit was passed.
{% 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:
-
Deferring script loading
-
Lazy loading non-critical resources
-
Preconnecting to required origins
The included sample app features a simple web page with three features coming from third-party sources:
-
A video embed
-
A data-visualization library for rendering a line graph
-
A social media sharing widget
You'll start by measuring the performance of the app and then apply each technique to improve different aspects of app performance.
Measure performance
First open the sample app in the fullscreen view: {% Instruction 'remix', 'ol' %} {% Instruction 'preview', 'ol' %}
Run a Lighthouse performance audit on the page to establish baseline performance:
{% Instruction 'devtools-lighthouse', 'ol' %}
- Click Mobile.
- Select the Performance checkbox. (You can clear the rest of the checkboxes in the Audits section.)
- Click Simulated Fast 3G, 4x CPU Slowdown.
- Select the Clear Storage checkbox.
- Click Run audits.
When you run an audit on your machine, the exact results may vary, but you should notice that the First Contentful Paint (FCP) time is pretty high, and that Lighthouse suggests two opportunities to investigate: Eliminate render-blocking resources and Preconnect to required origins. (Even if the metrics are all in the green, optimizations will still yield improvements.)
Defer third-party JavaScript
The Eliminate render-blocking resources audit identified that you can save some time by deferring a script coming from d3js.org:
D3.js is a JavaScript library for creating data visualizations. The script.js file in the sample app uses D3 utility functions to create the SVG line chart and append it to the page. The order of operations here matters: script.js has to run after the document is parsed and the D3 library has loaded, which is why it's included right before the closing </body> tag in index.html.
However, the D3 script is included in the page's <head>, which blocks the parsing of the rest document:
Two magic attributes can unblock the parser when added to the script tag:
-
asyncensures that scripts download in the background and execute at the first opportunity after they finish downloading. -
deferensures that scripts download in the background and execute after parsing is completely finished.
Since this chart is not really critical to the overall page and will most likely be below the fold, use defer to make sure there's no parser blocking.
Step 1: Load the script asynchronously with the defer attribute
On line 17 in index.html, add the defer attribute to the <script> element:
<script src="https://d3js.org/d3.v3.min.js" defer></script>
Step 2: Ensure the correct order of operations
Now that D3 is deferred, script.js will run before D3 is ready, resulting in an error.
Scripts with the defer attribute execute in the order in which they were specified. To ensure script.js gets executed after D3 is ready, add defer to it and move it up to the <head> of the document, right after the D3 <script> element. Now it no longer blocks the parser, and the download starts sooner.
<script src="https://d3js.org/d3.v3.min.js" defer></script>
<script src="./script.js" defer></script>
Lazy-load third-party resources
All resources that are below the fold are good candidates for lazy loading.
The sample app has a YouTube video embedded in an iframe. To check out how many requests the page makes and which come from the embedded YouTube iframe:
{% Instruction 'preview', 'ol' %} {% Instruction 'devtools-network', 'ol' %} {% Instruction 'disable-cache', 'ol' %}
- 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:
-
In the Network panel, right-click a column title.
-
In the dropdown menu, select the Domains column.
-
To sort the requests by domain, click the Domains column title.
The new sorting reveals that there are additional requests to Google domains. In total, the YouTube iframe makes 14 requests for scripts, stylesheets, images, and fonts. But unless users actually scroll down to play the video, they don't really need all those assets.
By waiting to lazy-load the video until a user scrolls down to that section of the page, you cut the number of requests the page initially makes. This approach saves users' data and speeds up the initial load.
One way to implement lazy loading is by using the Intersection Observer, a browser API that notifies you when an element enters or exits the browser's viewport.
Step 1: Prevent video from loading initially
To lazy-load the video iframe, you must first prevent it from loading in the usual way. Do that by replacing the src attribute with the data-src attribute to specify the video URL:
<iframe width="560" height="315" data-src="https://www.youtube.com/embed/lS9D6w1GzGY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
data-src is a data attribute, which allows you to store extra information on standard HTML elements. A data attribute can be named anything, as long as it starts with "data-".
An iframe without a src simply won't load.
Step 2: Use Intersection Observer to lazy-load the video
To load the video when a user scrolls to it you need to know when that happens. That's where the Intersection Observer API steps in. The Intersection Observer API lets you register a callback function that's executed whenever an element you want to track enters or exits the viewport.
To get started, create a new file and name it lazy-load.js:
{% 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>:
<link rel="preconnect" href="https://staticxx.facebook.com">
Reevaluate performance
Here's the state of the page after optimization. Follow the steps from the Measure performance section of the codelab to run another Lighthouse audit.
{% 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:
{% Instruction 'devtools-network' %}
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.
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.
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.
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.
Reload the application and notice how a very low priority request is made for
details.css after all the other files have been fetched.
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.
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.
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.
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.
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:
- Use preload for resources that are discovered late but are critical to the current page.
- Use prefetch for resources that are needed for a future navigation route or user action.
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 resource 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.
- Click on the Console tab.
- Make sure that
Warningsis enabled in the levels dropdown next to theFilterinput.
{% Img src="image/admin/DMdUUxzxNMEGZ6HsTd3P.png", alt="Warnings filter", width="800", height="244", class="screenshot" %}
- Take a look at the displayed warning.
{% 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:
- Firebase: a platform that provides a number of useful services for iOS, Android or web applications. Here its Realtime Database is used to store and sync the information for each kitten in real time.
- Moment.js: a utility library that makes it easier to
handle dates in JavaScript. The birth date of each kitten is stored in the
Firebase database, and
momentis used to calculate its age in weeks.
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:
firestoreauthstoragemessagingfunctions
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:
- Remove
BundleAnalyzerPluginin the list of plugins:
plugins: [
//...
new BundleAnalyzerPlugin()
];
- And now remove the unused import from the top of the file:
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.
Many other popular libraries, such as lodash, also allow developers to
selectively import different parts of their packages. Without doing much work,
updating library imports in an application to only include what is being used
can result in significant performance improvements.
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' %}
- In the console, run:
which ffmpeg
You should get a file path back:
/usr/bin/ffmpeg
Change GIF to video
- In the console, run
cd imagesto enter the images directory. - Run
lsto see the contents.
You should see something like this:
$ ls
cat-herd.gif
- In the console, run:
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.
- In the console, run:
ffmpeg -i cat-herd.gif -c vp9 -b:v 0 -crf 41 cat-herd.webm
- To check the file sizes run:
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:
- They play automatically.
- They loop continuously (usually, but it is possible to prevent looping).
- They're silent.
Luckily, you can recreate these behaviors using the <video> element.
- In the
index.htmlfile, replace the line with the<img>with:
<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' %}
- 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.
- Inspect
flower_logo.pngin the DevTools Elements panel.
{% 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' %}
- 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.
- Inspect
flower_photo.jpgin the DevTools elements panel.
{% 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.
- Use ImageMagick to resize the image to 960 pixels wide. In the terminal type:
# 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
- Re-run the Lighthouse Performance audit to verify that you have successfully re-sized the images.
{% 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' %}
- 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.
- Run this script in the console (don't forget the backticks):
`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.
- In
index.htmlreplace<img src="images/flower1.jpg"/>with the following HTML:
<picture>
<source type="image/webp" srcset="images/flower1.webp">
<source type="image/jpeg" srcset="images/flower1.jpg">
<img src="images/flower1.jpg">
</picture>
- Next, replace the
<img>tags forflower2.jpgandflower3.pngwith<picture>tags.
✔︎ 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' %}
- 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" %}
-
Enter
Show Coverageand hitEnterto display the Coverage tab. -
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" %}
-
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:
- Polyfills are included to emulate newer ES2015+ functions so that their APIs
can be used even if it is not supported by the browser. Here is an example of a
polyfill
of the
Array.includesmethod. - Plugins are used to transform ES2015 code (or later) into older ES5 syntax. Since these are syntax related changes (such as arrow functions), they cannot be emulated with polyfills.
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",
//...
}
@babel/coreis the core Babel compiler. With this, all the Babel configurations are defined in a.babelrcat the root of the project.babel-loaderincludes Babel in the webpack build process.
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"
}
]
},
@babel/polyfillprovides all the necessary polyfills for any newer ECMAScript features so that they can work in environments that do not support them. It is already imported at the very top ofsrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-envidentifies which transforms and polyfills are necessary for any browsers or environments chosen as targets.
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
}
]
]
}
- Click Tools.
- Click Logs.
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:
- A version that would work in newer browsers that support modules and that includes a module that is largely untranspiled but has a smaller file size
- A version that includes a larger, transpiled script that would work in any legacy 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' %}
- To get a color-coded list of assets and their sizes, type
webpackin 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 %}
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).
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.
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:
- 100000 bytes for individual assets (maxAssetSize)
- 100000 bytes for the entry-point (maxEntrypointSize)
In this case, there's only one bundle which also acts as the entry point.
Possible values for hints are:
warning(default): Displays a yellow warning message, but the build passes. It's best to use this in development environments.error: Displays a red error message, but the build still passes. This setting is recommended for production builds.false: No warnings or errors are shown.
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.🎉
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' %}
- Reload the app in differently sized browser windows to see the browser load different images and use different layouts at different browser sizes.
View the code
- Checkout
index.htmlfor the code that makes this work:
<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
-
Use a comma-separated list to specify multiple slot widths. Each list item, except for the last item, consists of a media condition (e.g.
max-widthormin-width) and a slot width. -
The last item in this list is the default slot width. It is the default, so you do not need to specify a media condition.
-
You can list as many slot widths as you want - the number of images listed in
srcsetdoes not matter. -
Slot width can be specified using a variety of units. The following are all valid widths:
-
100px -
33vw -
20em -
calc(50vw-10px)
The following is not a valid width:
25%(percentages cannot be used with the sizes attribute)
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:
- If webpack v4 or greater is used, no additional work needs to be done since code is minified by default in production mode. 👍
- If an older version of webpack is used, install and include
TerserWebpackPlugininto the webpack build process. The documentation explains this in detail. - Other minification plugins also exist and can be used instead, such as BabelMinifyWebpackPlugin and ClosureCompilerPlugin.
- If a module bundler is not being used at all, use Terser as a CLI tool or include it directly as a dependency.
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:
- General represents general headers relevant to the entire request-response interaction.
- Response Headers shows a list of headers specific to the actual response from the server.
- Request Headers shows a list of headers attached to the request by the client.
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:
- Gzip (
gzip): The most widely used compression format for server and client interactions. It builds on top of the Deflate algorithm and is supported in all current browsers. - Deflate (
deflate): Not commonly used. - Brotli (
br): A newer compression algorithm that aims to further improve compression ratios, which can result in even faster page loads. It is supported in the latest versions of most browsers.
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
- Creating and updating saved compressed versions of assets does not need to be done.
- Compressing on-the-fly works especially well for web pages that are dynamically generated.
Cons
- Compressing files at higher levels to achieve better compression ratios takes longer. This can cause a performance hit as the user waits for assets to compress before they are sent by the server.
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
- Latency due to high compression levels is not a concern anymore. Nothing needs to happen on-the-fly to compress files as they can now be fetched directly.
Cons
- Assets need to compressed with every build. Build times can increase significantly if high compression levels are used.
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 %}
- Click the Tools button.
- Click the Console button.
- In the console, run the following commands to change into the
publicdirectory and see all of its files:
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:
- Specifying
'*.js'as the first argument means that this works for every endpoint that is fired to fetch a JS file. - Within the callback,
.gzis attached to the URL of the request and theContent-Encodingresponse header is set togzip. - Finally,
next()ensures that the sequence continues to any callback that may be next.
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:
- 14% smaller than
gzipfor JavaScript - 21% smaller than
gzipfor HTML - 17% smaller than
gzipfor CSS
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):
Enabling Brotli
Dynamic compression
Dynamic compression involves compressing assets on-the-fly as they get requested by the browser.
Pros
- Creating and updating saved compressed versions of assets does not need to be done.
- Compressing on-the-fly works especially well for web pages that are dynamically generated.
Cons
- Compressing files at higher levels to achieve better compression ratios takes longer. This can cause a performance hit as the user waits for assets to compress before they are sent by the server.
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:
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
- Latency due to high compression levels is not a concern anymore. Nothing needs to happen on-the-fly to compress files as they can now be fetched directly.
Cons
- Assets need to compressed with every build. Build times can increase significantly if high compression levels are used.
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:
asset: The target asset name.[file]is replaced with the original asset file name.test: All assets that match this RegExp (that is, JavaScript assets ending in.js) are processed.
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.
- Click the Tools button.
- Click the Console button.
- In the console, run the following commands to change into the
publicdirectory and see all of its files:
cd public
ls -lh
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:
- Specifying
'*.js'as the first argument means that this works for every endpoint that is fired to fetch a JS file. - Within the callback,
.bris attached to the URL of the request and theContent-Encodingresponse header is set tobr. - The
Content-Typeheader is set toapplication/javascript; charset=UTF-8to specify the MIME type. - Finally,
next()ensures that the sequence continues to any callback that may be next.
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.
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' %}
- In the Throttling drop-down list, select Fast 3G to simulate a slow connection type.
- 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">
- Add the following
<link>element to the head of theviews/index.htmlfile:
<!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' %}
- In the Throttling drop-down list, select Fast 3G to simulate a slow connection type.
- 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' %}
- 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:
- The Size column shows "prefetch cache", which means this resource was retrieved from the browser's cache rather than the network.
- The Time column shows that the time it takes for the document to load is now about 10 ms.
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:
- It checks the effectiveType property of the Network Information API to determine if the user is on a 4G (or faster) connection.
- If that condition is fulfilled, it generates a
<link>tag withprefetchas the type of hint, passes the URL that will be prefetched in thehrefattribute, and indicates that the resource is an HTMLdocumentin theasattribute. - Finally, it injects the script dynamically in the
headof the page.
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' %}
- 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:
- 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
The HTTP Link header can be used to prefetch the same type of resources as the link tag. Deciding when to use one or the other mostly depends on your preference, as the difference in performance is insignificant. In this case, you'll use it to prefetch the main CSS of the product page, to further improve its rendering.
Add an HTTP Link header for style-product.css in the server response for the landing page:
- Open the
server.jsfile and look for theget()handler for the root url:/. - 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" %}
{% 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:
- Add the following
<script>tag toindex.html:
<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
- Add the class
lazyloadto images that should be lazy loaded. In addition, change thesrcattribute todata-src.
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' %}
- Open the console and find the images that were just added. Their classes
should change from
lazyloadtolazyloadedas you scroll down the page.
{% Img src="image/admin/yXej5KAOMzoqoQAB2paq.png", alt="Images being lazy loaded", width="428", height="252" %}
- Watch the network panel to see the image files load individually as you scroll down the page.
{% 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' %}
- 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:
- 100x100 pixel image is composed of 10,000 pixels
- 10,000 pixels x 4 bytes = 40,000 bytes
- 40,000 bytes / 1024 = 39 KB
{% 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!
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:
- Image is processed with a lossy filter that eliminates some pixel data.
- 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:
- Prefer vector formats: vector images are resolution and scale independent, which makes them a perfect fit for the multi-device and high-resolution world.
- Minify and compress SVG assets: XML markup produced by most drawing applications often contains unnecessary metadata which can be removed; Ensure that your servers are configured to apply GZIP compression for SVG assets.
- Prefer WebP or AVIF over older raster formats: WebP and AVIF images will usually be far smaller than older image formats.
- Pick best raster image format: determine your functional requirements and select the one that suits each particular asset.
- Experiment with optimal quality settings for raster formats: don't be afraid to dial down the "quality" settings, the results are often very good and byte savings are significant.
- Remove unnecessary image metadata: many raster images contain unnecessary metadata about the asset: geo information, camera information, and so on. Use appropriate tools to strip this data.
- Serve scaled images: resize images and ensure that the "display" size is as close as possible to the "natural" size of the image. Pay close attention to large images in particular, as they account for largest overhead when resized!
- Automate, automate, automate: invest into automated tools and infrastructure that will ensure that all of your image assets are always optimized.
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.
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.
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.
-
Cache eviction
Caches have a finite storage capacity. When a cache nears its capacity, it makes room for new resources by removing resources that haven't been accessed recently, or which take up a lot of space. This process is known as cache eviction. A resource being evicted from one cache does not necessarily mean that it has been evicted from all caches in a CDN network.
-
Purging
Purging (also known as "cache invalidation") is a mechanism for removing a resource from a CDN's caches without having to wait for it to expire or be evicted. It is typically executed via an API. Purging is critical in situations where content needs to be retracted (for example, correcting typos, pricing errors, or incorrect news articles). On top of that, it can also play a crucial role in a site's caching strategy.
If a CDN supports near instant purging, purging can be used as a mechanism for managing the caching of dynamic content: cache dynamic content using a long TTL, then purge the resource whenever it is updated. In this way, it is possible to maximize the caching duration of a dynamic resource, despite not knowing in advance when the resource will change. This technique is sometimes referred to as "hold-till-told caching".
When purging is used at scale it is typically used in conjunction with a concept known as "cache tags" or "surrogate cache keys". This mechanism allows site owners to associate one or more additional identifiers (sometimes referred to as "tags") with a cached resource. These tags can then be used to carry out highly granular purging. For example, you might add a "footer" tag to all resources (for example,
/about,/blog) that contain your site footer. When the footer is updated, instruct your CDN to purge all resources associated with the "footer" tag.
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
-
Private Resources
Private resources contain data intended for a single user and therefore should not be cached by a CDN. Private resources are indicated by the
Cache-Control: privateheader. -
Public Resources
Public resources do not contain user-specific information and therefore are cacheable by a CDN. A resource may be considered cacheable by a CDN if it does not have a
Cache-Control: no-storeorCache-Control: privateheader. The length of time that a public resource can be cached depends on how frequently the asset changes.
Dynamic and static content
-
Dynamic content
Dynamic content is content that changes frequently. An API response and a store homepage are examples of this content type. However, the fact that this content changes frequently doesn't necessarily preclude it from being cached. During periods of heavy traffic, caching these responses for very short periods of time (for example, 5 seconds) can significantly reduce the load on the origin server, while having minimal impact on data freshness.
-
Static content
Static content changes infrequently, if ever. Images, videos, and versioned libraries are typically examples of this content type. Because static content does not change, it should be cached with a long Time to Live (TTL) - for example, 6 months or 1 year.
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:
Cache-Control: max-age=Cache-Control: s-maxage=Expires
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:
-
Unnecessary query params
By default, a CDN would cache
example.com/blogandexample.com/blog?referral_id=2zjkseparately even though they are likely the same underlying resource. This is fixed by adjusting a CDN's configuration to ignore thereferral\_idquery param. -
Query param order
A CDN will cache
example.com/blog?id=123&query=dogsseparately fromexample.com/blog?query=dogs&id=123. For most sites, query param order does not matter, so configuring the CDN to sort the query params (thereby normalizing the URL used to cache the server response) will increase CHR.
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
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%.
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.
-
Multiplexing
Multiplexing is arguably the most important feature of HTTP/2. Multiplexing enables a single TCP connection to serve multiple request-response pairs at the same time. This eliminates the overhead of unnecessary connection setups; given that the number of connections that a browser can have open at a given time is limited, this also has the implication that the browser is now able to request more of a page's resources in parallel. Multiplexing theoretically removes the need for HTTP/1 optimizations like concatenation and sprite sheets - however, in practice, these techniques will remain relevant given that larger files compress better.
-
Stream prioritization
Multiplexing enables multiple concurrent streams; stream prioritization provides an interface for communicating relative priority of each of these streams. This helps the server to send the most important resources first - even if they weren't requested first.
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.
-
Elimination of head-of-line blocking
HTTP/2 introduced multiplexing, a feature that allows a single connection to be used to transmit multiple streams of data simultaneously. However, with HTTP/2, a single dropped packet blocks all streams on a connection (a phenomena known as a head-of-line blocking). With HTTP/3, a dropped packet only blocks a single stream. This improvement is largely the result of HTTP/3 using UDP (HTTP/3 uses UDP via QUIC) rather than TCP. This makes HTTP/3 particularly useful for data transfer over congested or lossy networks.
-
Reduced connection setup time
HTTP/3 uses TLS 1.3 and therefore shares its performance benefits: establishing a new connection only requires a single round-trip and resuming an existing connection does not require any roundtrips.
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
- Use a CDN: CDNs deliver resources quickly, reduce load on the origin server, and are helpful for dealing with traffic spikes.
- Cache content as aggressively as possible: Both static and dynamic content can and should be cached - albeit for varying durations. Periodically audit your site to make sure that you are optimally cacheing content.
- Enable CDN performance features: Features like Brotli, TLS 1.3, HTTP/2, and HTTP/3 further improve performance.
Best practices for cookie notices
This article discusses how cookie notices can affect performance, performance measurement, and user experience.
Performance
-
Largest Contentful Paint (LCP): Most cookie consent notices are fairly small and therefore typically don't contain a page's LCP element. However, this can happen—particularly on mobile devices. On mobile devices, a cookie notice typically takes up a larger portion of the screen. This usually occurs when a cookie notice contains a large block of text (text blocks can be LCP elements too).
-
First Input Delay (FID): Generally speaking, your cookie consent solution in and of itself should have a minimal impact on FID—cookie consent requires little JavaScript execution. However, the technologies that these cookies enable—namely advertising and tracking scripts—may have a significant impact on page interactivity. Delaying these scripts until cookie acceptance can serve as a technique to decrease First Input Delay (FID).
-
Cumulative Layout Shift (CLS): Cookie consent notices are a very common source of layout shifts.
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.
Load cookie notices scripts asynchronously
<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 %}
Load cookie notice scripts directly
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.
<link rel="preconnect" href="https://cdn.cookie-notice.com/">
{% 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 %}
Preload cookie notices as appropriate
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.
<link rel="preload" href="https://www.cookie-notice.com/cookie-script.js">
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:
- Caching and serving third-party cookie notice scripts from your own servers can improve the delivery speed of these resources.
- Using service workers can allow you more control over the fetching and caching of third-party scripts such as 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.
Testing cookie notices with WebPageTest
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:
combineStepstells WebPageTest to "combine" the results of the scripting steps that follow into a single set of traces and measurements. Running this script withoutcombineStepscan also be useful—separate traces make it easy to see whether resources were loaded before or after cookie acceptance.%URL%is a WebPageTest convention that refers to the URL that is being tested.clickAndWaittells WebPageTest to click on the element indicated byattribute=valueand wait for the subsequent browser activity to complete. It follows the formatclickAndWait attribute=Value.
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" %}
Testing cookie notices with Lighthouse
DevTools
{% 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:
- UX: Is this a good user experience? How will this particular design affect existing page elements and user flows?
- Business: What is your site's cookie strategy? What are your goals for the cookie notice?
- Legal: Does this comply with legal requirements?
- Engineering: How much work would this be to implement and maintain? How difficult would it be to change?
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" %}
-
UX: Controls for configuring cookie usage are most commonly displayed using a separate modal that is launched when the user responds to the initial cookie consent notice. However, if space permits, some sites will display these controls inline within the initial cookie consent notice.
-
Granularity: The most common approach to cookie configurability is to allow users to opt-in to cookies by cookie "category". Examples of common cookie categories include functional, targeting, and social media cookies.
However, some sites will go a step further and allow users to opt-in on a per-cookie basis. Alternatively, another way of providing users with more specific controls is to break down cookie categories like "advertising" into specific use cases—for example, allowing users to separately opt-in to "basic ads" and "personalized ads".
{% 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:
- The
buffered: trueoption indicates that thePerformanceObservershould check the browser's performance entry buffer for performance entries that were created before the observer's initialization. As a result, thePerformanceObserverwill report layout shifts that happened both before and after it was initialized. Keep this in mind when inspecting the console logs. An initial glut of layout shifts can reflect a reporting backlog, rather than the sudden occurrence of numerous layout shifts. - To avoid impacting performance, the
PerformanceObserverwaits until the main thread is idle to report on layout shifts. As a result, depending on how busy the main thread is, there may be a slight delay between when a layout shift occurs and when it is logged in the console. - This script ignores layout shifts that occurred within 500 ms of user input and therefore do not count towards CLS.
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.
- The
xandycoordinates report the x-coordinate and y-coordinate respectively of the top-left corner of the element - The
widthandheightproperties report the width and height respectively of the element. - The
top,right,bottom, andleftproperties report the x or y coordinate values corresponding to the given edge of the element. In other words, the value oftopis equal toy; the value ofbottomis equal toy+height.
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:
- Changes to the position of a DOM element
- Changes to the dimensions of a DOM element
- Insertion or removal of a DOM element
- Animations that trigger layout
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:
- Did the position or dimensions of the preceding element change?
- Was a DOM element inserted or removed before the shifted element?
- Was the position of the shifted element explicitly changed?
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.
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:
- Stylesheets that are loaded late or overwrite previously declared styles.
- Animation and transition effects.
Changes to the dimensions of an element
This type of change is often a result of:
- Stylesheets that are loaded late or overwrite previously declared styles.
- Images and iframes without
widthandheightattributes that load after their "slot" has been rendered. - Text blocks without
widthorheightattributes that swap fonts after the text has been rendered.
The insertion or removal of DOM elements
This is often the result of:
- Insertion of ads and other third-party embeds.
- Insertion of banners, alerts, and modals.
- Infinite scroll and other UX patterns that load additional content above existing content.
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:
{% 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:
- Open the page in Chrome. {% Instruction 'devtools-performance', 'ol' %}
- 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:
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:
- In DevTools, open the Command Menu, by pressing
Control+Shift+PorCommand+Shift+P(Mac). - Type "Coverage" and select Show Coverage.
- Click the Reload button, to reload the page and start capturing the coverage.
Double-click on the report, to see classes marked in two colors:
- Green (critical): These are the classes the browser needs to render the visible content (like the title, subtitle, and accordion buttons).
- Red (non-critical): These styles apply to content that's not immediately visible (like the paragraphs inside the accordions).
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:
- Extract the class definitions marked with green in the report obtained from
the coverage tool, and put those classes inside a
<style>block at the head of the page:
<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>
- Then, load the rest of the classes asynchronously, by applying the following pattern:
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
This is not the standard way of loading CSS. Here's how it works:
link rel="preload" as="style"requests the stylesheet asynchronously. You can learn more aboutpreloadin the Preload critical assets guide.- The
onloadattribute in thelinkallows the CSS to be processed when it finishes loading. - "nulling" the
onloadhandler once it is used helps some browsers avoid re-calling the handler upon switching the rel attribute. - The reference to the stylesheet inside of a
noscriptelement works as a fallback for browsers that don't execute JavaScript.
{% 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:
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!):
The Eliminate render-blocking resources suggestion is no longer under Opportunities, and now belongs to the Passed Audits section:
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:
-
Remove it if it doesn't add clear value to your site.
-
Optimize the loading process.
This post explains how to optimize the loading process of third-party scripts with the following techniques:
-
Using the
asyncordeferattribute on<script>tags -
Establishing early connections to required origins
-
Lazy loading
-
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" %}
-
Use
asyncif it's important to have the script run earlier in the loading process. -
Use
deferfor less critical resources. A video player that's below-the-fold, for example.
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 -
dns-prefetch
preconnect
<link rel="preconnect"> informs the browser that your page intends to establish a connection to another origin, and that you'd like the process to start as soon as possible. When the request for a resource from the pre-connected origin is made, the download starts immediately.
<link rel="preconnect" href="https://cdn.example.com">
{% 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
<link rel="dns-prefetch> handles a small subset of what is handled by <link rel="preconnect">. Establishing a connection involves the DNS lookup and TCP handshake, and for secure origins, TLS negotiations. dns-prefetch instructs the browser to only resolve the DNS of a specific domain before it has been explicitly called.
The preconnect hint is best used for only the most critical connections; for less critical third-party domains use <link rel=dns-prefetch>.
<link rel="dns-prefetch" href="http://example.com">
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:
<link rel="preconnect" href="http://example.com">
<link rel="dns-prefetch" href="http://example.com">
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.
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:
- Reduce DNS lookup and round-trip times.
- Improve HTTP caching headers.
- Take advantage of HTTP/2, or the newer HTTP/3.
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 %}
{% 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.
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.
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.
Summary
A few key areas where Fetch Priority can help:
- Boost the priority of the LCP image by specifying
fetchpriority="high"on the image element, causing LCP to happen sooner. - Increase the priority of
asyncscripts using better semantics than the current hack that is commonly used (inserting a<link rel="preload">for theasyncscript). - Decrease the priority of late-body scripts to allow for better sequencing with images.
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,
- CSS, fonts, scripts, images, and third-party resources are assigned different priorities.
- The location or order in which you reference resources in the document also affects the priority of resources.
- The
preloadresource hint helps the browser to discover a resource faster and thus load it before the document loads it and affects priority. - Priority computation changes for
asyncordeferscripts.
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).
type = "font" on BBC news detail pageWhen 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
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:
- 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.
- 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 thefetchpriority='high'on the preload, otherwise it will still start with the default "Low" priority for images. - Declaring scripts as
asyncordefertells 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. - 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. - 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:
high: You consider the resource a high priority and want the browser to prioritize it as long as the browser's heuristics don't prevent that from happening.low: You consider the resource a low priority and want the browser to deprioritize it if it's heuristics permit.auto: This is the default value where you don't have a preference and let the browser decide the appropriate priority.
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.
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.
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:
-
The
fetchpriorityattribute is a hint and not a directive. The browser will try to respect the developer's preference. It is also possible that the browser will apply its preferences for resource priority as deemed necessary in case of conflicts. -
Fetch Priority should not be confused with a preload. They are both distinct because:
- Preload is a mandatory fetch and not a hint.
- Preload allows the browser to discover the resource early, but it will still fetch it with the default priority. Conversely, Fetch Priority does not aid discoverability, but does allow you to increase or decrease the fetch priority.
- It is easier to observe and measure the effects of a preload.
Fetch Priority can complement preloads by increasing the granularity of prioritization. If you had already specified a preload as one of the first items in the
<head>for an LCP image, then ahighFetch Priority may not result in significant gains. However, if the preload was after other resources, then ahighFetch Priority can improve the LCP. If a critical image is a CSS background image, you should preload it withfetchpriority = "high". -
The noticeable gains due to prioritization will be more relevant in environments where more resources contend for the available network bandwidth. This is common for HTTP/1.x connections where parallel downloads are not possible or in low bandwidth HTTP/2 connections. Prioritization can resolve bottlenecks in these conditions.
-
CDNs do not uniformly implement HTTP/2 prioritization. Even if the browser communicates the priority suggested using Fetch Priority; the CDN may not reprioritize resources in the required order. This makes testing of Fetch Priority difficult. The priorities are applied both internally within the browser and with protocols that support prioritization (HTTP/2 and HTTP/3). It is still worth using even for just the internal browser prioritization independent of CDN or origin support, as that will often change when resources are requested by the browser—for example low priority resources like images are often held back from being requested while the browser processes the critical
<head>items. -
It may not be possible to introduce Fetch Priority as a best practice in your initial design. It is an optimization that you can apply later in the development cycle. You can check the priorities being assigned to different resources on the page, and if they do not match your expectations, you could introduce Fetch Priority for further optimization.
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:
- Including the preload in HTTP headers will cause it to jump ahead of everything else.
- Generally, preloads will load in the order the parser gets to them for anything above "Medium" priority—so be careful if you are including preloads at the beginning of the HTML.
- Font preloads will probably work best towards the end of the head or beginning of the body.
- Import preloads (dynamic
import()ormodulepreload) should be done after the script tag that needs the import (so the actual script gets loaded/parsed first). Basically, if the script tag loads a script that will trigger the load of dependencies, make sure the<link rel=preload>for the dependencies is after the parent script tag, otherwise the dependencies may end up loading before the main script. In the proper order, the main script can be parsed/eval'd while the dependencies are loading. - Image preloads will have a "Low" priority (without Fetch Priority) and should be ordered relative to async scripts and other low or lowest priority tags.
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:
-
Even though a larger proportion of your sales might come from the physical stores, most customers will use a digital platform such as your site before the purchase in the stores. If the site doesn't give visitors a good user experience, then the revenue in physical stores risk suffering as well. Plus, studies show that it also affects customers' willingness to recommend your business and their loyalty to your business.
-
The growth of mobile visitors is stronger than desktop visitors, making mobile-first either a current or future state to prepare for. Also, there's a strong correlation between site speed and conversions on mobile, with one study from SOASTA showing that for every second delay in mobile page load, conversions can fall by up to 20%.
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:
- Calculate your relative mobile conversion rate. Work with your analytics team and perform the analysis described in The value of speed. If you historically have one period (at least 2-3 months) when the load times have been much higher than another period of equal length, then the slow site speed will usually have affected mobile conversions heavily. Through the analysis you can calculate how much revenue the company lost during the period of bad speed, proving the value of speed. If load times have stayed at about the same level, the analysis will be difficult to perform and the below alternatives can give more answers.
- Estimate the value of speed from statistical models. With the help of Google's TestMySite you can run a test and then calculate the impact that a faster site can have on your business. All you need is data around average monthly visitors, conversion rate, and average order value, which your analytics team can help you with.
- Provide best practice case studies. You can also use cases from other companies, who've made their site faster and seen the effect. How Page Load Time Affects Conversion Rates: 12 Case Studies provides some examples.
- Show the importance of mobile. Ask the analytics team to provide data around what percentage of your traffic comes from mobile versus desktop.
- Research how much it will cost. The above calculations are ways to show stakeholders the possible revenue uplift from speed, but in order for stakeholders to make decisions they need to know the ROI, so if you come prepared with an estimated cost then the executive decision can be reached faster. To do this, evaluate how long the initial focused speed project will take, and how much time you will need for maintenance afterwards. For instance, in Fast-forward thinking: TUI speed up their site by breaking down silos, the developers were given 20% of their time free from backlogs and campaigns, which they could devote to speed projects and other ways of making the site better.
Meet with your stakeholders
- Present how much traffic you are getting from mobile versus desktop, and the growth you've had from mobile the last few years. This showcases the importance of creating a great mobile experience.
- Present your findings on how speed influences the company's revenue.
- Let the stakeholders know that the developers can help increase revenue from the mobile visitors if they are given time to work on speed continuously.
- Suggest that one person from the development team should be invited to executive meetings where decisions around the site will be taken. Nowadays we sell on digital platforms. The winners will be the companies who start communicating with their digital experts. Modern companies will need to have communication streams set up between departments and between stakeholders and developers. A study by the Harvard Business Review showed that one in six IT projects has a cost overrun of 200%. One example was Kmart, which was losing its competitive position to Walmart and Target when it began its $1.4 billion IT modernization project in 2000. By setting up communication and maintenance early, silos can be broken down and knowledge sharing can become a competitive advantage.
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:
- Avoid duplicates that measure approximately the same thing.
- Remove tools that are no longer used.
- Don't measure "nice-to-have" things. Only measure business-critical things.
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
- Create a list of the scripts that are implemented on the site.
- Send the list to marketing and ask them to take a look at it in advance and sort them into
three buckets:
- Business-critical. These scripts must stay.
- Nice-to-have. Scripts that might be able to be used less or removed. For instance, sometimes companies collect heat maps or screen recordings, but the data might only be reviewed infrequently and not lead to action or increased conversions. In cases such as these it can be better to agree to only run the tools for (say) two weeks, collect the insights for analysis, then clean out the scripts until it's time for the next period of focused data collection.
- Not used or not owned. These should be removed.
- Check how much each script slows down your site by comparing site speed with and without it. In WebPageTest this can be set up via the Block tab in Advanced Settings. Or you can block network requests in Chrome DevTools.
Meet with your marketing team
- Go through the list of scripts and give your view on each script. For instance, are any of the nice-to-have scripts very damaging to site performance? By receiving recommendations it's easier for the marketing team to prioritize, and by hearing about what is business-critical for them it's easier for you to understand their point of view.
- Discuss the following topics:
- Are the benefits of the data collected leading to an uplift in conversions that exceeds the decrease caused by the site getting slower? Data can guide the decision.
- Has your company chosen a provider that has the fastest version of the tool on the market? The marketing department might need help from developers to work out whether business critical tools can be replaced by faster alternatives.
- Are there alternatives to any of the scripts on the site? For instance, instead of heatmaps it might be possible to start doing usability tests where you also hear about the "why" behind user behavior, often leading to deeper insights.
{% 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
- Decide on a process moving forward. Should marketing always ask the developers to review new tools and their impact on speed before implementation? Or should marketing get a performance budget where they learn how to check speed and they themselves can prioritize which tools they want as long as they meet the speed target?
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
- Track which pages on the site have the highest load time and highest page weight. For instance, if you're working with an e-commerce site, make sure you check not just the home page, but also any campaign pages, some of the biggest category pages, some of the product pages, and the checkout flow.
- Prepare a performance budget proposal for the designers. One alternative is to start as simple as possible with page weight as the metric and set 1 MB as the target for all pages (or 1.5 MB if branding is very important for conversions).
- Prepare a list of which pages end up above the target page weight level.
Meet with your web design team
- Commit to investigating how the development team can deliver images faster through various image optimization techniques, such as image compression, responsive images, image resizing, lazy loading, caching, and server optimization.
- Create a test page where images are published in different image qualities and dimensions, and agree on a level that balances performance and quality on different screens. Remember that visitors on mobile often have short session length, with one study showing that more than half of mobile sessions are 30 seconds or shorter. These users might not study images in detail and might prefer the site to be fast.
- Propose a performance budget where the web designers commit to making sure that all web pages have a page weight below 1 MB (or at maximum 1.5 MB).
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
- Track speed regularly. One recommendation is to use an API with a tool like Lighthouse CI, PageSpeed Insights, or WebPageTest, enabling you to collect stats regularly in a controlled lab setting for easy benchmarking across different time periods.
- Forward The value of speed to the analytics team and ask if they can perform the analysis so that you have the chart ready to be analyzed during the meeting.
Meet with your analytics team
- Check the chart created from The value of speed. Relative mobile conversion rate is one way of tracking the impact of speed and how your site is improving. Ask the analytics team if they're open to start tracking this.
- Decide if the load time metric in Google Analytics is good enough for simplified business reports. The value of speed uses this metric. As many developers know, this is only one of many important speed metrics, and since it's in Google Analytics it's also field data instead of lab data. But, is it good enough to be used for simplified business reports? Check the result you get from the analysis above, and see if it is possible to observe the relative mobile conversion rate going down when load times have been high for at least 2–3 months, and if the relative mobile conversion rate goes up when load times have been low for 2–3 months. If your load times have remained pretty stable, it will be difficult to know if the analysis is working for you until you've improved speed heavily. If so, go with the next alternative.
- Show your charts for the speed metrics the developers use. Explain them to the analytics team and see if you discover correlations with the conversion rate or the relative mobile conversion rate.
- Give the analytics team time to think about ways to use speed in calculations connected to revenue and how to report this to stakeholders. This is often a new topic for them. Then meet again and try out an approach that you agree upon. As always, you need to revise data collection until you find the right fit, but this is a way to start the collaboration.
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:
- Delayed text rendering: If a web font has not loaded, browsers typically delay text rendering. In many situations, this delays First Contentful Paint (FCP). In some situations, this delays Largest Contentful Paint (LCP).
- Layout shifts: The practice of font swapping has the potential to cause layout shifts and so impact Cumulative Layout Shift (CLS). These layout shifts occur when a web font and its fallback font take up different amounts of space on the page.
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.
<head>
<link rel="preconnect" href="https://fonts.com">
</head>
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.
<head>
<link rel="preconnect" href="https://fonts.com">
<link rel="preconnect" href="https://fonts.com" crossorigin>
</head>
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.
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
</head>
{% 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.
Bram Stein, from the 2022 Web Almanac
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.
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 |
- Block period: The block period begins when the browser requests a web font. During the block period, if the web font is not available, the font is rendered in an invisible fallback font and thus the text will not be visible to the user. If the font is not available at the end of the block period, it will be rendered in the fallback font.
- Swap period: The swap period comes after the block period. If the web font becomes available during the swap period, it will be "swapped" in.
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:
-
If performance is a top priority: Use
font-display: optional. This is the most "performant" approach: text render is delayed for no longer than 100ms and there is assurance that there will be no font-swap related layout shifts. However, the downside here is the web font will not be used if it arrives late. -
If displaying text quickly is a top priority, but you want to still ensure the web-font is used: Use
font-display: swapbut make sure to deliver the font early enough that it does not cause a layout shift. The downside of this option is the jarring shift when the font arrives late. -
If ensuring text is displayed in a web font is a top priority: Use
font-display: blockbut make sure to deliver the font early enough that it minimises the delay of the text. The downside of this is the initial text display will be delayed. Note despite this deplay, it can still cause a layout shift as the text is actually drawn invisible, and the fallback font space is therefore user to reserver the space. Once the web font loads, this may require difference space and hence a shift. This may, however, be a less jarring shift thanfont-display: swapas the text itself will not be seen to shift.
{% 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.
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.
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:
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:
- Gather multiple metrics and try to narrow and filter afterwards on what might be relevant for the task at hand.
- Abstract metrics into an overall score, as for example Lighthouse does. This can be especially useful for non-technical staff and other stakeholders, but is probably insufficient for deeper technical analysis.
- Try to find the one metric which is most relevant as a predictor for your conversions and then optimize towards 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:
{% 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:
- Always keep third party content out of the critical rendering path. If the third party has server problems and times out, it will impact your website heavily. You can test and simulate this via WebPageTest Single-Point-of-Failure tests.
- Continually measure and report the ratio of third party content, for example via WebPageTest domain breakdown. Make sure to use performance budgets for third party content.
- If you suspect that a particular third party component is having a detrimental effect on performance, do a performance comparison with it included and excluded. Find out how.
- Try to stay on one stack from one vendor if possible. For example, if you have a tag manager and analytics on one stack, you may only need a single script, and may be able to take advantage of HTTP2 synergies as there is only one host involved.
- Make sure not to use the same functionality from two different vendors. You shouldn't need two tag managers or two analytics platforms.
- Routinely audit and clean out redundant third party scripts, trackers and widgets. This can easily be done via Ghostery Extension or tools like WhatRuns:
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:
- Facebook and Google do this by providing slow networks across the company for testing.
- Make average, low-spec devices with low bandwidth or high latencies available to management and other stakeholders.
- Consider adding overlays showing performance metrics on your development or staging servers. Connections to these servers from mobile can potentially be throttled by default on corporate networks.
- Monitors can be placed strategically across the company showing videos or timestrips of your website's loading behavior, preferably in comparison to competitors. WebPageTest can create these very easily and automatically.
- Performance can also be fun—maybe a game based on an individual Lighthouse report can reach audiences which can't be reached by pure reports and metrics?
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:
| 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' %}
- Click Mobile.
- Select the Performance checkbox. (You can clear the rest of the checkboxes in the Audits section.)
- Click Simulated Fast 3G, 4x CPU Slowdown.
- Select the Clear Storage checkbox.
- Click Run audits.
{% 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.
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' %}
- Right-click any request in the Network panel.
- 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:
- 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.)
- Block the URLs or domains responsible for third-party scripts you believe are an issue.
- 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.
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.
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:
- Client hints (for example, viewport width, DPR, and image width)
- The
Save-Dataheader - The User-Agent request header
- The Network Information API
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.
- Thumbor is the most popular self-managed image CDN. While it is open-source and free to use, it generally has fewer features than most commercial CDNs, and its documentation is somewhat limited. Wikipedia, Square, and 99designs are three sites that use Thumbor. See the How to install the Thumbor image CDN post for instructions on setting it up.
- Imaginary
- Imagor
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:
- 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.
- Consider using a
fetchpriorityattribute value of"high"on the LCP image element so that the browser can begin loading that image as soon as possible. - If an image is not immediately discoverable in the initial HTML, consider using a
rel=preloadhint for your LCP candidate image so that the browser can load that image ahead of time. - 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:
- resource size
- resource count
You can set budgets for any of the following resource types:
documentfontimagemediaotherscriptstylesheetthird-partytotal
Budgets are set in a JSON file and after defining them the new "Over Budget" column tells you whether you're exceeding any limits.
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.
The default limit for both assets and entry-points is 250 KB. You can set your own targets in the configuration file.
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 %}
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.
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.
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
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.
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. ⛔
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.
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 | ✔️ | ❌ |
|
| webpack | ✔️ | ❌ |
|
| bundlesize | ✔️ | ✔️ |
|
| Lighthouse Bot | ❌ | ✔️ |
|
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:
You've probably already seen lazy loading in action, and it goes something like this:
- You arrive at a page, and begin to scroll as you read content.
- At some point, you scroll a placeholder image into the viewport.
- The placeholder image is suddenly replaced by the final image.
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.
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:
- It wastes data. On unmetered connections, this isn't the worst thing that could happen (although you could be using that precious bandwidth for downloading other resources that are indeed going to be seen by the user). On limited data plans, however, loading stuff the user never sees could effectively be a waste of their money.
- It wastes processing time, battery, and other system resources. After a media resource is downloaded, the browser must decode it and render its content in the viewport.
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.
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:
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:
- The
classattribute, which is what you'll select the element with in JavaScript. - The
srcattribute, which references a placeholder image that will appear when the page first loads. - The
data-srcanddata-srcsetattributes, 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 }
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:
- lazysizes is a full-featured lazy
loading library that lazy-loads images and iframes. The pattern it uses is quite
similar to the code examples shown here in that it automatically binds to a
lazyloadclass on<img>elements, and requires you to specify image URLs indata-srcand/ordata-srcsetattributes, the contents of which are swapped intosrcand/orsrcsetattributes, respectively. It uses Intersection Observer (which you can polyfill), and can be extended with a number of plugins to do things like lazy-load video. Find out more about using lazysizes. - vanilla-lazyload is a lightweight option for lazy loading images, background images, videos, iframes, and scripts. It leverages Intersection Observer, supports responsive images, and enables browser-level lazy loading.
- lozad.js is a another lightweight option that uses Intersection Observer only. As such, it's highly performant, but will need to be polyfilled before you can use it on older browsers.
- If you need a React-specific lazy loading library, consider react-lazyload. While it doesn't use Intersection Observer, it does provide a familiar method of lazy loading images for those accustomed to developing applications with React.
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:
- In Chrome, the default for
preloadused to beauto, but as of Chrome 64, it now defaults tometadata. Even so, on the desktop version of Chrome, a portion of the video may be preloaded using theContent-Rangeheader. Other Chromium-based browsers and Firefox behave similarly. - As with Chrome on desktop, 11.0 desktop versions of Safari will preload a range of the video. From version 11.2, only the video metadata is preloaded. In Safari on iOS, videos are never preloaded.
- When Data Saver mode is
enabled,
preloaddefaults tonone.
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:
- They play automatically when loaded.
- They loop continuously (though that's not always the case).
- 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:
- vanilla-lazyload and lozad.js are super lightweight options that use Intersection Observer only. As such, they are highly performant, but will need to be polyfilled before you can use them on older browsers.
- yall.js is a library that uses
Intersection Observer and falls back to event handlers. It can also lazy load video
posterimages using adata-posterattribute. - If you need a React-specific lazy loading library, you might consider react-lazyload. While it doesn't use Intersection Observer, it does provide a familiar method of lazy loading images for those accustomed to developing applications with React.
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:
- It uses spaces for indentation purposes and contains comments, which are ignored by the browser, so they can be removed.
- The
<h1>and<h2>elements have the same styles: instead of declaring them separately: "h1 {...} h2 {...}" they could be expressed as "h1, h2{...}". - The background-color,
#000000could be expressed as just#000.
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:
- Open the measure page.
- Enter the URL:
https://fav-kitties-animated.glitch.meand click Run Audit. - Click View report.
- 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:
- Open the Fav Kitties site in Chrome. (It might take a while for Glitch servers to respond the first time.) {% Instruction 'devtools-network', 'ol' %}
- Click the CSS filter.
- 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.
- Click animate.css.
- 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:
- mini-css-extract-plugin will extract each style sheet into its own file, as one of the steps of the build process.
- webpack-fix-style-only-entries is used to correct an issue in wepback 4, to avoid generating an extra JS file for each CSS file listed in webpack-config.js.
You will now make some changes in the project:
- 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:
- In Glitch console, run
npm install --save-dev optimize-css-assets-webpack-plugin. - 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' %}
- Click the CSS filter.
- 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:
- Open the measure page.
- Enter the URL of the optimized site.
- Click View report.
- 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
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.
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:
domainLookupStartis when DNS lookup begins.domainLookupEndis when DNS lookup ends.
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:
connectStartis when the browser starts to open a connection to a web server.secureConnectionStartmarks when the client begins TLS negotiation.connectEndis when the connection to the web server has been established.
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:
- Extrinsic factors: These are things like latency and bandwidth. Beyond choosing a hosting company and a CDN, they're (mostly) out of our control, as users can access the web from anywhere.
- Intrinsic factors: These are things like server and client-side architectures, as well as resource size and our ability to optimize for those things, which are within our control.
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:
fetchStartmarks when the browser begins to fetch a resource (Resource Timing) or a document for a navigation request (Navigation Timing). This precedes the actual request, and is the point at which the browser is checking caches (for example, HTTP andCacheinstances).workerStartmarks when a request starts being handled within a service worker'sfetchevent handler. This will be0when no service worker is controlling the current page.requestStartis when the browser makes the request.responseStartis when the first byte of the response arrives.responseEndis when the last byte of the response arrives.
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
- Page redirects: Redirects are an overlooked source of added latency, especially redirect chains. Latency gets added in a number of ways, such as HTTP-to-HTTPs hops, as well as 302/uncached 301 redirects. The
redirectStart,redirectEnd, andredirectCounttimings are helpful in assessing redirect latency. - Document unloading: In pages that run code in an
unloadevent handler, the browser must execute that code before it can navigate to the next page.unloadEventStartandunloadEventEndmeasure document unloading. - Document processing: Document processing time may not be consequential unless your website sends very large HTML payloads. If this describes your situation, the
domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd, anddomCompletetimings may be of interest.
{% 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.
The recommended approach for collecting entries from the performance entry buffer is to use a PerformanceObserver. PerformanceObserver listens for performance entries, and provides them as they're added to the buffer:
// 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:
- Avoid averages, as they're not representative of any one user's experience, and may be skewed by outliers.
- Rely on percentiles. In datasets of time-based performance metrics, lower is better. This means that when you prioritize low percentiles, you're only paying attention to the fastest experiences.
- Prioritize the long tail of values. When you prioritize experiences at the 75th percentile or higher, you're putting your focus where it belongs: on the slowest experiences.
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:
- 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:
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:
background-position: center center: Center the image vertically and horizontally.background-repeat: no-repeat: Show the image only once.background-attachment: fixed: Avoid making the background image scroll.background-size: cover: Resize the image to cover the entire container.background-image: url(images/background-desktop.jpg): The URL of the image.
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:
- The same amount of bytes are sent, regardless of the screen size, even when, for some devices, like phones, a smaller and more lightweight image background would look just as good. In general, you want to send the smallest possible image that still looks good on the user's screen to improve performance and save user data.
- In smaller devices the image will be stretched or cut to cover the entire screen, potentially hiding relevant parts of the background to users.
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.
- In
style.cssremove the line that contains the background image URL:
body {
background-position: center center;
background-attachment: fixed;
background-repeat: no-repeat; background-size: cover;
background-image: url(images/background-desktop.jpg);
}
- Next, create a breakpoint for each screen width, based on the common dimensions in pixels that mobile, tablet, and desktop screens usually have:
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:
- Open the optimized site in a new Chrome tab.
- Make your viewport narrow (less than
480px). {% Instruction 'devtools-network', 'ol' %} {% Instruction 'reload-page', 'ol' %} Notice how thebackground-mobile.jpgimage was requested. - Make your viewport wider. Once it's wider than
480pxnotice howbackground-tablet.jpgis requested. Once it's wider than1025pxnotice howbackground-desktop.jpgis 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:
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" %}
- The browser requests the HTML document.
- The browser begins parsing the HTML response and constructing the DOM.
- The browser discovers CSS, JS, and other resources and dispatches requests.
- 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.
- 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:
- 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.
- 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.
- 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:
autoblockswapfallbackoptional
For more information on preloading fonts, and the font-display property, see the following posts:
- Avoid invisible text during font loading
- Controlling font performance using font-display
- Prevent layout shifting and flashes of invisibile text (FOIT) by preloading optional fonts
The Font Loading API
Used together, <link rel="preload"> and the CSS font-display give you a great deal of control over font loading and rendering,
without adding in much overhead.
But if you need additional customizations,
and are willing to incur the overhead introduced by running JavaScript, there is another option.
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:
- You can hold all text rendering until the font is available.
- You can implement a custom timeout for each font.
- You can use the fallback font to unblock rendering and inject a new style that uses the desired font after the font is available.
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
- Customize font loading and rendering using
<link rel="preload">,font-display, or the Font Loading API: default lazyloading behavior may result in delayed text rendering. These web platform features allow you to override this behavior for particular fonts, and to specify custom rendering and timeout strategies for different content on the page. - Specify revalidation and optimal caching policies: fonts are static resources that are infrequently updated. Make sure that your servers provide a long-lived max-age timestamp and a revalidation token to allow for efficient font reuse between different pages. If using a service worker, a cache-first strategy is appropriate.
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:
- Preload key requests
- Uses inefficient cache policy on static assets
- All text remains visible during WebFont loads
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:
- Maximum size of images
- Maximum number of web fonts
- Maximum size of scripts, including frameworks
- Total number of external resources, such as third-party scripts
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.
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:
- Under 5 s Time to Interactive
- Under 170 KB of critical-path resources (compressed/minified)
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:
- Our product page must ship less than 170 KB of JavaScript on mobile
- Our search page must include less than 2 MB of images on desktop
- Our home page must load and get interactive in < 5 s on slow 3G on a Moto G4 phone
- Our blog must score > 80 on Lighthouse performance audits
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:
- Optimize an existing feature or asset 🛠️
- Remove an existing feature or asset 🗑️
- Not add the new feature or asset ✋⛔
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.
@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.
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:
<link rel="preload" as="script" href="critical.js">
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:
<head>
<link rel="preload" as="script" href="critical.js">
</head>
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:
<link rel="preload" href="ComicSans.woff2" as="font" type="font/woff2" crossorigin>
{% 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:
- Preload fonts while using the default
blockvalue forfont-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 withfont-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 withfont-display: block;may be an acceptable compromise. - Preload fonts while using the
fallbackvalue forfont-display.fallbackis a compromise betweenswapandblock, in that it has an extremely short blocking period. - Use the
optionalvalue forfont-displaywithout 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 theoptionalvalue. In adverse conditions,optionalwill 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:
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.
<link rel="preload">informs the browser that a resource is needed as part of the current navigation, and that it should start getting fetched as soon as possible.<link rel="preconnect">informs the browser that your page intends to establish a connection to another origin, and that you'd like the process to start as soon as possible.<link rel="prefetch">is somewhat different from<link rel="preload">and<link rel="preconnect">, in that it doesn't try to make something critical happen faster; instead, it tries to make something non-critical happen earlier, if there's a chance.
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:
- Classes (ES2015)
- Arrow functions (ES2015)
- Generators (ES2015)
- Block scoping (ES2015)
- Destructuring (ES2015)
- Rest and spread parameters (ES2015)
- Object shorthand (ES2015)
- Async/await (ES2017)
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:
-
Legacy JavaScript is typically around 20% larger and slower than equivalent modern code. Tooling deficiencies and misconfiguration often widen this gap even further.
-
Installed libraries account for as much as 90% of typical production JavaScript code. Library code incurs an even higher legacy JavaScript overhead due to polyfill and helper duplication that could be avoided by publishing modern code.
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.
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.
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:
-
Goals. Key performance metrics related to user experience. For example, tap to paint in under 100 milliseconds. Since human perception is relatively constant, these goals are unlikely to change any time soon.
-
Guidelines. Recommendations that help you achieve goals. These may be specific to current hardware and network connection conditions, and therefore may change over time.
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:
-
To ensure a visible response within 100 ms, process user input events within 50 ms. This applies to most inputs, such as clicking buttons, toggling form controls, or starting animations. This does not apply to touch drags or scrolls.
-
Though it may sound counterintuitive, it's not always the right call to respond to user input immediately. You can use this 100 ms window to do other expensive work, but be careful not to block the user. If possible, do work in the background.
-
For actions that take longer than 50 ms to complete, always provide feedback.
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:
Animation: produce a frame in 10 ms
Goals:
-
Produce each frame in an animation in 10 ms or less. Technically, the maximum budget for each frame is 16 ms (1000 ms / 60 frames per second ≈ 16 ms), but browsers need about 6 ms to render each frame, hence the guideline of 10 ms per frame.
-
Aim for visual smoothness. Users notice when frame rates vary.
Guidelines:
-
In high pressure points like animations, the key is to do nothing where you can, and the absolute minimum where you can't. Whenever possible, make use of the 100 ms response to pre-calculate expensive work so that you maximize your chances of hitting 60 fps.
-
See Rendering Performance for various animation optimization strategies.
{% Aside %} Recognize all the types of animations. Animations aren't just fancy UI effects. Each of these interactions are considered animations:
- Visual animations, such as entrances and exits, tweens, and loading indicators.
- Scrolling. This includes flinging, which is when the user starts scrolling, then lets go, and the page continues scrolling.
- Dragging. Animations often follow user interactions, such as panning a map or pinching to zoom. {% endAside %}
Idle: maximize idle time
Goal: Maximize idle time to increase the odds that the page responds to user input within 50 ms.
Guidelines:
-
Use idle time to complete deferred work. For example, for the initial page load, load as little data as possible, then use idle time to load the rest.
-
Perform work during idle time in 50 ms or less. Any longer, and you risk interfering with the app's ability to respond to user input within 50 ms.
-
If a user interacts with a page during idle time work, the user interaction should always take the highest priority and interrupt the idle time work.
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:
-
Optimize for fast loading performance relative to the device and network capabilities of your users. Currently, a good target for first loads is to load the page and be interactive in 5 seconds or less on mid-range mobile devices with slow 3G connections.
-
For subsequent loads, a good target is to load the page in under 2 seconds.
{% Aside %}
Be aware that these targets may change over time.
{% endAside %}
Guidelines:
-
Test your load performance on the mobile devices and network connections that are common among your users. You can use Chrome User Experience Report to find out the connection distribution of your users. If the data is not available for your site, The Mobile Economy 2019 suggests that a good global baseline is a mid-range Android phone, such as a Moto G4, and a slow 3G network (defined as 400 ms RTT and 400 kbps transfer speed). This combination is available on WebPageTest.
-
Keep in mind that although your typical mobile user's device might claim that it's on a 2G, 3G, or 4G connection, in reality the effective connection speed is often significantly slower, due to packet loss and network variance.
-
You don't have to load everything in under 5 seconds to produce the perception of a complete load. Consider lazy loading images, code-splitting JavaScript bundles, and other optimizations suggested on web.dev.
{% Aside %}
Recognize the factors that affect page load performance:
- Network speed and latency
- Hardware (slower CPUs, for example)
- Cache eviction
- Differences in L2/L3 caching
- Parsing JavaScript
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:
-
Throttle your CPU to simulate a less-powerful device.
-
Throttle the network to simulate slower connections.
-
View main thread activity to view every event that occurred on the main thread while you were recording.
-
View main thread activities in a table to sort activities based on which ones took up the most time.
-
Analyze frames per second (FPS) to measure whether your animations truly run smoothly.
-
Monitor CPU usage, JS heap size, DOM nodes, layouts per second, and more in real-time with the Performance Monitor.
-
Visualize network requests that occurred while you were recording with the Network section.
-
Capture screenshots while recording to play back exactly how the page looked while the page loaded, or an animation fired, and so on.
-
View interactions to quickly identify what happened on a page after a user interacted with it.
-
Find scroll performance issues in real-time by highlighting the page whenever a potentially problematic listener fires.
-
View paint events in real-time to identify costly paint events that may be harming the performance of your animations.
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
-
Max Potential First Input Delay. Estimates how long your app will take to respond to user input, based on main thread idle time.
-
Does not use passive listeners to improve scrolling performance.
-
Total Blocking Time. Measures the total amount of time that a page is blocked from responding to user input, such as mouse clicks, screen taps, or keyboard presses.
-
Time To Interactive. Measures when a user can consistently interact with all page elements.
Load
-
Does not register a service worker that controls page and start_url. A service worker can cache common resources on a user's device, reducing time spent fetching resources over the network.
-
Defer offscreen images. Defer the loading of offscreen images until they're needed.
-
Properly size images. Don't serve images that are significantly larger than the size that's rendered in the mobile viewport.
-
Avoid an excessive DOM size. Reduce network bytes by only shipping DOM nodes that are needed for rendering the page.
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.
-
Focus on the user.
-
Respond to user input in under 100 ms.
-
Produce a frame in under 10 ms when animating or scrolling.
-
Maximize main thread idle time.
-
Load interactive content in under 5000 ms.
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.
Popular module bundlers like webpack,
Parcel, and
Rollup allow you to split your
bundles using dynamic imports.
For example, consider the following code snippet that shows an example of a
someFunction method that gets fired when a form is submitted.
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.
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.
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.
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):
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.
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:
- Minification
- Data compression
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.
- If you're using webpack v4 or greater, you should be good to go without doing any additional work. 👍
- If you are using an older version of webpack, install and include
TerserWebpackPlugininto your webpack configuration settings. Follow the documentation to learn how. - If you are not using a module bundler, use
Terseras a CLI tool or include it directly as a dependency to your application. The project documentation provides instructions.
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:
- Dynamically
- Statically
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.
Express is a popular web framework for Node and provides a compression middleware library. Use it to compress any asset as it gets requested. Here is an example of an entire server file that uses it correctly:
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.
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.
- Serve WOFF 2.0 variant to modern browsers.
- If absolutely necessary—such as if you still need to support Internet Explorer 11, for example—serve the WOFF as a fallback.
- Alternatively, consider not using web fonts for legacy browsers and falling back to the system fonts. This may be more performant for older, more constrained, devices as well.
- Since WOFF and WOFF 2.0 cover all bases for modern and legacy browsers still in use, use of EOT and TTF are no longer necessary and can result in longer web font download times.
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:
- The
local()directive allows you to reference, load, and use locally installed fonts. If the user already has the font installed on their system, this bypasses the network entirely, and is the fastest. - The
url()directive allows you to load external fonts, and are allowed to contain an optionalformat()hint indicating the format of the font referenced by the provided URL.
{% 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:
- 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.
- For each required font, the browser checks if the font is available locally.
- If the font is not available locally, the browser iterates over external definitions:
- If a format hint is present, the browser checks if it supports the hint before initiating the download. If the browser doesn't support the hint, the browser advances to the next one.
- If no format hint is present, the browser downloads the resource.
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:
- Single codepoint (for example,
U+416) - Interval range (for example,
U+400-4ff): indicates the start and end codepoints of a range - Wildcard range (for example,
U+4??):?characters indicate any hexadecimal digit
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.
- 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-facerules. - 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.
- 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
- 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.
The above diagram illustrates a font family that offers three different bold weights:
- 400 (regular).
- 700 (bold).
- 900 (extra bold).
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?
- If an exact font match isn't available, the browser substitutes the closest match.
- If no stylistic match is found (for example, no italic variants were declared in the example above), then the browser synthesizes its own font variant.
{% 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
- Audit and monitor your font use: don't use too many fonts on your pages, and, for each font, minimize the number of used variants. This helps produce a more consistent and a faster experience for your users.
- Avoid legacy formats if possible: EOT, TTF, and WOFF formats are larger than WOFF 2.0. EOT and TTF are strictly unnecessary formats, where as WOFF may be acceptable if you need to support Internet Explorer 11. If you're only target modern browsers, using WOFF 2.0 only is the simplest and most performant option.
- Subset your font resources: many fonts can be subset, or split into multiple unicode-ranges to deliver just the glyphs that a particular page requires. This reduces the file size and improves the download speed of the resource. However, when defining the subsets, be careful to optimize for font re-use. For example, don't download a different but overlapping set of characters on each page. A good practice is to subset based on script: for example, Latin, and Cyrillic.
- Give precedence to
local()in yoursrclist: listinglocal('Font Name')first in yoursrclist ensures that HTTP requests aren't made for fonts that are already installed. - Use Lighthouse to test for text compression.
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:
- They play automatically.
- They loop continuously (usually, but it is possible to prevent looping).
- They're silent.
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.
- YouTube found that switching to WebP thumbnails resulted in 10% faster page loads.
- Facebook experienced a 25-35% filesize savings for JPEGs and an 80% filesize savings for PNGs when they switched to using WebP.
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' %}
-
The
<source>tag for the "preferred" image format (in this case that is WebP) should be listed first, before other<source>tags. -
The value of the
typeattribute should be the MIME type corresponding to the image format. An image's MIME type and its file extension are often similar, but they aren't necessarily the same thing (e.g..jpgvs.image/jpeg).
{% 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' %}
- The `` tag should always be included, and it should always be listed last, after all `` tags.
- The resource specified by the `` tag should be in a universally supported format (e.g. JPEG), so it can be used as a fallback.
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:
- Absolute units: Elements styled using absolute units will always be displayed at the same size, regardless of device. Examples of valid, absolute CSS units: px, cm, mm, in.
- Relative units: Elements styled using relative units will be displayed at varying sizes, depending on the relative length specified. Examples of valid, relative CSS units: %, vw (1vw = 1% of the width of the viewport), em (1.5 em = 1.5 times font size).
The "Good" Approach
For images with sizing based on…
- Relative units: Resize the image to a size that will work across all devices.
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.
- Absolute units: Resize the image to match the size that it is displayed at.
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:
- Identify which browsers you want to target.
- Use
@babel/preset-envwith appropriate browser targets. - 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:
- A version only for browsers that support modules.
- A version that includes a compiled script which works in any legacy browser. This has a larger file size, since transpilation needs to support a wider range of browsers.
{% 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:
100px33vw20emcalc(50vw-10px)
The following is not a valid size:
25%(percentages cannot be used with the sizes attribute) {% endAside %}
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:
- Narcity improved LCP by 41%
- Paper Magazine noticed a 27% increase in Sessions per user
- MLT Blog decreased Page Load time by 21%
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.
The Preview tab provides more information about the contents of a SXG.
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.
-
sxg-rs/fastly_computeruns on Fastly Compute@Edge. -
Automatic Signed Exchanges is a Cloudflare feature that automatically acquires certificates and generates Signed Exchanges.
-
NGINX SXG module generates and serves SXGs for sites that use nginx. Setup instructions can be found here.
-
Envoy SXG Filter generates and serves SXGs for sites that use Envoy.
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:
-
sxg_rsis a Rust library for generating SXGs. It is the most featureful SXG library and is used as the basis for thecloudflare_workerandfastly_computetools. -
libsxgis a minimal C library for generating SXGs. It is used as the basis for the NGINX SXG module and the Envoy SXG Filter. -
go/signed-exchangeis a minimal Go library provided by the webpackage specification as a reference implementation of generating SXGs. It is used as the basis for its reference CLI tool,gen-signedexchangeand the more featureful Web Packager tools.
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:
- SXGs are cached and may be sent to users until they expire. Thus, many prefetches can be handled solely by the cache server.
- SXGs can be shown to users both with and without cookies on your site. Thus, there are fewer times when the page will need to be fetched again after navigation.
Page speed improvement
You may see additional page speed improvement due to the prefetch surfaces and capabilities it currently supports:
- SXGs can be shown to users with cookies for your site.
- SXG also prefetches subresources for your pages, such as JavaScript, CSS, fonts, and images, when specified using a
Linkheader. - In the near future, SXG prefetching from Google Search will be available on more search result types.
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
- Draft spec for Signed HTTP Exchanges
- Web Packaging explainers
- Get started with signed exchanges on Google Search
- How to set up Signed Exchanges using Web Packager
- Demo of Signed Exchanges
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
-
Generate a private key.
openssl ecparam -out priv.key -name prime256v1 -genkeyThe private key will be saved as a file named
priv.key. -
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 Demothat has the common nameexample.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.
-
Create a certificate that has the
CanSignHttpExchangesextension.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-extfileflag associates the certificate with theCanSignHttpExchangescertificate extension (1.3.6.1.4.1.11129.2.1.22is the object identifier for theCanSignHttpExchangesextension). In addition, the-extfileflag also definesexample.comas 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 -textYou are done creating private keys and certificates. You will need the
priv.keyandcert.pemfiles in the next section.
Setup the Web Packager server for testing
Prerequisites
-
Install Web Packager.
git clone https://github.com/google/webpackager.git -
Build
webpkgserver.cd webpackager/cmd/webpkgserver go build .webpkgserveris a specific binary within the Web Packager project. -
Verify that
webpkgserverhas been installed correctly../webpkgserver --helpThis 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
{% 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:
- A resource with the type
signed-exchange. This is the SXG. - A resource with the type
cert-chain+cbor. This the SXG certificate. SXG certificates must use theapplication/cert-chain+cborformat. - A resource with the type
document. This is the content that has been delivered via SXG.
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
-
You have a
CanSignHttpExchangescertificate. This page lists the CAs that offer this type of certificate. -
If you don't have a certificate, then you can configure your webpkgserver to automatically retrieve certificates from your CA. You can follow the directions for what goes in
webpkgserver.tomlin this page. -
Although not a requirement, it is strongly recommended that you run
webpkgserverbehind an edge server. If you do not use an edge server, you will need to configure theTLS.PEMFileandTLS.KeyFileoptions inwebpkgserver.toml. By default,webpkgserverruns over HTTP. However, SXG certificates must be served over HTTPS to be considered valid by the browser. ConfiguringTLS.PEMFileandTLS.KeyFileallowswebpkgserverto use HTTPS and therefore serve the SXG certificate directly to the browser.
Instructions
-
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.
-
Create a fresh
webpkgsever.tomlfile by copying the example.cp ./webpkgserver.example.toml ./webpkgserver.toml -
Open
webpkgserver.tomlwith the editor of your choice and make the following changes:- Change the line
PEMFile = cert.pemto 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
webpkgserverauto-renew the SXG certificate every 90 days (45 days for Google), configure the options in the[SXG.ACME]section ofwebpkgserver.toml. This option only applies to sites with a DigiCert or Google ACME account setup.
- Change the line
-
Configure your edge server to forward traffic to the
webpkgserverinstance.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,
webpkgserverserves 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:
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:
- Firing additional network requests
- Pulling in unoptimized images and videos
- Insufficient HTTP caching, which forces frequent fetching of network resources
- Insufficient server compression of resources
- Multiple instances of frameworks and libraries pulled in by different third-party embeds
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:
- When choosing third-party resources, favor those that send the least amount of code while still giving you the functionality you need.
- Use performance budgets for third-party content to keep their cost in check.
- Don't use the same functionality from two different vendors. You probably don't need two tag managers or two analytics platforms.
- Routinely audit and clean out redundant third-party scripts.
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:
- A budget of 3000ms for Time to Interactive.
- A budget of 1000ms for First Meaningful Paint
- A budget of 125 KB for the total amount of JavaScript on the page.
- A budget of 300 KB for the overall size of the page.
- A budget of 10 requests for the number of requests made to third-party origins.
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.
-
On Glitch, click Tools > Git, Import, and Export > Export to GitHub.
-
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". -
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.
-
Click the Tools button.
-
Then click the Console button. This will open the console in another tab.
-
In the console, type
webpackand wait for it to finish the build. -
Export the code to GitHub from Tools > Git, Import, and Export > Export to GitHub.
-
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).
{% 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
firebase login
Now initialize the project:
firebase init
The console will ask you a series of questions during setup:
- When prompted to select features, choose "Hosting."
- For the default Firebase project, select the project that you've created in the Firebase console.
- Type in "public" as your public directory.
- Type "N" (no) to configuring as a single-page app.
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:
- To have a
.travis.ymlfile in the root directory - 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:
- Checkout a new branch
- Push it to GitHub
- 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.
- Check that speed was the only big change to the site during the period you want to study. If there were other changes, Rel mCvR can tell you the effect of the changes as a group, but not of an individual change.
- Watch out for any changes or events that affected the desktop site but not the mobile site —they can skew your results. If you discover any desktop-only changes, omit the affected period from your analysis.
- You might wonder whether an increase in Rel mCvR is caused by a shift in conversions from desktop to mobile rather than an increase in conversions overall. While there is likely to be some change in the mix of desktop and mobile conversions due to speed improvements, keep in mind that Rel mCvR calculates the mobile conversion rate relative to the desktop conversion rate. So, you only see an increase in Rel mCvR when mCvR goes up more than dCvR. In other words, when doing this calculation you're already counting low, which means you have a safety margin that can compensate for any shifts in the channel mix.
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:
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:
- First Contentful Paint (FCP): The time the user stares at a blank screen. This is when most users bounce, as they don't see progress.
- First Meaningful Paint (FMP): When the user begins to see the main content they came for. This is often the hero image, but for a landing page it may even be a call to action such as a Buy button, since the user may have arrived with a clear intent (for example, through a targeted ad campaign).
- First Input Delay (FID): The time the website needs to react to the user's first input. Excessive JavaScript and other asset loading problems can block this, leading to failed taps or clicks, erroneous inputs and page abandonment.
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:
- Tell WebPageTest to navigate through the pages of the flow with the
navigatecommand. - If needed script the clicking of buttons via
clickAndWaitcommands and fill text fields viasetValue. For testing of Single Page Applications useclickAndWaitrather thannavigatecommands for all steps after the first, asnavigatewill do a full load instead of the lightweight virtual page load. - Make sure to combine the different steps of the flow in the analysis via
combineStepsto produce a single overall result report for the complete flow.
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:
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:
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.
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 | FCP | TTI |
|---|---|---|
| Homepage | 1,680 ms | 5,550 ms |
| Results page | 2,060 ms | 6,690 ms |
| 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:
- Google search's "related:" keyword
- Alexa's similar sites feature
- 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.
| 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 |
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.
| 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.
| 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:
- quantity-based metrics
- rule-based metrics
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.
Next steps
Make sure your performance budget is enforced throughout the project and incorporate it into your build process.