Action on Visibility
Defer work until it's actually seen — IntersectionObserver-driven lazy images, components, and data.
Most pages load a pile of stuff the user never scrolls to. Action on visibility flips that: defer the work — images, heavy components, even API requests — until the element is actually about to be seen. The browser gives you a precise tool for it: the Intersection Observer.
Non-critical means “not yet”
On any real page, only the top is visible at first — everything below the fold can wait. Component one needs its data now; components three and four don't exist on screen yet, so loading them immediately just steals bandwidth and main-thread time from the part the user is actually looking at. The fix is to tie the work to visibility.
Lazy-load on scroll
Below is a real scroll container wired to an Intersection Observer. In On visibility mode, each card only fetches its data the moment it scrolls into view — watch the “deferred” counter shrink as you scroll. Switch to Eager and everything loads up front, even what you never reach:
The Intersection Observer
Rather than firing on every scroll event (expensive), the browser watches elements for you and tells you when their visibility crosses a threshold. Three options shape it: root (what you measure against — usually the viewport), rootMargin (grow/shrink that box to fire early or late), and threshold (how much of the element must show). Play with the threshold:
Wiring it to a lazy component
The callback receives entries (the observed elements) and the observer. When something becomes visible you do your action — and call unobserve if it's a one-time load, to drop the overhead. Combined with a dynamic import(), you defer the component and its heavy dependencies until it's seen:
const io = new IntersectionObserver((entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
loadHeavyChart(); // fetch data / render
observer.unobserve(entry.target); // one-time: stop watching
}
}
}, { root: null, rootMargin: "200px", threshold: 0 });
io.observe(document.querySelector("#chart-section"));React.lazy + Suspense; Vue has defineAsyncComponent; native loading="lazy" on <img> handles images for free. All of them lean on the same visibility idea.What you can defer
“Action” is open-ended — anything expensive can be tied to visibility:
- Images below the fold — the most common (
loading="lazy"). - Heavy components — render only when scrolled to (lazy + split).
- Back-end requests — don't fetch a section's data until the section is visible.
- Infinite scroll — load the next page when a sentinel near the bottom appears.
threshold: 0 mean?observer.unobserve() after a one-time load?- →Defer non-critical, below-the-fold work until the element is about to be seen.
- →The Intersection Observer tracks visibility efficiently — no per-scroll computation.
- →Tune it with root, rootMargin (pre-load early), and threshold.
- →Defer images, heavy components (lazy + split), API requests, and power infinite scroll.
- →
unobserveafter one-time loads to drop overhead; frameworks wrap all this for you.