Why Your CLS Sucks

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.