Stop Scrolling the Main Thread: Using Intersection Observer in Chrome for Lazy Loading
Scroll listeners that call getBoundingClientRect() thrash layout. Chrome's Intersection Observer API fixes lazy loading and infinite scroll with asynchronous, reflow-free visibility callbacks.
10 Aug 2026, 20:01 UTC

If you've ever attached a scroll listener that calls getBoundingClientRect() on every frame to decide whether an image is visible, you've built a layout-thrash machine. Each call forces Chrome to synchronously compute layout, and on a busy page that means dropped frames and a hot phone.
The Intersection Observer API is Chrome's supported answer: you register a target element, and the browser notifies you asynchronously when it crosses a visibility threshold relative to the viewport (or a container you choose). No scroll handlers, no forced reflows, and the work happens off your critical path.
Why the scroll-listener approach hurts
Scroll events fire on the main thread, often dozens of times per second. If your handler reads layout properties like offsetTop or getBoundingClientRect() after anything has written to the DOM, Chrome must recalculate styles and layout before it can answer. Do that for fifty images and you've turned scrolling into a layout benchmark.
Intersection Observer moves the geometry tracking into the browser's compositing pipeline. The callback fires with precomputed intersection data, so you never need to measure anything yourself.
A minimal lazy-loading observer
Run this in any page's script (no special permissions needed — it's plain JavaScript in the renderer). The placeholder data-src attribute keeps the real image URL out of the network queue until needed:
const observer = new IntersectionObserver((entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const img = entry.target;
img.src = img.dataset.src; // swap in the real URL
obs.unobserve(img); // stop watching once loaded
}
}, {
root: null, // null = viewport
rootMargin: '200px', // start loading 200px before it appears
threshold: 0.01 // fire when ~1% is visible
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));Two details matter here. First, rootMargin: '200px' expands the effective viewport so images start fetching before the user reaches them — this hides latency and improves perceived performance. Second, unobserve() after loading prevents the observer from holding references to elements you no longer care about.
Verifying it actually works
Open Chrome DevTools and check three things:
- Network panel: reload the page and confirm off-screen images don't request until you scroll near them.
- Performance panel: record a scroll and compare against your old scroll-handler version. You should see far fewer purple "Layout" blocks in the main thread flame chart.
- Console sanity check: add
console.log(entry.intersectionRatio)inside the callback to confirm thresholds fire as expected.
Note that native lazy loading (<img loading="lazy">) is also supported in Chrome and is simpler for plain images. Intersection Observer earns its complexity when you need custom logic — infinite scroll, pausing off-screen video, analytics on viewed elements, or deferring heavy components.
Trade-offs and gotchas
The API isn't free. Observing thousands of tiny elements still costs memory and bookkeeping, so for infinite scroll a common pattern is to observe a single sentinel element at the bottom of the list rather than every item. Also, threshold must be between 0.0 and 1.0 (or an array of such values); anything else silently behaves in ways you didn't intend. Finally, intersection is computed against clipping ancestors — an element inside an overflow: hidden container may report differently than you expect, so test with your real layout.
The actionable takeaway: pick one scroll handler on your site that measures geometry, replace it with an observer using rootMargin to prefetch early, and measure the difference in the Performance panel. It's one of the cheapest main-thread wins Chrome gives you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.