Engineering

Lazy Rendering With CSS content-visibility

Skip layout and paint for off-screen DOM with content-visibility and contain-intrinsic-size — a browser-native performance win.

View reference repoOpen live demo

Long pages with dozens of self-contained sections — dashboards, documentation, feed-style layouts — pay a rendering tax for every block in the DOM, even when most of it is off-screen. JavaScript-based lazy loading works, but it adds observers, state, and coordination overhead. Modern CSS offers a declarative alternative: tell the browser to defer layout and paint for elements outside the viewport, without removing them from the document.

Our Angular reference demo at learn-lazy-rendering-using-css.vercel.app renders fifty identical content blocks and toggles optimization on and off so you can compare behavior side by side. The full source is available at learn_lazy-rendering-using-css.

What content-visibility Does

The content-visibility property controls whether the browser renders an element's contents. Setting it to auto enables rendering containment: the engine skips layout, style, and paint work for off-screen subtrees until they approach the viewport. Unlike display: none, the element remains in the document flow and participates in accessibility trees once rendered. Unlike mounting components conditionally in JavaScript, no application code runs to decide visibility — the browser handles it natively during scroll and resize.

This is particularly effective on pages where each section is structurally independent: card grids, comment threads, FAQ accordions rendered inline, or repeated list items that share a template but carry distinct content. The DOM stays intact, which simplifies SEO, print styles, and server-side rendering compared to virtualization libraries that recycle a small pool of nodes.

Core insight

content-visibility: auto is not lazy loading in the network sense. It does not defer fetching images or delay script execution. It skips rendering work for off-screen subtrees — reducing main-thread layout and paint cost on initial load and during scroll.

The CSS Pattern

In the demo, each block is an Angular component with a modifier class applied when optimization is enabled. The stylesheet applies two properties together:

.content-block.optimized {
  content-visibility: auto;
  contain-intrinsic-size: 400px;
}

content-visibility: auto tells the browser to treat each block as a rendering containment boundary. Off-screen blocks skip their internal layout and paint passes. When the user scrolls near a block, the browser promotes it to fully rendered state automatically.

contain-intrinsic-size

When content is not yet rendered, the browser has no measured height for the subtree. Without a placeholder size, the scrollable area collapses and shifts as blocks come into view — a jarring experience on long pages. contain-intrinsic-size supplies an estimated block size (400px in the demo) so the scrollbar thumb and document length remain stable before real layout runs.

Choose the estimate from typical block dimensions in your design system. Underestimating causes scroll position jumps when content expands; overestimating leaves empty gaps in the scrollbar track. For heterogeneous sections, consider per-component values or a conservative average rather than a single global constant.

Demo behavior

The demo page renders fifty app-content-block components, each with a title, placeholder image, and three-column grid. A control panel toggles the .optimized class on every block simultaneously. With optimization disabled, the browser lays out and paints all fifty blocks on load — observable as slower initial render and higher main-thread activity in DevTools Performance. With optimization enabled, only blocks near the viewport incur full rendering cost; scrolling reveals the rest on demand with no application-level observer code.

Tradeoffs and Limitations

CSS-driven lazy rendering is low ceremony, but it is not free of edge cases. Teams should evaluate these constraints before applying the pattern broadly.

Scrollbars and layout stability

Incorrect contain-intrinsic-size values produce inaccurate scroll ranges. Users may see the scrollbar thumb jump or experience unexpected scroll anchoring when a block's real height differs substantially from the estimate. Measure representative blocks in production and revisit the estimate when templates change.

Accessibility

Content inside skipped subtrees is generally not available to assistive technologies until the browser renders it — behavior equivalent to off-screen content that has not been painted. For critical information above the fold or content that must be discoverable via in-page search regardless of scroll position, do not rely on deferred rendering alone. Test with screen readers and keyboard navigation on your target browsers; behavior varies slightly across engines as implementations mature.

Browser support

content-visibility is supported in Chromium-based browsers and Firefox. Safari added support in recent releases; verify against your analytics baseline before treating this as a universal optimization. Where unsupported, the properties are ignored and the page renders normally — a safe progressive enhancement with no runtime feature detection required, though teams targeting older Safari may need a fallback strategy for the heaviest pages.

CSS vs Intersection Observer vs Virtualization

Three approaches address overlapping problems with different cost profiles. Choosing the right tool depends on page structure, content weight, and how much control you need over lifecycle.

When CSS is the better fit

Prefer content-visibility when sections are already in the DOM, structurally similar, and expensive primarily because of layout and paint — not because of heavy JavaScript initialization or network fetches. Documentation pages, settings panels with many independent sections, and long static feeds with moderate per-item complexity are strong candidates. The implementation cost is one CSS rule per section type.

Intersection Observer suits cases where you need application logic at visibility boundaries: loading data, starting animations, firing analytics, or swapping placeholder content. It does not skip rendering for elements already mounted; it notifies JavaScript when they enter or leave the viewport. Use it when the optimization target is I/O or component lifecycle, not paint cost alone.

Virtualization (windowing libraries such as react-window or Angular CDK virtual scroll) recycles a fixed number of DOM nodes and is appropriate when the list is unbounded or each row is expensive to keep mounted — large datasets, complex row components, or memory-constrained mobile targets. Virtualization adds scroll-position math, item height estimation, and accessibility complexity. It is the right choice when thousands of rows would otherwise exist in the DOM; it is unnecessary overhead when a few dozen self-contained blocks can stay mounted with CSS deferring their paint.

Prefer CSS content-visibility

  • Tens to low hundreds of similar, independent sections
  • Rendering cost dominates; network and JS init are modest
  • Full DOM presence is required for SEO, print, or find-in-page
  • Team wants zero observer wiring and minimal maintenance

Prefer Observer or virtualization

  • Thousands of rows or unbounded collections
  • Rows must not exist in DOM until explicitly loaded
  • Per-item JS initialization is the bottleneck, not paint
  • Precise scroll restoration and variable row heights need library support

Practical Checklist

  1. Identify repeated, self-contained sections where layout and paint show up in Performance profiles.
  2. Apply content-visibility: auto on a containment boundary — typically the outer wrapper of each section, not every nested child.
  3. Set contain-intrinsic-size from measured heights; adjust when templates change.
  4. Verify scroll behavior, find-in-page, and assistive technology on target browsers.
  5. Compare before and after using the live demo toggle and the reference repository as a baseline for what optimized long-page rendering should feel like.

Not every performance problem needs JavaScript coordination. For long pages built from independent blocks, two CSS properties can shift rendering work from load time to scroll time — with no observers, no virtual scroll math, and no change to how your components are structured. Measure first, apply containment at section boundaries, and reserve heavier tooling for the workloads that genuinely require it.