Skip to main content

How CSS Opacity Animations Can Delay The Largest Contentful Paint

· 4 min read

Fade-in animations make a website look more polished, but can also cause a slower Largest Contentful Paint. That's because of how elements with an opacity of 0 are counted when measuring LCP.

This article explains why fade-in animations can delay LCP and what you can do about it.

Filmstrip showing LCP being delayed by fade-in animation

Elements with an opacity of 0 are not LCP candidates

The Core Web Vitals measure user experience, so counting a paint with opacity 0 as the LCP element doesn't make sense.

Accordingly, in August 2020 Chrome made a change to ignore these elements.

[LCP] Ignore paints with opacity 0
This changes the opacity 0 paints that are ignored by the LCP algorithm. [...] After this change, even will-change opacity paints are ignored, which could result in elements not becoming candidates because they are never repainted. In the special case where documentElement changes its opacity, we consider the largest content that becomes visible as a valid LCP candidate.

Even after an element is faded in it still doesn't become an LCP candidate unless it is repainted. However, if an element is repainted then the LCP will be higher than expected!

Note the special case for documentElement. Many A/B testing tools initially hide all page content, so without this exception no element would ever be counted for LCP.

What causes repaints?

If there's no repaint then another page element will simply be used to measure the LCP. However, your the LCP can increase if a large element repaints and becomes the new LCP element.

What might cause an element to repaint? A few examples:

  • Element changes, like when a web font finishes loading
  • Changing the lang attribute on the html tag
  • Resizing the window or changing the device orientation
  • Changes in content size because a scrollbar is added or removed (for example when showing a modal)

This is what was happening on the website where I first noticed this issue:

  • The H1 element was faded in with the AOS animation library
  • AccessiBe set the html lang attribute
  • The H1 was repainted

The H1 was registered as the LCP element in the last step, even though no visual change occurred at that point.

Example: repaint when web font load is loaded

Let's look at a page where the LCP element is the H1 element and that uses a web font.

LCP without an animation

Without an animation the LCP is registered when the heading is first rendered. When the web font is loaded later on it doesn't cause the LCP to update.

Then we'll add a CSS fade-in animation to the H1, starting at opacity 0.

h1 { animation: fade-in 0.2s forwards; }
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}

With the animation, no LCP is registered for the initial render as the element starts with an opacity of 0. The LCP doesn't update when the fade-in animation completes, but it is updated when the web font load causes a repaint.

LCP with the fade-in animation

How to fix this LCP issue

Disabling the animation would be the easiest fix.

You could also start the fade-in animation with a non-zero opacity like 0.1, to ensure the initial render counts as an LCP candidate.

This doesn't seem to apply to images

For both text and images, the LCP is reported when the content is repainted. However, it appears that for images the startTime reported by Chrome shows when the element was originally rendered, rather than the most recent update.

Image LCP reported after window resize

Compare this to a text node that's faded in, followed by resizing the browser to trigger a repaint.

Text LCP reported after window resize

Monitor your website performance

Working on improving Largest Contentful Paint? DebugBear can track your Core Web Vitals over time and provide tailored recommendations to improve them.

DebugBear performance dashboard

Get a monthly email with page speed tips