Optimize Largest Contentful Paint

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

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

Good LCP values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement
A good LCP value is 2.5 seconds or less.

A number of factors can affect how quickly the browser can load and render a web page, and delays across any of them can have a significant impact on LCP.

It's rare that a quick fix to a single part of a page will result in a meaningful improvement to LCP. To improve LCP you have to look at the entire loading process and make sure every step along the way is optimized.

Understand your LCP metric

Before optimizing LCP, developers must understand whether their site has an LCP issue, and if so to what extent.

A number of tools can measure LCP, but not all of them measure it in the same way. To understand real users' LCP experience, you must understand what real users are experiencing, rather than only what a lab-based tool like Lighthouse or local testing can show. These lab-based tools can give a wealth of information to explain LCP and help you improve your metrics, but lab tests alone aren't entirely representative of what your users experience.

You can surface LCP data based on real users from Real User Monitoring (RUM) tools installed on a site, or through the Chrome User Experience Report (CrUX), which collects anonymous data from real Chrome users for millions of websites.

Use CrUX data in PageSpeed Insights

PageSpeed Insights provides access to CrUX data in the Discover what your real users are experiencing section. More detailed lab-based data is available in the Diagnose performance issues section. Always focus on the CrUX data first if it's available.

CrUX data shown in PageSpeed Insights
CrUX data shown in PageSpeed Insights.

Where CrUX doesn't provide data (for example, for a page with insufficient traffic to get page-level data), you can supplement CrUX with RUM data collected using JavaScript APIs running on the page. This can also provide much more data than CrUX can expose as a public dataset. Later in this guide, we'll explain how to collect this data using JavaScript.

LCP data

PageSpeed Insights shows up to four different CrUX data sets:

  • Mobile data for This URL
  • Desktop data for This URL
  • Mobile data for the whole Origin
  • Desktop data for the whole Origin

You can toggle these in the controls at the top and top right-hand side of this section. If a URL doesn't have enough data to be shown at the URL level, but does have data for the origin, PageSpeed Insights always shows the origin data.

PageSpeed Insight's falling back to origin-level data where url-level data is not available
When PageSpeed Insights doesn't have URL-level data, it shows origin-level data.

The LCP for the whole origin might be very different to an individual page's LCP, depending on how the LCP is loaded on that page compared to other pages on that origin. It can also be affected by how visitors navigate to these pages. Home pages tend to be visited by new users and are therefore often loaded without any cached content, making them the slowest pages on a website.

Looking at the four different categories of CrUX data can help you understand whether an LCP issue is specific to this page, or a more general site-wide issue. Similarly, it can show which device types have LCP issues.

Supplementary metrics

Developers working on optimizing LCP can also use the First Contentful Paint (FCP) and Time to First Byte (TTFB) timings, which are good diagnostic metrics that can provide valuable insights into LCP.

TTFB is the time from the visitor starting to navigate to a page (for example, clicking a link), until the first bytes of the HTML document are received. A high TTFB can make achieving a 2.5 second LCP challenging or even impossible.

A high TTFB can be caused by multiple server redirects, visitors located far from the nearest site server, visitors on poor network conditions, or an inability to use cached content due to query parameters.

Once a page starts rendering, there might be an initial paint (for example, the background color), followed by some content appearing (for example, the site header). The appearance of the initial content is measured by FCP, and the difference between FCP and other metrics can be very telling.

A large difference between TTFB and FCP might indicate that the browser needs to download a lot of render-blocking assets. It can also be a sign that the browser must complete a lot of work to render any meaningful content, suggesting that the site relies heavily on client-side rendering.

A large difference between FCP and LCP indicates that the LCP resource is either not immediately available for the browser to prioritize (for example, text or images that are managed by JavaScript rather than being available in the initial HTML), or that the browser must complete other work before it can display the LCP content.

Use PageSpeed Insights Lighthouse data

The Lighthouse section of PageSpeed Insights offers some guidance to improving LCP, but first you should check if the LCP given is broadly in agreement with real user data provided by CrUX. If Lighthouse and CrUX disagree, then CrUX likely provides a more accurate picture of your user experience. Make sure your CrUX data is for your page, not the full origin, before you act on it.

If both Lighthouse and CrUX show LCP values that need improvement, the Lighthouse section can provide valuable guidance on ways to improve LCP. Use the LCP filter to only show audits relevant to LCP as follows:

Lighthouse LCP Opportunities and Diagnostics
Lighthouse diagnostics and suggestions for improving LCP.

As well as the Opportunities to improve, there is Diagnostic information that can provide more information to help diagnose the issue. The Largest Contentful Paint element diagnostic shows a useful breakdown of the various timings that made up the LCP:

Lighthouse LCP phases
Lighthouse's breakdown of LCP elements.

The next section explores subcategories of LCP in more detail.

LCP breakdown

This section presents a methodology breaking down LCP into its most important subcategories, along with specific recommendations and best practices for optimizing each subcategory.

Most page loads typically include several network requests, but for the purposes of identifying opportunities to improve LCP, we recommend starting with only the initial HTML document, and if applicable, the LCP resource.

While other requests on the page can affect LCP, these two requests—specifically the times when the LCP resource begins and ends—reveal whether or not your page is optimized for LCP.

To identify the LCP resource, you can use developer tools, such as PageSpeed Insights, Chrome DevTools, or WebPageTest, to determine the LCP element. From there, you can match the URL (if applicable) loaded by the element on a network waterfall of all resources loaded by the page.

For example, the following visualization shows these resources highlighted on a network waterfall diagram from a typical page load, where the LCP element requires an image request to render.

A network waterfall with the HTML and LCP resources highlighted
A waterfall diagram showing the loading times for a webpage's HTML and the resources the LCP needs.

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 a particular page follows this principle, you can break down the total LCP time into the following subcategories:

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 time
The 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.

Every page's LCP consists of these four subcategories. There's no gap or overlap between them, and they add up to the full LCP time.

A breakdown of LCP showing the four subcategories
The same waterfall diagram, with the four LCP subcategories overlaid on the timeline.

When optimizing LCP, it's helpful to try to optimize these subcategories However, you need to make sure all of them are optimized, because some optimizations shift the time saved on one part to another instead of actually reducing LCP.

For example, in the network waterfall example, reducing the file size of the image by compressing it more or switching to a more optimal format (such as AVIF or WebP) would reduce the resource load time, but it wouldn't improve LCP because that time becomes part of the element render delay. This is because the LCP element is hidden until its associated JavaScript finishes loading, after which it's revealed.

The same breakdown of LCP shown earlier where the resource load time subcategory is shortened but the overall LCP time remains the same.
Shortening the resource load time increases the element render delay without reducing LCP.

Optimal subcategory times

To optimize each subcategory of LCP, it's important to understand what the ideal breakdown of these subcategories is on a well-optimized page.

The two subcategories involving delays must be reduced as much as possible. The other two involve network requests, which inherently take time and can't be optimized away completely.

The following is an idealized LCP distribution.

LCP sub-part % of LCP
Time to first byte (TTFB) ~40%
Resource load delay <10%
Resource load time ~40%
Element render delay <10%
TOTAL 100%

These times are guidelines, not strict rules. If your pages' LCP times are consistently 2.5 seconds or less, then it doesn't really matter what the breakdown looks like. However, if your delay categories are unnecessarily long, you'll have trouble reaching the 2.5 second target.

We recommend thinking about the LCP time breakdown as follows:

  • The vast majority of the LCP time must be spent loading the HTML document and LCP source.
  • Any time before LCP in which one of these two resources isn't loading is an opportunity to improve.

How to optimize each category

Now that you understand how the LCP subcategory times look on a well-optimized page, you can start optimizing your own pages.

The following sections present recommendations and best practices optimizing each category, starting with the optimizations that are likely to have the biggest impact.

Eliminate resource load delay

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

A good rule of thumb is to make sure your LCP resource starts at the same time as the first resource the page loads.

A network waterfall diagram showing the LCP resource starting after the first resource, showing the opportunity for improvement
On this page, the LCP resource starts loading well after the style sheet that loads first. There's room for improvement here.

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

  • 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, that resource must be discoverable in the initial HTML document response by the browser's preload scanner. Some examples of discoverable LCP resources include the following:

  • An <img> element whose src or srcset attributes are in the initial HTML markup.
  • Any element requiring a CSS background image, as long as that image is preloaded by <link rel="preload"> in the HTML markup (or using a Link header).
  • A text node that requires a web font to render, as long as the font is preloaded by <link rel="preload"> in the HTML markup (or using a Link header).

Here are some LCP resources that can't be discovered by scanning the HTML document response. In each case, the browser has to run a script or apply a style sheet before it can discover and start loading the LCP resource, requiring it to wait for network requests to finish.

  • An <img> dynamically added to the page using JavaScript.
  • Any element that's lazily loaded using a JavaScript library that hides its src or srcset attributes (often as data-src or data-srcset).
  • Any element that requires a CSS background image.

To eliminate unnecessary resource load delay, your LCP resource must be discoverable from the HTML source. In cases where the resource is only referenced from an external CSS or JavaScript file, the LCP resource must 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 might 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, which often causes it to load later than it otherwise would.

Even without lazy loading, browsers don't initially load images with high priority because they're not render-blocking resources. You can increase a resource's loading priority using the fetchpriority attribute as follows:

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

It's a good idea to set fetchpriority="high" on an <img> element if you think it's likely to be your page's LCP element. However, setting a high priority on more than one or two images makes priority setting unhelpful in reducing LCP.

You can also lower the priority of images that might be early in the document response but aren't visible due to styling, such as images in carousel slides that aren't visible at startup:

<img fetchpriority="low" src="/path/to/carousel-slide-3.webp">

Deprioritizing certain resources can provide more bandwidth to resources that need it more, but be careful not to overdo it. Always check resource priority in DevTools and test your changes with lab and field tools.

After you've 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):

A network waterfall diagram showing the LCP resource now starting at the same time as the first resource
The LCP resource now starts loading at the same time as the style sheet.

Key point: Another reason your LCP resource might not start loading as early as possible, even when it's discoverable from the HTML source, is if it's hosted on a different origin that the browser must connect to before it can start loading the resource. When possible, we recommend hosting critical resources on the same origin as your HTML document resource so that the browser can reuse the existing connection to save time (more on this point later).

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 hasn't yet been added to the DOM because it's waiting for JavaScript code to load.
  • The element is hidden by some other code, such as an A/B testing library that hasn't yet decided which experimental group to put the user in.
  • The main thread is blocked due to long tasks, and rendering work has to wait until those long tasks complete.

The following sections explain how to address the most common causes of unnecessary element render delay.

Reduce or inline render-blocking style sheets

Style sheets loaded from the HTML markup block rendering of all content that follows them. This is usually a good thing, because it allows the style sheet to take effect before other elements load. However, if the style sheet is so large that it takes significantly longer to load than the LCP resource, then it prevents the LCP element from rendering even after its resource has finished loading, as shown in this example:

A network waterfall diagram showing a large CSS file blocking rendering of the LCP element because it takes longer to load than the LCP resource
The image and the style sheet start loading at the same time, but the image can't render until the style sheet is ready.

To fix this, you can either:

  • inline the style sheet into the HTML to avoid the additional network request; or,
  • reduce the size of the style sheet.

Inlining your style sheet is only effective for reducing LCP if the style sheet is small. However, if the style sheet takes longer to load than your LCP resource, it's probably too big to inline effectively, so we recommend reducing the complexity of your style sheet as follows:

  • Remove unused CSS: use Chrome DevTools to find CSS rules that aren't being used and can potentially be removed (or deferred).
  • Defer non-critical CSS: split your style sheet out into styles that for initial page load and then styles that can be loaded lazily.
  • Minify and compress CSS: for styles that are critical, make sure to reduce their transfer size as much as possible.

Defer or inline render-blocking JavaScript

We recommend making all scripts on your pages asynchronous, using the async or defer attributes. Using synchronous scripts is almost always bad for performance.

However, if you have JavaScript that needs to run as early as possible in the page load, you can improve LCP by inlining small scripts to reduce the time the browser spends waiting for network requests.

Do
<head>
  <script>
    // Inline script contents directly in the HTML.
    // IMPORTANT: only do this for very small scripts.
  </script>
</head>
Don't
<head>
  <script src="/path/to/main.js"></script>
</head>

Use server-side rendering

Server-side rendering (SSR) is the process of running your client-side application logic on the server and responding to HTML document requests with the full HTML markup.

SSR helps optimize LCP in the following ways:

  • It makes your resources discoverable from the HTML source, as discussed in Eliminate resource load delay.
  • It prevents your page from needing additional JavaScript requests to finish before it can render.

The main downside of SSR is that it requires additional server processing time, which can slow down your TTFB. However, this trade-off is usually worth it because server processing times are within your control, whereas the network and device capabilities of your users are not.

We also recommend generating your HTML pages in a build step instead of on demand for better performance. This practice is called static site generation (SSG) or prerendering.

Break up long tasks

Even if you've followed all of this advice, and your JavaScript code isn't render-blocking or responsible for rendering your elements, it can still delay LCP.

The most common reason is that when a page loads a large JavaScript file, it takes time for the browser to parse and execute the code on its main thread. This means that, even if the LCP resource is fully downloaded, it might still have to wait to render until an unrelated script finishes executing.

All browsers render images on the main thread, which means that anything blocking the main thread can also lead to unnecessary element render delay. Therefore, we recommend breaking up a large JavaScript file into multiple script files that can each be parsed as needed.

Reduce resource load time

The goal of this step is to reduce the time the browser spends transferring the resource over the network to the user's device. In general, there are a few ways to do this:

  • Reduce the size of the resource.
  • Reduce the distance the resource has to travel.
  • Reduce contention for network bandwidth.
  • Eliminate the network time entirely.

Reduce the size of the resource

LCP resources are usually images or web fonts. The following guides provide details on how to reduce the sizes of both:

Reduce the distance the resource has to travel

You can also reduce load times by locating your servers as geographically close to your users as possible. The best way to do this is using a content delivery network (CDN).

In fact, image CDNs in particular are especially helpful because they both reduce the distance the resource has to travel and often reduce the size of the resource following the strategies mentioned previously.

Key point: Image CDNs are a great way to reduce resource load times. However, using a third-party domain to host your images comes with an additional connection cost. While preconnecting to the origin can mitigate some of this cost, the best option is to serve images from the same origin as your HTML document. To allow this, many CDNs allow you to proxy requests from your origin to theirs.

Reduce contention for network bandwidth

If your page loads many resources at the same time, any one resource might take a long time to load. This problem is known as network contention.

If you've given your LCP resource a high fetchpriority and started loading it as soon as possible, the browser does its best to prevent lower-priority resources from competing with it. However, loading too many resources at once can still affect LCP, especially if many of those resources have high fetchpriority. We recommend reducing network contention by making sure that the only resources with high fetchpriority are those that need to load most quickly.

Eliminate the network time entirely

The best way to reduce resource load times is to eliminate the network from the process entirely. If you serve your resources with an efficient cache-control policy, visitors who request those resources a second time will have them served from the cache, reducing the resource load time to essentially zero.

If your LCP resource is a web font, in addition to reducing web font size, we recommend considering whether you need to block rendering on the web font resource load. If you set a font-display value of anything other than auto or block, text is always visible during load, and LCP doesn't have to wait for an extra network request.

Finally, if your LCP resource is small, it might make sense to inline the resources as a data URI to eliminate the additional network request. However, using data URIs has its drawbacks: it prevents resources from being cached, and can cause longer render delays in some cases because of the additional decode cost.

4. Reduce time to first byte

The goal of this step is to deliver the initial HTML as quickly as possible. This step is listed last because it's usually the one developers have the least control over. However, it's also one of the most important steps because it directly affects every step that comes after it. Nothing can happen on the frontend until the backend delivers that first byte of content, so anything you can do to speed up your TTFB will also improve every other load metric.

A common cause of a slow TTFB for an otherwise fast site is visitors arriving through multiple redirects, such as from advertisements or shortened links. Always minimize the number of redirects a visitor must wait through.

Another common cause is when cached content can't be used from a CDN edge server, requiring and all requests to be directed all the way back to the origin server. This can happen if visitors use unique URL parameters for analytics, even if they don't result in different pages.

For specific guidance on reducing TTFB, see Optimize TTFB.

Monitor LCP breakdown in JavaScript

The timing information for all LCP subcategories is available in JavaScript through a combination of the following performance APIs:

Computing these timing values in JavaScript lets you send them to an analytics provider or log them to your developer tools to help with debugging and optimizing. For example, the following screenshot uses the performance.measure() method from the User Timing API to add bars to the Timings track in the Chrome DevTools Performance panel:

User Timing
  measures of the LCP subcategories visualized in Chrome DevTools
The Timings track shows timelines for the LCP subcategories.

Visualizations in the Timings track are particularly helpful alongside the Network and Main thread tracks, which let you see what else is happening on the page during these timespans.

You can also use JavaScript to compute what percentage of the total LCP time each subcategory takes up, to determine whether your pages meet the recommended percentage breakdowns.

This screenshot shows an example that logs the total time of each LCP subcategory to the console, as well as its percentage of the total LCP time.

The LCP subcategory times, as well as their percent of LCP, printed to the console
LCP subcategory timings and percentages.

Both of these visualizations were created with the following code:

const LCP_SUB_PARTS = [
  'Time to first byte',
  'Resource load delay',
  'Resource load time',
  'Element render delay',
];

new PerformanceObserver((list) => {
  const lcpEntry = list.getEntries().at(-1);
  const navEntry = performance.getEntriesByType('navigation')[0];
  const lcpResEntry = performance
    .getEntriesByType('resource')
    .filter((e) => e.name === lcpEntry.url)[0];

  // Ignore LCP entries that aren't images to reduce DevTools noise.
  // Comment this line out if you want to include text entries.
  if (!lcpEntry.url) return;

  // Compute the start and end times of each LCP sub-part.
  // WARNING! If your LCP resource is loaded cross-origin, make sure to add
  // the `Timing-Allow-Origin` (TAO) header to get the most accurate results.
  const ttfb = navEntry.responseStart;
  const lcpRequestStart = Math.max(
    ttfb,
    // Prefer `requestStart` (if TOA is set), otherwise use `startTime`.
    lcpResEntry ? lcpResEntry.requestStart || lcpResEntry.startTime : 0
  );
  const lcpResponseEnd = Math.max(
    lcpRequestStart,
    lcpResEntry ? lcpResEntry.responseEnd : 0
  );
  const lcpRenderTime = Math.max(
    lcpResponseEnd,
    // Use LCP startTime (the final LCP time) because there are sometimes
    // slight differences between loadTime/renderTime and startTime
    // due to rounding precision.
    lcpEntry ? lcpEntry.startTime : 0
  );

  // Clear previous measures before making new ones.
  // Note: due to a bug, this doesn't work in Chrome DevTools.
  LCP_SUB_PARTS.forEach((part) => performance.clearMeasures(part));

  // Create measures for each LCP sub-part for easier
  // visualization in the Chrome DevTools Performance panel.
  const lcpSubPartMeasures = [
    performance.measure(LCP_SUB_PARTS[0], {
      start: 0,
      end: ttfb,
    }),
    performance.measure(LCP_SUB_PARTS[1], {
      start: ttfb,
      end: lcpRequestStart,
    }),
    performance.measure(LCP_SUB_PARTS[2], {
      start: lcpRequestStart,
      end: lcpResponseEnd,
    }),
    performance.measure(LCP_SUB_PARTS[3], {
      start: lcpResponseEnd,
      end: lcpRenderTime,
    }),
  ];

  // Log helpful debug information to the console.
  console.log('LCP value: ', lcpRenderTime);
  console.log('LCP element: ', lcpEntry.element, lcpEntry.url);
  console.table(
    lcpSubPartMeasures.map((measure) => ({
      'LCP sub-part': measure.name,
      'Time (ms)': measure.duration,
      '% of LCP': `${
        Math.round((1000 * measure.duration) / lcpRenderTime) / 10
      }%`,
    }))
  );
}).observe({type: 'largest-contentful-paint', buffered: true});

You can use this code as-is for local debugging, or modify it to send this data to an analytics provider so you can get a better understanding of what your pages' LCP breakdown is for real users.

Monitor LCP breakdown using the Web Vitals extension

The Web Vitals extension logs the LCP time, LCP element, and the four subcategories in the console logging, to show this breakdown.

Screenshot of the console logging of the Web Vitals extension showing the LCP sub-part timings
The Console panel for the Web Vitals extension shows the LCP breakdown.