Common misconceptions about how to optimize LCP

Brendan Kenny
Brendan Kenny

The Largest Contentful Paint (LCP) of a page can be complicated to improve, often involving multiple moving parts and tradeoffs. This post looks at field data from real page loads across the web to determine where developers should focus their optimization efforts.

Classic LCP advice: reduce the size of your images!

For most pages on the web, the LCP element is an image. It's natural then to assume that the best way to improve LCP is to optimize your LCP image.

In the five years or so since LCP was introduced, that has often been the headline advice. Ensure your images are sized appropriately and compressed sufficiently, and maybe use a 21st-century image format while you're in there. Lighthouse even has three different audits to make these suggestions.

The three image-optimization audits in a Lighthouse report
The three image-optimization audits in a Lighthouse report.

Part of the reason this is such common advice is that excessive bytes are easy to measure and image compression tools are easy to suggest. Depending on your build and deployment pipelines, it may also be easy to implement.

If so, do it! Sending fewer bytes to your users is almost always a win. There are many sites on the web that are still serving needlessly large images that even basic compression would fix.

However, when we started looking at field performance data for users in Chrome to see where the time to LCP is typically being spent, we found that image download time is almost never the bottleneck.

Instead, other parts of LCP are a much bigger problem.

LCP sub-part breakdown

To understand what the biggest opportunity areas were to improve LCP, we looked at data from the LCP sub-parts, as described in Optimize LCP.

While every page and every framework may take a different approach to loading and displaying what becomes the page's LCP element, every one can be divided into these sub-parts:

A breakdown of LCP showing the four sub-parts

Quoting from that article, the sub-parts are:

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, 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.

Real navigation performance data

A bar chart visualizing the differences in time spent in each LCP subpart, grouped into LCP buckets of good, needs improvement, and poor. TTFB and load delay rise rapidly in duration, while load duration and render delay remain short. Data is reproduced in table below

LCP rating TTFB (ms) Image load delay (ms) Image load duration (ms) Render delay (ms)
Good 600 350 160 230
Needs improvement 1,360 720 270 310
Poor 2,270 1,290 350 360

For this post, we used data from page navigations with a subresource image LCP in Chrome to take a look at the LCP sub-parts. We've looked at this kind of data before, but never from field data to see where real users are spending their time while waiting for a page's LCP.

Like with Core Web Vitals, we took the 75th percentile (p75) of each LCP sub-part for each origin in the CrUX dataset, resulting in four distributions of p75 values (one for each sub-part). To summarize these distributions, we took the median of those values across all origins for each of the four LCP sub-parts.

Finally, we split origins into buckets based on whether they have a "good", "needs improvement", or "poor" LCP at the 75th percentile. This helps show what distinguishes an origin with good LCP versus an origin with poor LCP.

Reduce the size of your LCP image? This time with data

Load duration is the measure of how long it takes to fetch the LCP resource, in this case, an image. This time is usually proportional to the number of bytes in the image, hence all the performance advice to reduce that number of bytes.

When looking at where time is going in the earlier graphs, one thing that stands out is that there is not a lot of time being spent in image load duration. In fact, it's the shortest LCP sub-part, in all LCP buckets. The load duration is longer for poor-LCP origins compared to good-LCP origins, but that's still not where time is largely being spent.

The majority of origins with poor LCP spend less than 10% of their p75 LCP time downloading the LCP image.

Yes, you should make sure your images are optimized, but that's just one part of improving LCP. And it's clear from this data that for the typical origin on the web, the potential millisecond gains for LCP overall is small no matter how sophisticated the compression scheme.

One final surprise: slow load durations were once typically blamed on mobile devices and the quality of mobile networks. We might have once expected a typical phone to take multiple times longer to download the same image as a desktop machine on a wired connection. The data suggests that's no longer the case. For origins with poor LCP, the median p75 image load duration is only 20% slower on mobile than desktop.

Time to First Byte (TTFB)

For navigations that make a network request, TTFB will always take some time. It takes time to do a DNS lookup and start a connection. And you can't beat physics: a request has to travel through the real world over wires and optical cables to reach a server, then the response has to make the trip back. Even the median origin with good LCP spends more than half a second on TTFB at its 75th percentile.

However, the TTFB disparity between the good and poor LCP origins shows the opportunity for improvement. For at least half of the origins with poor LCP, the p75 TTFB of 2,270 milliseconds alone nearly guarantees that the p75 LCP can't be faster than the 2.5 second "good" threshold. Even a moderate percentage reduction of that time would mean a significant LCP improvement.

You may not be able to beat physics, but there are things that can be done. For example, if your users are often in a very different location than your servers, a CDN can get your content closer to them.

For more, see the Optimizing TTFB guide.

Resource load delay, the overlooked slow LCP culprit

If TTFB can be improved but any improvements are bound by physics, resource load delay can potentially be eliminated, in practice only bound by your serving architecture.

This sub-part measures the time from the arrival of the first byte of the HTML response (TTFB) to when the browser starts a request for the LCP image. We've been focused for years on how long it takes to download LCP images, but we've often ignored the time wasted before the browser is even told to start the download.

The median site with poor LCP spends almost four times as long waiting to start downloading the LCP image as it does actually downloading it, waiting 1.3 seconds between TTFB and image request. That's more than half of the 2.5 second LCP budget gone in a single sub-part.

Dependency chains are a common reason for long load delays. On the simpler end is a page loading a style sheet, which, after the browser does layout, sets a background image which will end up being the LCP. All those steps have to happen before the browser even knows to start downloading the LCP image.

Using HTTP Archive public crawl data, which records the "initiator" chain of network requests from the HTML document to an LCP image, you can see the clear correlation of request chain length with slower LCP.

A graph visualizing the relationship of dependent request chains with LCP. The median LCP goes up from 2150 milliseconds with 0 dependent requests, to 2540 milliseconds with 1 dependent request, to 2850 milliseconds with 2 dependent requests
The relationship of dependent request chains with LCP.

The key is to let the browser know as soon as possible what the LCP will be so it can start loading it, even before there's a place for it in the page's layout. There are a few tools available to accomplish this, like a classic <img> tag in the HTML so the preload scanner can find it quickly and start downloading it, or a <link rel="preload"> tag (or HTTP header) for images that won't be <img>s.

It's also important to help the browser determine which resources to prioritize. This is especially true if your page is making lots of requests during page load, as the browser often won't know what the LCP element will be until after many of those resources have loaded and layout has occurred. Annotating the probable LCP element with a fetchpriority="high" attribute (and making sure to avoid loading="lazy") makes it clear to the browser to start loading the resource immediately.

Read more advice about optimizing load delay.

Render delay

Render delay measures the time from when the browser has the LCP image loaded and ready, but for some reason there's a delay before it's shown on screen. Sometimes this is a long task blocking the main thread when the image is ready, in other cases it may be a UI choice to reveal a hidden element.

For the typical origin on the web there doesn't appear to be a huge render delay opportunity, but during optimization you can sometimes create render delay out of time previously spent in other sub-parts. For example, if a page starts preloading the LCP image so it's available quickly there will no longer be a long load delay, but if the page itself isn't ready to display the image—like from a large render-blocking style sheet or a client-side rendering app that has to finish loading all its JavaScript before anything can be displayed—LCP will still be slower than it should be, and the time spent waiting will now show up as render delay. This is why server side rendering or static HTML often has an advantage when it comes to LCP.

If your own content is affected, read more advice about optimizing render delay.

Check all those sub-parts

It's clear that to effectively optimize LCP, developers need to look at the page load holistically, and not just focus on optimizing images. Check every part of the time to LCP, because there's likely much larger opportunities for improvement.

For gathering this data in the field, the web-vitals library's attribution build includes timings for the LCP sub-parts. The Chrome User Experience Report (CrUX) doesn't yet include all this data, but it does have entries for TTFB and LCP, so it's a start. We hope to include the data used for this post in CrUX in the future, so stay tuned for more news on that.

For testing LCP sub-parts locally, try the Web Vitals extension or the JavaScript snippet in this article. Lighthouse also includes the breakdown in its "Largest Contentful Paint element" audit. Look for more LCP sub-part advice in the DevTools Performance panel, coming soon.