--- layout: post title: "Optimising Largest Contentful Paint" date: 2022-03-28 23:02:11 categories: Web Development main: "https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/wp-content/uploads/2022/03/chart-full.png" meta: "Let’s look at some more technical and non-obvious aspects of optimising Largest Contentful Paint" faq: - question: "What is Largest Contentful Paint?" answer: "Largest Contentful Paint is a Core Web Vitals metric that measures when the largest visible content element in the viewport finishes rendering." - question: "What usually improves LCP most?" answer: "The biggest wins usually come from improving server response times, choosing a good LCP candidate, and making sure the browser can discover and prioritise it quickly." - question: "Should I lazy-load my LCP image?" answer: "No. Lazy-loading your LCP candidate usually delays it and makes the metric worse." - question: "Can animations or fades hurt LCP?" answer: "Yes. Fade-ins and similar effects can delay when the browser considers the content fully rendered, which can worsen LCP." - question: "Are image-based LCPs always best?" answer: "Not necessarily. In many cases a text-based candidate is easier to prioritise and can produce a faster, more reliable LCP than a heavy image." --- [Largest Contentful Paint](https://web.dev/lcp/) (LCP) is my favourite Core Web Vital. It’s the easiest to optimise, and it’s the only one of the three that works the exact same in the lab as it does in the field (don’t even get me started on this…). Yet, surprisingly, it’s the least optimised CWV in CrUX—at the time of writing, [**only half of origins in the dataset had a Good LCP**](https://twitter.com/ChromeUXReport/status/1501325517634490376)! {% include promo.html %} This genuinely surprises me, because LCP is the simplest metric to improve. So, in this post, I want to go deep and show you some interesting tricks and optimisations, as well as some pitfalls and bugs, starting with some very simple tips. Let’s go. ## Solve Everything Beforehand _**Struggling to hit 2.5s?** [Work with me one-to-one and deliver a faster LCP](/consultancy/)._ Let’s start with the easy stuff. LCP is a milestone timing—it measures… > …the render time of the largest image or text block visible within the > viewport, relative to when the page first started loading. The important thing to note here is that Google doesn’t care how you get to LCP, as long as you get there fast. There are a lot of other things that could happen between the start of the page load lifecycle and its LCP. These include (but are not limited to): * DNS, TCP, TLS negotiation * Redirects * TTFB * First Paint * First Contentful Paint {% include cross-sell.html %} If any of these are slow, you’re already on the back foot, and they’re going to have a knock-on effect on your LCP. The metrics above don’t matter in and of themselves, but it’s going to help your LCP if you can get them as low as possible.
Treo is an incredible tool for getting timings data from CrUX.
An analogy I use with non-technical stakeholders goes a little like this: You need to get the kids to school for 08:30. That’s all the school cares about—that the kids are there on time. You can do plenty to help make this happen: prepare their clothes the night before; prepare their lunches the night before (do the same for yourself). Set appropriate alarms. Have a morning routine that everyone follows. Leave the house with plenty of time to spare. Plan in suitable buffer time for traffic issues, etc. The school doesn’t care if you laid out uniforms the night before. You are being judged on your ability to get the kids to school on time; it’s just common sense to do as much as you can to make that happen. Same with your LCP. Google doesn’t (currently) care about your TTFB, but a good TTFB is going to help get closer to a good LCP. Optimise the entire chain. Make sure you get everything beforehand as fast as possible so that you’re set up for success. ## Optimise Your LCP Candidate A tip that hopefully doesn’t need me to go into any real detail: if you have an image-based LCP, make sure it is well optimised—suitable format, appropriately sized, sensibly compressed, etc. Don’t have a 3MB TIFF as your LCP candidate. ## Avoid Image-Based LCPs This isn’t going to work for a lot, if not most, sites. But the best way to get a fast LCP is to ensure that your LCP is text-based. This, in effect, makes your FCP and LCP synonymous[^1][^2]. That’s it. As simple as that. If possible, avoid image-based LCP candidates and opt instead for textual LCPs. The chances are, however, that won’t work for you. Let’s look at our other options. ## Use the Best Candidate Okay. Now we’re getting into the fun stuff. Let’s look at which LCP candidates we have, and whether there are any relative merits to each. There are several potential candidates for your LCP. Taken straight from web.dev’s [Largest Contentful Paint (LCP)](https://web.dev/lcp/) page, these are: * `` elements * `` elements inside an `` element * `` JavaScript file in order to: 1. exaggerate the waterfalls, and; 2. stall the parser to see if or how each LCP type is impacted by the preload scanner. It’s also worth noting that each demo is very stripped back, and doesn’t _necessarily_ represent realistic conditions in which many responses would be in-flight at the same time. Once we run into resource contention, LCP candidates’ discovery may work differently to what is exhibited in these reduced test cases. In cases like these, we might look to [Priority Hints](https://web.dev/priority-hints/) or [Preload](https://web.dev/preload-critical-assets/) to lend a hand. All I’m interested in right now is inherent differences in how browsers treat certain resources. The initial demos can be found at: * [🔗 ``](https://csswizardry.dev/lcp/img.html) ```html ``` * [🔗 `` in ``](https://csswizardry.dev/lcp/svg.html) ```html ``` * [🔗 `poster`](https://csswizardry.dev/lcp/video.html) ```html ``` * [🔗 `background-image: url();`](https://csswizardry.dev/lcp/background-image.html) ```html
...
``` {% include cross-sell.html %} [The WebPageTest comparison](https://webpagetest.org/video/compare.php?tests=%2C220320_BiDc0C_6AJ%2C220320_BiDcJT_6AP%2C220320_BiDcN0_6AM%2C220320_BiDc56_6AK&thumbSize=200&ival=100&end=full) is available for you to look through, though we’ll pick apart individual waterfalls later in the article. That all comes out looking like this:
Note a bug in reported LCP with <image> in <svg>: more on this later. (View full size.)
**`` and `poster` are identical in LCP**; **`` in `` is the next fastest**, although there is a bug in the LCP time that Chrome reports; **`background-image`-based LCPs are notably the slowest**.
A bug in Chrome ≤101 mistakenly reports a text node as the LCP element. This is fixed in version 102.
As we can see, **not all candidates are born equal.** Let’s look at each in more detail. ### `` Elements
LCP candidate discovered immediately.
Of the image-based LCPs, this is probably our favourite. `` elements, as long as we don’t mess things up, are quick to be discovered by [the preload scanner](https://andydavies.me/blog/2013/10/22/how-the-browser-pre-loader-makes-pages-load-faster/), and as such, can be requested in parallel to preceding—even blocking—resources. #### `` and `` It’s worth noting that the `` element behaves the same way as the `` element. This is why you need to write so much verbose syntax for your `srcset` and `sizes` attributes: the idea is that you give the browser enough information about the image that it can [request the relevant file via the preload scanner](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/parser/html_preload_scanner.cc;l=565?q=HTMLPreloadScanner&ss=chromium) and not have to wait until layout. (Although, I guess—technically—there must be like a few milliseconds compute overhead working out which combination of ``, `srcset`, `sizes` to use, but that will be mooted pretty quickly by virtually any other moving part along the way.) ### `` in `` `` elements defined in ``s display two very interesting behaviours. The first of which is a simple bug in which Chrome misreports the LCP candidate, seemingly overlooking the `` entirely. Depending on your context, this could mean much more favourable and optimistic LCP scores.
At the time of writing, there is a bug in Chrome ≤101 in which the reported LCP comes back as not-the <image> element. In our demo, it is actually flagged as being the much smaller <p> element.
Once the fix rolls out in M102 (which is Canary at the time of writing, and will reach Stable on [24 May, 2022](https://chromiumdash.appspot.com/schedule)), we can expect accurate measurements. This does mean that you may experience degraded LCP scores for your site.
This bug is fixed in Chrome 102.
{% include cross-sell.html %} Because of the current reporting bug, `` in `` is likely to go from being (inadvertently) one of the fastest LCP types, to one of the slowest. In the unlikely event that you are using `` in ``, it’s probably something that you want to check on sooner rather than later—your scores are likely to change. The bug pertains only to reported LCP candidate, and does not impact how the browser actually deals with the resources. To that end, waterfalls in all Chrome versions look identical, and networking/scheduling behaviour remains unchanged. Which brings me onto the second interesting thing I spotted with `` in ``:
LCP candidate is hidden from the preload scanner.
`` elements defined in ``s appear to be hidden from the preload scanner: that is to say, the `href` attribute is not parsed until the browser’s primary parser encounters it. I can only guess that this is simply because the preload scanner is built to scan HTML and not SVG, and that this is by design rather than an oversight. Perhaps an optimisation that Chrome could make is to preload scan embedded SVG in HTML…? But I’m sure that’s much more easily said than done… ### `` element, and is discovered early by the preload scanner.
LCP candidate discovered immediately.
This means that `poster` LCPs are inherently pretty fast, so that’s nice news. The other news is that it looks like there’s [intent to take the first frame of a video](https://bugs.chromium.org/p/chromium/issues/detail?id=1289664) as the LCP candidate if no `poster` is present. That’s going to be a difficult LCP to get under 2.5s, so either don’t have a `