Wanna see something cool? Check out Angular Spotify 🎧

Improve Largest Contentful Paint (LCP)

Times really flies. Since the last article on improving FCP, it has been almost half a year. In this article, we’ll explore how to improve Largest Contentful Paint (LCP).

Largest Contentful Paint (LCP)

Largest Contentful Paint (LCP) is one of the three Core Web Vitals metrics, and it represents how quickly the main content of a web page is loaded. Specifically, LCP measures the time from when the user initiates loading the page until the largest image or text block is rendered within the viewport.

What elements are considered?

As currently specified in the Largest Contentful Paint API, the types of elements considered for Largest Contentful Paint are:

  • <img> elements (the first frame presentation time is used for animated content such as GIFs or animated PNGs)
  • <image> elements inside an <svg> element
  • <video> elements (the poster image load time or first frame presentation time for videos is used—whichever is earlier)
  • An element with a background image loaded using the url() function, (as opposed to a CSS gradient)
  • Block-level elements containing text nodes or other inline-level text element children.

Understanding your LCP metric

Before optimizing LCP, developers should seek to understand if they even have an LCP issue, and the extent of any such issue.

LCP can be measured in a number of tools and not all of these measure LCP in the same way. To understand LCP of real users, we should look at what real users are experiencing, rather than what a lab-based tool like Lighthouse or local testing shows. These lab-based tools can give a wealth of information to explain and help you improve LCP, but be aware that lab tests alone may not be entirely representative of what your actual users experience.

LCP data based on real users can be surfaced from Real User Monitoring (RUM) tools installed on a site, or by using the Chrome User Experience Report (CrUX) which collect anonymous data from real Chrome users for millions of websites

The Performance panel of Chrome DevTools shows your local LCP experience next to the page or origin’s CrUX LCP in the live metrics view.

LCP in Chrome DevTools

What is a good LCP score?

To provide a good user experience, sites should strive to have an LCP of 2.5 seconds or less for at least 75% of page visits.

LCP Threshold

Improve LCP

Now that we know how to get the first bytes out the door, we can make some infrastructure changes to ensure that we’re delivering our bytes as quickly as possible.

But how do we ensure that the bytes we’re sending to load the page are the right ones to reach this point of completion as soon as possible? As a reminder, the Largest Contentful Paint (LCP) is the time between the start of the page load and when the user sees that most of the page is ready, or almost ready.

Improve LCP

Let’s look at the sequence of events in a simplified version of what we would see in the performance panel of Google DevTools. At the beginning of a request, the first thing we need to do is fetch the HTML document. We need to pull down the initial document and have the browser parse it to determine what other resources are needed.

  • As the document is being parsed, the browser finds other resources that need to be downloaded, such as a CSS file in the head of the page. This CSS file might then require additional resources like a background image, a font, or another CSS file import, causing a chain of events that can increase load time.
  • You’ll also likely have images in the file, which the browser will try to download in parallel as much as possible. However, depending on the number of resources and the browser’s parallel download capabilities, some images might be deferred until later when there is available bandwidth.
  • Most applications will have some JavaScript, which often pulls down additional assets, such as other JavaScript files or modules. This can create a pattern where JavaScript loads more JavaScript, further delaying the LCP.

If we attach our metrics here, the First Contentful Paint (FCP) occurs right after the HTML is downloaded and the first elements are rendered on the screen. However, the LCP happens only after all other resources, such as images, JavaScript, and CSS, have been fully loaded and rendered.

So how do we achieve a faster LCP? We do this by reducing the number of tasks the browser needs to perform and focusing on delivering the user’s most important content as quickly as possible.

For a well-optimized page, you want your LCP resource request to start loading as early as it can, and you want the LCP element to render as quickly as possible after the LCP resource finishes loading. To help visualize whether or not a particular page is following this principle, you can break down the total LCP time into the following sub-parts:

Improve LCP

  • Time to First Byte (TTFB): The time from when the user initiates loading the page until the browser receives the first byte of the HTML document response.
  • Resource load delay: The time between TTFB and when the browser starts loading the LCP resource. If the LCP element doesn’t require a resource load to render (for example, if the element is a text node rendered with a system font), this time is 0.
  • Resource load duration: The duration of time it takes to load the LCP resource itself. If the LCP element doesn’t require a resource load to render, this time is 0.
  • Element render delay: The time between when the LCP resource finishes loading and the LCP element rendering fully.

1. Eliminate resource load delay

The goal in this step is to ensure the LCP resource starts loading as early as possible. While in theory the earliest a resource could start loading is immediately after TTFB, in practice there is always some delay before browsers actually start loading resources.

A good rule of thumb is that your LCP resource should start loading at the same time as the first resource loaded by that page. Or, to put that another way, if the LCP resource starts loading later than the first resource, then there’s opportunity for improvement.

Eliminate resource load delay

Generally speaking, there are two factors that affect how quickly an LCP resource can be loading:

  • When the resource is discovered.
  • What priority the resource is given.

Optimize when the resource is discovered

To ensure your LCP resource starts loading as early as possible, it’s critical that the resource is discoverable in the initial HTML document response by the browser’s preload scanner. For example, in the following cases, the browser can discover the LCP resource by scanning the HTML document response:

  • The LCP element is an <img> element, and its src or srcset attributes are present in the initial HTML markup.
  • The LCP element requires a CSS background image, but that image is preloaded using <link rel="preload"> in the HTML markup (or using a Link header).
  • The LCP element is a text node that requires a web font to render, and the font is loaded using <link rel="preload"> in the HTML markup (or using a Link header).

Here are some examples where the LCP resource cannot be discovered from scanning the HTML document response:

  • The LCP element is an <img> that is dynamically added to the page using JavaScript.
  • The LCP element is lazily loaded with a JavaScript library that hides its src or srcset attributes (often as data-src or data-srcset).
  • The LCP element requires a CSS background image.

In each of these cases, the browser needs to run the script or apply the stylesheet—which usually involves waiting for network requests to finish—before it can discover the LCP resource and could start loading it. This is never optimal.

To eliminate unnecessary resource load delay, your LCP resource should be discoverable from the HTML source. In cases where the resource is only referenced from an external CSS or JavaScript file, the LCP resource should be preloaded with a high fetch priority, for example:

<!-- Load the stylesheet that will reference the LCP image. -->
<link rel="stylesheet" href="/path/to/styles.css" />

<!-- Preload the LCP image with a high fetchpriority so it starts loading with the stylesheet. -->
<link
  rel="preload"
  fetchpriority="high"
  as="image"
  href="/path/to/hero-image.webp"
  type="image/webp"
/>

Optimize the priority the resource is given

Even if the LCP resource is discoverable from the HTML markup, it still may not start loading as early as the first resource. This can happen if the browser preload scanner’s priority heuristics don’t recognize that the resource is important, or if it determines that other resources are more important.

For example, you can delay your LCP image using HTML if you set loading="lazy" on your <img> element. Using lazy loading means that the resource won’t be loaded until after layout confirms the image is in the viewport and so may begin loading later than it otherwise would.

Even without lazy loading, images are not initially loaded with the highest priority by browsers as they are not render-blocking resources. You can hint to the browser as to which resources are most important using the fetchpriority attribute for resources that could benefit from a higher priority:

<img fetchpriority="high" src="/path/to/hero-image.webp" />

After you have optimized your LCP resource priority and discovery time, your network waterfall should look like this (with the LCP resource starting at the same time as the first resource):

Optimized network waterfall

2. Eliminate element render delay

The goal in this step is to ensure the LCP element can render immediately after its resource has finished loading, no matter when that happens.

The primary reason the LCP element wouldn’t be able to render immediately after its resource finishes loading is if rendering is blocked for some other reason:

  • Rendering of the entire page is blocked due to stylesheets or synchronous scripts in the <head> that are still loading.
  • The LCP resource has finished loading, but the LCP element has not yet been added to the DOM (it’s waiting for some JavaScript code to load).

Defer or inline render-blocking JavaScript

It is almost never necessary to add synchronous scripts (scripts without the async or defer attributes) to the <head> of your pages, and doing so will almost always have a negative impact on performance.

In cases where JavaScript code needs to run as early as possible in the page load, it’s best to inline it so rendering isn’t delayed waiting on another network request. As with stylesheets, though, you should only inline scripts if they’re very small.

❌ DON’T

<head>
  <script src="/path/to/main.js"></script>
</head>

âś… DO

<head>
  <script>
    // Inline script contents directly in the HTML.
    // IMPORTANT: only do this for very small scripts.
  </script>
</head>

3. Reduce resource load duration

The goal of this step is to reduce the time spent transferring the bytes of the resource over the network to the user’s device. In general, there are four ways to do that:

  • Reduce the size of the resource
    • Use modern image formats
    • Serve the optimal image size
  • Reduce the distance the resource has to travel.
    • Image CDNs
  • Reduce contention for network bandwidth.
    • Lazy load images
  • Eliminate the network time entirely.

Use modern image formats.

The LCP resource of a page (if it has one) will either be an image or a web font. The following guides go into great detail about how to reduce the size of both:

AVIF and WebP are image formats that have superior compression and quality characteristics compared to their older JPEG and PNG counterparts. Encoding your images in these formats rather than JPEG or PNG means that they will load faster and consume less cellular data.

AVIF is supported in Chrome, Firefox, and Opera and offers smaller file sizes compared to other formats with the same quality settings.

WebP is supported in the latest versions of Chrome, Firefox, Safari, Edge, and Opera and provides better lossy and lossless compression for images on the web

WebP vs AVIF vs JPEG

Image CDNs

Image CDNs are a type of CDN that specializes in serving images. They can help reduce the distance the resource has to travel by caching images closer to the user and provide additional features like image optimization and resizing.

Image CDNs

Image CDNs

Lazy load images

Lazy loading images means that images are only loaded when they are in the viewport. This can reduce contention for network bandwidth by ensuring that images are only loaded when they are needed.

Lazy Load Images

Lazy Load Images

Lazy Load Images

Summary

LCP is complex, and its timing can be affected by a number of factors. But if you consider that optimizing LCP is primarily about optimizing the load of the LCP resource, it can significantly simplify things.

At a high level, optimizing LCP can be summarized in a few steps:

  • Ensure the LCP resource starts loading as early as possible.
  • Ensure the LCP element can render as soon as its resource finishes loading.
  • Reduce the load time of the LCP resource as much as you can without sacrificing quality.

References

Published 1 Feb 2024

Read more

 — How to change Visual Studio Code terminal font?
 — @next/bundle-analyzer throw error Module not found: Can't resolve child_process
 — Angular Material 15 Migration
 — Improve First Contentful Paint (FCP)
 — Measuring Web Performance