Visibility-aware rendering
Use `content-visibility` with `contain-intrinsic-size` to skip layout and paint for off-screen content, and Intersection Observer to drive lazy behaviour, instead of scroll and resize listeners.
What it is
Two platform primitives for “do work only when content is actually visible”.
content-visibility: auto is a CSS property from the Containment module. It tells the browser it may skip rendering work — layout, style, paint — for a subtree while that subtree is off-screen. When the element scrolls near the viewport, the browser renders it as normal.
Intersection Observer is a Web API that fires a callback when an element crosses a configurable threshold relative to the viewport (or another scroll root). It replaces hand-rolled scroll and resize listeners that ran on every frame.
Why it matters
- Long pages and card grids get cheaper. A page with hundreds of sections, product cards, or comments pays the layout and paint cost for all of them on every change.
content-visibility: autoreduces that to just what is on screen, often a multiple-times reduction in rendering time on initial load and interaction. - Scroll handlers stop being the bottleneck.
scrolllisteners fire dozens of times per second on the main thread. Intersection Observer hands the work to the browser’s compositor and only wakes your code at the threshold you asked for. - Built-in primitives beat libraries. Lazy-init for embeds, “load more” triggers, viewport-driven analytics, and reveal animations all collapse to the same small API.
How to implement
content-visibility on long, repeating sections. Always pair it with contain-intrinsic-size so the browser can reserve layout space for the skipped subtree:
.card,
article > section {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
The intrinsic size is a placeholder used until the real content is rendered. Pick a value close to the real rendered size of the block.
Intersection Observer for everything loading="lazy" doesn’t cover. Native loading="lazy" on <img> and <iframe> already handles the simple image case — reach for IO for widgets, infinite-scroll triggers, and visibility analytics:
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.dataset.seen = 'true';
io.unobserve(entry.target);
}
}
}, { rootMargin: '200px 0px' });
document.querySelectorAll('[data-lazy]').forEach((el) => io.observe(el));
rootMargin lets you start work before the element is on screen, so the user doesn’t see a flash of empty space.
Common mistakes
- Omitting
contain-intrinsic-size. The skipped subtree collapses to zero height, the scrollbar jumps as content streams in, and CLS suffers. - Picking an intrinsic size much smaller than reality. Same scrollbar-jank problem, just spread across the whole page.
- Using Intersection Observer where
loading="lazy"would do. For below-the-fold images and iframes, the HTML attribute is simpler and ships in every modern browser. - Debounced scroll listeners. If you’re throttling
scrollto fake an IO callback, just use IO. It is cheaper and more accurate. - Forgetting to
disconnect(). In long-lived single-page apps, observers attached to torn-down views leak. Disconnect when the view unmounts.
Verification
- Chrome DevTools → Performance panel. Record a scroll through a long page with and without
content-visibility: auto. The “Rendering” and “Painting” bands should shrink sharply. - DevTools → Rendering → “Layout Shift Regions” and the Web Vitals overlay. Verify CLS does not regress after adding
content-visibility— if it does, yourcontain-intrinsic-sizeis wrong. - In the console, inspect
IntersectionObserverEntry.intersectionRatioandentry.isIntersectinginside your callback to confirm thresholds fire when expected. - Search the codebase for
addEventListener('scroll'andaddEventListener('resize'. Each hit is a candidate for replacement.
Related topics
Sources & further reading
- CSS Containment Module Level 2 — W3C CSSWG
- MDN — content-visibility — MDN
- Intersection Observer — W3C
- MDN — Intersection Observer API — MDN