Sommaire

Largest contentful paint (LCP): what is it?

Largest contentful paint (LCP): what is it?

Largest Contentful Paint (LCP) is part of  Google’s Core Web Vitals  , the essential metrics for user experience. It helps assess the perception of speed by indicating when the main content of the page is displayed. In UX terms, a good LCP helps reassure that the content of the page is relevant.

How to measure the speed at which the main content of a page is displayed on a browser? This is a topic of questioning for web developers…

Old metrics such as load or DOMContentLoaded are not enough because they do not reflect what the Internet user sees on their screen (NdT: we regularly discuss the evolutions of webperf metrics in our monthly newsletter).

Additionally, newer, user-centric performance metrics like  First Contentful Paint (FCP)  only capture the very beginning of the loading process. As such, if a page displays a  splash screen  or a loading indicator, the FCP won’t truly represent what the user sees as they navigate.

In the past, Google has recommended First Meaningful Paint and  Speed ​​Index  (available in  Google Lighthouse ), but they also don’t help identify when the main content is loaded.

After research and discussions with the W3C, Google has agreed that the best way to gauge when the main content of a page is rendered is to look at  when the largest element on the page is rendered .  This is the Largest Contentful Paint.

What is LCP? Definition.

The LCP (Largest Contentful Paint) metric indicates the rendering time of the largest visible content element in the  viewport .

For a good user experience, a website should make the Largest Contentful Paint happen within the  first 2.5 seconds  after the page starts loading.
But how do you know if this goal is being achieved for most users? The best way is to refer to the 75th percentile of your page load times, for both mobile and  desktop devices , according to  Google’s recommendations .

 

What are the elements taken into account for the calculation of the Largest Contentful Paint?

As specified in the Largest Contentful Paint API, the elements taken into account for the calculation of the LCP are the following:

  • <img> elements 
  • <image> elements within an <svg> element 
  • <video> elements (thumbnail used)
  • elements with a background image loaded via the url() function (as opposed to a  CSS gradient )
  • Block-level elements containing  text nodes or dependent inline   text elements  .

 

NB: Google intentionally restricts the elements considered in the LCP calculation to promote simplicity. Elements will be added over time, such as <svg>, <video>…

 

How is the size of an element determined for measuring LCP? 

The size of the element reported for LCP is usually the size visible to the user in the  viewport . If the element extends outside the viewport, or if any of the elements are clipped or overflow , those parts do not count.

 

For image elements  resized  from their intrinsic size (Intrisic Size), the size that is indicated is either the visible size or the intrinsic size, the smaller of the two will be retained.

 

For example :

  • for images reduced to a size much smaller than their intrinsic size, it is the size at which they are displayed that is taken into account;
  • for images that are stretched or expanded to a larger size, the intrinsic size is taken into account.

 

For text elements, only the size of their  text node  is taken into account (the smallest rectangle that includes all  text nodes ).

 

For all elements, no  margin ,  padding  or  border  applied via CSS is taken into account.

 

NB: Determining which  text nodes  belong to which elements can be tricky, especially for elements whose dependencies include  inline elements  and  raw text nodes  , but also block-level elements. The point to remember is that each  text node  belongs to (and only to) its nearest block-level parent element. According to the spec: each  text node  belongs to the element that generates its containing block.

 

When is the largest image on a page considered?

Web pages often load in stages and therefore it is possible that the largest element on the page may vary.

 

To handle this, the browser sends a PerformanceEntry of type largest-contentful-paint, identifying the largest element on the page as soon as the browser renders the first frame. Then, after rendering subsequent frames, it generates a new PerformanceEntry each time the largest content element changes.

 

Be aware that an element can only be considered largest once it is rendered and visible to the user.

 

So, images that have not yet been loaded are not considered “rendered”, just like  text nodes  that do not use  fonts  during the  font block period . In such cases, a smaller element may be reported as the largest, but as soon as the larger element is finished rendering, it will be reported via another PerformanceEntry object.

 

In addition to images and fonts that load later, a page may add new elements to the DOM as new content becomes available. If any of these new elements are larger than the previous largest content element, a new PerformanceEntry will also be reported.

 

If a page removes an element from the DOM, that element will no longer be considered. Similarly, if the image resource associated with an element changes (e.g. changing img.src via JavaScript), then that element will be broken until the new image loads.

 

The browser stops reporting new  inputs  as soon as the user interacts with the page (touch, scroll, or keypress), because the interaction often changes what is visible to the user (especially when scrolling  ) .

 

In terms of analytics, you should only report the last PerformanceEntry sent to your analytics tool.

 

NB: Since users can open pages in a tab in the background, it is possible that the Largest Contentful Paint will not occur until the user returns to a given tab, and thus arrive much later than when it first loads.

 

Difference between loading time and rendering time 

For security reasons, the image rendering timestamp is not exposed for  cross-origin images  that do not have the Timing-Allow-Origin header. Only their loading time is exposed (as it is already exposed via many other web APIs).

 

The example we’ll see below (in “Measuring LCP in JavaScript”) shows how to handle elements whose render time is not available. But if possible, it’s always recommended to set the Timing-Allow-Origin header so that your measurements are more accurate.

 

LCP: How are element layouts and size changes handled? 

To limit computational costs, changes to the size or position of an element do not generate new “candidates” as the most important element on the page. Only the initial size and position of the element in the  viewport  are taken into account.

 

This means that images that are initially rendered out of  viewport  and then end up in viewport may not be reported. Also, elements that were initially rendered in the  viewport  and are then pushed down out of view will continue to be reported at their original size.

 

However, (as mentioned above) an element may no longer be considered if it is removed from the DOM or its associated image resource changes.

 

Examples of Largest Contentful Paint

Here are some examples of when Largest Contentful Paint occurs:

In both  timelines  above, the largest element changes as the content loads. In the first example, new content is added to the DOM, which changes which element is considered largest. In the second example, the layout changes and the previously largest content is removed from the viewport.

 

Although content loaded later on a page is often the largest, this is not always the case. This is seen in the following two examples where the largest content is visible before the page is fully loaded:

In the first example, the Instagram logo is loaded relatively early and remains the most important element even as other content is gradually displayed. 

 

In the Google search results page example, the largest element is a paragraph of text that appears before the images or logo load. Since all the images are smaller than this paragraph, this block of text remains the largest element throughout the loading process.

 

NB: In the first frame of the  Instagram timeline  , you may notice that the camera logo is not highlighted in green. This is because it is an <svg> element and <svg> elements are not currently considered to be potentially the largest elements considered for measuring LCP. So the first “candidate” for LCP is the text in the second frame.

 

How to measure Largest Contentful Paint?

LCP can be measured by  Lab  or  Field tools  :

LES OUTILS FIELD (REAL USER MONITORING)

LES OUTILS LAB (SYNTHETIC MONITORING)

MEASURING LCP IN JAVASCRIPT 

The easiest way to measure LCP (like all  Web Vitals Field  metrics ) is with the web-vitals JavaScript library, which simplifies manual LCP measurement into a single function:

				
					import {getLCP} from 'web-vitals';
// Measure and log the current LCP value,
// any time it's ready to be reported.
getLCP(console.log);
				

To manually measure LCP, you can use the Largest Contentful Paint API. The following example shows how to create a PerformanceObserver that tracks the largest items’ records and  logs , and reports the LCP value to the console:

				
					// Keep track of whether (and when) the page was first hidden, see:
// https://github.com/w3c/page-visibility/issues/29
// NOTE: ideally this check would be performed in the document <head>
// to avoid cases where the visibility state changes before this code runs.
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
document.addEventListener('visibilitychange', (event) => {
 firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
}, {once: true});
// Sends the passed data to an analytics endpoint. This code
// uses `/analytics`; you can replace it with your own URL.
function sendToAnalytics(data) {
 const body = JSON.stringify(data);
 // Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
 (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
     fetch('/analytics', {body, method: 'POST', keepalive: true});
}
// Use a try/catch instead of feature detecting `largest-contentful-paint`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
 // Create a variable to hold the latest LCP value (since it can change).
 let lcp;
 function updateLCP(entry) {
   // Only include an LCP entry if the page wasn't hidden prior to
   // the entry being dispatched. This typically happens when a page is
   // loaded in a background tab.
   if (entry.startTime < firstHiddenTime) {
     // NOTE: the `startTime` value is a getter that returns the entry's
     // `renderTime` value, if available, or its `loadTime` value otherwise.
     // The `renderTime` value may not be available if the element is an image
     // that's loaded cross-origin without the `Timing-Allow-Origin` header.
     lcp = entry.startTime;
   }
 }
	
 // Create a PerformanceObserver that calls `updateLCP` for each entry.
 const po = new PerformanceObserver((entryList) => {
   entryList.getEntries().forEach(updateLCP);
 });
 // Observe entries of type `largest-contentful-paint`, including buffered entries,
 // i.e. entries that occurred before calling `observe()` below.
 po.observe({
   type: 'largest-contentful-paint',
   buffered: true,
 });
	
 // Log the final LCP score once the
 // page's lifecycle state changes to hidden.
 addEventListener('visibilitychange', function fn(event) {
   if (document.visibilityState === 'hidden') {
     removeEventListener('visibilitychange', fn, true);
     // Force any pending records to be dispatched.
     po.takeRecords().forEach(updateLCP);
     // If LCP is set, report it to an analytics endpoint.
     if (lcp) {
       sendToAnalytics({lcp});
     }
   }
 }, true);
} catch (e) {
 // Do nothing if the browser doesn't support this API.
}
				

NB: LCP should not be reported if the page was loaded in a background tab. The above code partially solves this problem, but it is not perfect as the page could have been hidden and then shown before this code was executed. A solution is being discussed on Page Visibility API spec.

 

WHY IS THE LARGEST CONTENTFUL PAINT DIFFERENT BETWEEN LAB AND FIELD DATA ON PAGESPEED INSIGHTS?

The different LCP elements

The LCP element identified for Lab data may not be the same one that users see when they visit your page. If you run a Lighthouse report for a given page, you’ll get the same LCP element every time. But if you look at the Field data for the same page, you’ll typically find a variety of different LCP elements, depending on the specific circumstances of each page visit.

 

For example, the following factors can all contribute to determining a different LCP element for the same page:

  • Different  screen sizes  of devices cause different elements to be visible in the viewport.
  • If  the user is logged in , or if personalized content is displayed in some way, the LCP element may be very different from one user to another.
  • Similar to the previous point, if an  A/B test  is running on the page, very different elements may be displayed.
  • The font set  installed  on the user’s system can affect the size of the text on the page (and therefore which element is the LCP element).
  • Lab tests are typically performed on the “base” URL of a page, without adding  query parameters or hash fragments . But in the real world, users often share URLs that contain a fragment ID or text fragment, so the LCP element may actually be in the middle or bottom of the page (rather than above the fold).

 

Since LCP in Field data is calculated based on the 75th percentile of all visits to a page, if a large percentage of those users have an LCP element that loads very quickly – for example, a paragraph of text rendered in a system font – even if some of those users have a large image that loads slowly as an LCP element, this may not affect that page’s score as long as it happens to less than 25% of visitors.

 

The reverse can also be true. With Lab data, you might identify a text block as the LCP element because it emulates a Moto G4 phone with a relatively small viewport, and your page’s main image is initially rendered off-screen. Your Field data, however, might include mostly Pixel XL users with larger screens, so for them that slow-loading main image is their LCP element.

 

The effect of cache state on LCP

Lab data usually corresponds to a page with a  “cold” cache , but when real users visit that page, some of its resources may already be cached.

 

The first time a user loads a page, it may load slowly, but if the page is cached properly, the next time that user returns, the page will load faster.

 

While some Lab tools allow the same page to be run multiple times (to simulate the experience of returning visitors), it is not possible for a Lab tool to know what percentage of real-world visits are from new users versus returning ones.

 

Sites with well-optimized cache configurations and lots of repeat visitors may find that their real-world LCP is much faster than the lab data indicates.

 

AMP or Signed Exchange optimizations 

AMP sites or sites that use  Signed Exchange  (SXG) can be preloaded by content aggregators like Google Search. This can result in significantly better loading speeds for users visiting your pages from these platforms.

 

In addition to cross-origin prefetching, sites themselves can prefetch content for subsequent pages on their site, which could improve LCP for those pages as well.

 

Lab tools don’t simulate the gains you get from these optimizations, and even if they did, they wouldn’t be able to tell  what percentage of your traffic  is coming from platforms like Google Search versus other sources.

 

The effect of  bfcache  on LCP

When pages are restored from the  bfcache , the loading experience is near instantaneous, and these experiences are included in your Field data.

 

The Lab data does not take  bfcache into account, so if your pages are bfcache friendly  , this will likely result in faster LCP scores in the Field data.

 

The effect of user interactions on LCP

The LCP identifies the rendering time of the largest image or block of text in the viewport, but this largest element can change as the page loads or if new content is dynamically added to the viewport.

 

For lab data, the browser will wait until the page is fully loaded before determining what the LCP element was. But for data collected from real users, the browser will stop monitoring larger elements after the user scrolls or interacts with the page.

 

This makes sense (and is necessary) because users typically wait to interact with a page until it “appears” to be loaded, which is exactly what the LCP metric aims to detect. It would also make no sense to consider elements added to the viewport after the user interaction, since these elements might only have loaded because of a user action.

 

The implication is that Field data for a page may report faster LCP times, depending on how users behave on that page.

 

Field or Lab: which metrics should be prioritized to optimize LCP?

As a general rule, if you have both field data and lab data for a web page, the field data is what you should use to prioritize your efforts. Since field data represents what real users experience, it’s the most accurate way to truly understand what’s causing problems for your users and what needs improvement.

 

On the other hand, if your field data shows good results overall, but your lab data suggests there is still room for improvement, it is worth digging deeper to understand what additional optimizations can be made.

 

What if the element measured by the LCP is not the most important?

In some cases, the most important element(s) are not the largest, and it may be more interesting to measure the rendering of these most important elements. This is possible thanks to the Element Timing API, by calling Custom Metrics.

 

How to improve Largest Contentful Paint

LCP can be degraded by several factors:

  • long server response times,
  • render-blocking JavaScript or CSS,
  • resource loading time,
  • client-side rendering.

 

Here are the techniques to improve LCP in detail  .
Google also offers additional techniques to improve LCP (in English):

  • Applying instant loading with the PRPL pattern
  • Optimize the critical rendering path
  • Optimize CSS
  • Optimize images
  • Optimize Web Fonts
  • Optimize JavaScript (for client-side rendered sites)

 

Follow the evolutions of the Largest Contentful Paint

Google is fixing and evolving the APIs used to measure metrics, as well as the definition of the metrics themselves. These changes may appear as improvements or regressions in your internal reports and dashboards. To help you track these developments, the changes are logged in this  CHANGELOG .

 

Want to explore the topic in video? The replay of our webinar dedicated to LCP is here:

Want to know how to automate optimizations that will improve your LCP and all of your Core Web Vitals?

 


Ask a demo

Summary
Share!
Recevoir la newsletter

Published by

Partagez !

Discover other articles…

But stay informed!

Register!

Every month, no more, receive:

🆕 The latest publications on our blog: platform developments, new features, events, technical advice, analyses…

💡 A selection of monitoring articles: technical news, tips, tutorials, and other findings on web performance…