Why Your CLS Sucks

Update: The Chrome 89 release in March 2021 resolves this issue, ignoring layout shifts under opacity: 0 (more details).

Currently, Chrome’s implementation of the Layout Instability API treats elements with opacity: 0 as visible, causing unexpected layout shift scores.

If you use opacity: 0 to hide page content until it’s ready, Lighthouse penalizes you for above-the-fold layout shifts.

Here’s the open issue & Chromium bug where others are discussing the problem:

What’s Happening?

The Layout Instability API (what Lighthouse uses for scoring CLS) considers any element “visible” as long as its layout position is in the viewport bounds and it’s not display: none or visibility: hidden—it ignores opacity altogether.

This makes sense, as determining true element visibility can get complicated fast. Better to keep the logic simple, right?

But the downside of simple rules: it doesn’t cover all cases. Enter opacity: 0.

Compare PageSpeed Scores

Some sites, for whatever reason, still need to hide their page content as it loads to avoid truly visible layout shifts.

The quickest & easiest workaround is to apply body { opacity: 0 } until the page is ready to display, then reset that style to display the page.

Visual load is the same whether you use opacity or visibility

In the examples above, the visual load is exactly the same.

However, their Cumulative Layout Shift is dramatically different:

  • Opacity Only: 0.159
  • Opacity & Visibility: 0
CLS scores are dramatically different between opacity and visibility

The difference? One line of code: visibility: hidden.

The Quick Fix

Until the Layout Instability API spec and Chrome implementation are updated to treat elements with opacity: 0 as invisible, anyone using opacity: 0 to hide content during page load should also use visibility: hidden.

Update: Better, remove opacity: 0 from the body and target specific areas instead, then add visibility: hidden to ensure they don’t cause CLS (see below).

Hopefully this is a quick win for anyone wondering why their CLS score sucks.

P.S. Holler at me if I missed anything obvious here, or stated anything incorrectly. I’ll continue to update this post as I learn more.

Update 2020-09-26: First Contentful Paint & Overall Score

It turns out Chrome also ignores opacity: 0 for First Contentful Paint (FCP).

This means that, for sites using opacity: 0 to delay the page rendering, Lighthouse currently reports a much faster FCP than real users actually see (i.e., we’re getting better grades than we deserve 😬).

For some, this may be a shock: adding visibility: hidden will lower your CLS but can also increase your FCP, causing a lower overall score. Lighthouse weights its scores (5% for CLS and 15% for FCP) so the worse FCP outweighs the better CLS, causing your overall score to drop.

Now what? Well, you’ve got a couple options:

  1. Keep opacity: 0 alone and accept your fate.
  2. Add visibility: hidden to the body and risk a higher FCP.
  3. Remove opacity: 0 from the body and target specific areas instead, then add visibility: hidden to ensure they don’t cause CLS.

As for me, I’m taking this as an opportunity to improve. Let’s fix both CLS & FCP by hiding specific elements instead of the whole page.

For more on optimizing CLS, check out web.dev: Optimize Cumulative Layout Shift.

Update 2020-12-22: Chrome 89

In a related bug ticket about First Contentful Paint ignoring opacity: 0, Paul Irish shared that Chrome is changing CLS to ignore opacity: 0. This change fixes the issue in the demos I linked to above.

The fix is in Chrome 89 and scheduled for release March 2021.

So mark your calendars everyone: come Spring, our CLS scores will start magically improving.

For reference, here’s a link to the commit in Chromium: https://chromium.googlesource.com/chromium/src/+/e1e1bb4a2731c8e3c47ce4660c91cce0e0ddd4dd.