Infrequently Noted

Alex Russell on browsers, standards, and the process of progress.

`content-visiblity` Without Jittery Scrollbars

Update: After further investigation, an even better solution has presented itself, which is documented in the next post.

The new content-visibility CSS property finally allows browsers to intelligently decide to defer layout and rendering work for content that isn't on-screen. For pages with large DOMs, this can be transformative.

In applications that might otherwise be tempted to adopt large piles of JS to manage viewports, it can be the difference between SPA monoliths and small progressive enhancements to HTML.

One challenge with naive application of content-visibility, though, is the way that it removes elements from the rendered tree once they leave the viewport -- particularly as you scroll downward. If the scroll position depends on elements above the currently viewable content "accordion scrollbars" can dance gleefully as content-visibility: auto does its thing.

To combat this effect, I've added some CSS to this site that optimistically renders the first article on a page but defers others. Given how long my articles are, it's a safe bet this won't usually trigger multiple renders for "above the fold" content on initial pageload (sorry not sorry).

This is coupled with an IntersectionObserver that to unfurls subsequesnt articles as you scroll down.

The snippets:

<style>
/* Defer rendering for the 2nd+ article */
body > main > *+* {
content-visibility: auto;
}
</style>
<script type="module">
let observer = new IntersectionObserver(
(entries, o) => {
entries.forEach((entry) => {
let el = entry.target;
// Not currently in intersection area.
if (entry.intersectionRatio == 0) {
return;
}
// Trigger rendering for elements within
// scroll area that haven't already been
// marked.
if (!el.markedVisible) {
el.attributeStyleMap.set(
"content-visibility",
"visible"
);
el.markedVisible = true;
}
});
},
// Set a rendering "skirt" 50px above
// and 100px below the main scroll area.
{ rootMargin: "50px 0px 100px 0px" }
);

let els =
document.querySelectorAll("body > main > *+*");
els.forEach((el) => { observer.observe(el); });
</script>

With this fix in place content will continue to appear at the bottom (ala "infinite scrolling") as you scroll down, but never vanish from the top or preturb your scroll position in unnatural ways.