Skip to content
RU
← All articles

content-visibility: Skipping Work the User Cannot See

In short. A browser lays out and paints your whole page, including the parts nobody has scrolled to. content-visibility: auto tells it to skip that work until an element approaches the viewport. On a long page it can remove most of the initial rendering cost — and if you omit the companion property that reserves space, it will make the scrollbar jump instead.

What the browser stops doing

Rendering is not one step. The browser computes styles, calculates layout, paints, and composites — and by default it does all of that for every element in the document, whether or not it is on screen. On a page with fifty sections, forty-five of which nobody will ever reach, that is forty-five sections of work spent before the first one is usable.

content-visibility changes the default. Marked elements are skipped entirely until the browser decides they are relevant — close to the viewport, focused, or found by search — at which point they render normally.

This is a rendering optimisation, not a loading one. The HTML still arrives, the images still download unless they are lazily loaded, and the JavaScript still runs. What is saved is the layout and paint work, which on content-heavy pages is often the larger half of the time before a page becomes interactive.

A long document where only the section near the viewport is fully drawn and the rest are outlined placeholders
Work is deferred, not removed. Sections render as they approach; the ones nobody reaches are never rendered at all.

The values, and what each one costs you

ValueRenderingFound by Ctrl+FIn the accessibility tree
visibleAlways, the defaultYesYes
autoSkipped while far offscreenYesYes
hiddenAlways skipped until you change itNoNo

The middle row is the one to use. auto keeps the content reachable — in-page search finds it and renders it, screen readers see it, anchor links jump to it — while still skipping the work when it is far away.

hidden is a different tool with a different purpose: it behaves like hiding the element, except rendering state is preserved so showing it again is cheap. It suits things you control the visibility of, such as inactive tab panels. It is not a performance switch to sprinkle on content, because content nobody can find is not content.

The property you must not omit

A skipped element has no laid-out contents, so it reports a height of zero unless you tell the browser what to assume. Zero-height placeholders make the page shorter than it will be, which means the scrollbar is wrong and grows as you scroll — the exact behaviour users describe as the page "fighting" them.

/* Wrong: skipped sections collapse, the scrollbar lurches */
.section { content-visibility: auto; }

/* Right: reserve a plausible size for the skipped state */
.section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

The auto keyword inside contain-intrinsic-size is worth understanding: it means "use the real size once you have measured it, and this number until then". A section rendered once keeps its measured height when it goes back offscreen, so the scrollbar stops changing after the first pass through the page.

Estimate the placeholder from real content rather than picking a round number. Too small and the page grows as the user scrolls; too large and it shrinks. Either way the scroll position drifts, and a drifting scroll position is a worse experience than the rendering cost you removed.

Where it genuinely helps

  • Long articles with many sections — the canonical case, and the one where the gain is largest relative to the effort.
  • Feeds and long lists where items are structurally similar, so one intrinsic size fits all of them.
  • Comment threads below the fold, which are often a large share of the DOM and almost never in the first viewport.
  • Documentation pages with deep tables of contents and long bodies.

Where it does not, and where it hurts

  • Short pages. If everything fits in a viewport or two, there is nothing offscreen to skip and you have added a property for nothing.
  • Above-the-fold content. Marking the hero costs a check and saves nothing, because it renders immediately anyway.
  • Elements whose height varies wildly and cannot share an intrinsic size — the scrollbar behaviour will be poor for some of them no matter what number you pick.
  • Anything relying on measuring offscreen elements from JavaScript. A skipped element has no layout, so reading its dimensions returns the placeholder rather than the content — and forcing it to render to answer the question defeats the purpose entirely.

That last point catches carousels, sticky headers computed from content height and any code that measures the document to position something. Reading geometry from a skipped element is exactly the kind of forced synchronous work covered in the forced reflow guide.

A scrollbar of the correct length beside one that changes as skipped sections are measured
Without a reserved size the page's height is a guess that keeps changing. The scrollbar is where the user notices.

Measuring whether it did anything

The gain is in rendering time, so measure rendering rather than the total load. Two comparisons are enough:

  1. Before and after, same page, same conditions. Record a performance profile and compare the time spent in layout and paint during load.
  2. With CPU throttling on. The saving scales with how slow the device is, so a fast desktop understates the benefit that mobile users get.
// Rough in-page measure: how long until layout settles
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.entryType === 'layout-shift' && !e.hadRecentInput) {
      console.log('shift', e.value, e.startTime);
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

// And confirm sections are actually being skipped
document.querySelectorAll('.section').forEach((el) => {
  console.log(el.offsetHeight, getComputedStyle(el).contentVisibility);
});

Watch layout shift specifically. The failure mode of this optimisation is not that it fails to speed things up — it is that a missing or wrong intrinsic size trades rendering time for visible instability, and layout shift is where that trade shows up.

A safe way to adopt it

  1. Profile first. If layout and paint are not a meaningful share of your load, stop here.
  2. Apply to one repeating section type, not to everything.
  3. Set contain-intrinsic-size in the same rule, always, from a measured average.
  4. Check in-page search still finds text inside the skipped sections.
  5. Check anchor links still jump correctly.
  6. Measure layout shift before and after, on a throttled CPU.

Steps four and five take a minute and they are the ones that catch a mistaken hidden, which looks identical in a profile and is very different for a reader.

A search action reaching into a deferred section and causing it to render, contrasted with a sealed section the search cannot reach
Deferred content is still findable and renders on demand. Hidden content is not — which is the difference between the two values.

How it differs from lazy loading and virtualisation

Three techniques defer work on long pages and they are routinely confused, which leads to reaching for the wrong one.

TechniqueDefersDOM sizeEffort
Lazy loading imagesDownloadsUnchangedOne attribute
content-visibilityLayout and paintUnchangedTwo CSS declarations
VirtualisationEverything, including the DOMReduced dramaticallyA component rewrite

They stack rather than compete. Lazy loading handles bytes, this property handles rendering, and virtualisation handles the DOM itself — and only the last one helps when the document is so large that merely holding it is the problem.

The practical order is the order of that table: apply the cheap ones first, measure, and reach for virtualisation only if a page with tens of thousands of nodes is still slow after both. Rewriting a list into a virtualised one is a large change with its own failure modes — scroll restoration, accessibility, in-page search — and it is worth avoiding while two CSS lines will do.

How to check your page

Use the speed check for the rendering metrics before and after — the numbers that move are the ones related to layout and paint, not the transfer size, which this technique does not change. The header checker is worth a look too, since a page heavy enough to need this is usually also worth checking for compression and cache headers.

Because the risk here is visible instability rather than slowness, and instability is intermittent by nature, a single check after deploying is weak evidence. Scheduled monitoring records the measurement over time, which is how a regression introduced by a later content change gets noticed rather than reported.

A stable measurement series interrupted by a step change following a content update
The property is set once; the content it applies to keeps changing. A recorded series is what catches the day an estimate stopped fitting.

Frequently asked questions

Does it reduce the size of my page?

No. Everything still downloads. What is skipped is layout and paint work, so the gain is in rendering time and it is largest on slower devices.

Will search engines still see the content?

Content under auto remains in the DOM and in the accessibility tree, and is rendered when needed. hidden is a different matter and should not be used for content you want found.

Why does my scrollbar jump?

Because skipped elements report no height without contain-intrinsic-size. Add it in the same rule, using a measured estimate, and prefer the auto keyword so real sizes are remembered after the first render.

Can I apply it to everything?

You can, and it will cost more than it saves. Elements already on screen gain nothing, and each one adds a relevance check. Apply it to repeating content that is genuinely offscreen.

What happens in browsers that do not support it?

The declaration is ignored and the page renders as it always did. It is a progressive enhancement with no fallback needed.

How does it interact with lazy-loaded images?

They complement each other — one defers rendering work, the other defers downloads. Using both on a long page is the normal combination, and neither substitutes for the other.

Checklist

  • Profile layout and paint before deciding this is your problem.
  • Use auto, not hidden, for content readers should find.
  • Always pair it with contain-intrinsic-size in the same rule.
  • Estimate the intrinsic size from measured content, not a round number.
  • Prefer the auto keyword so measured sizes are remembered.
  • Do not apply it above the fold.
  • Verify in-page search still finds the deferred text.
  • Verify anchor links still jump correctly.
  • Avoid measuring skipped elements from JavaScript.
  • Measure layout shift on a throttled CPU, before and after.

Check your website right now

Check your site's speed →
More articles: Performance
Performance
Gzip vs Brotli: Web Compression Compared
16.03.2026 · 700 views
Performance
CDN Cache Invalidation: Strategies for Fresh Content
16.03.2026 · 680 views
Performance
Resource Hints: Prefetch, Preload, Preconnect, DNS-Prefetch
16.03.2026 · 536 views
Performance
Latency vs Throughput: Network Performance Metrics
16.03.2026 · 529 views