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.
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.
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.
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.
Compare this to a text node that's faded in, followed by resizing the browser to trigger a repaint.
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.