Frontend Performance/Loading Less, Sooner
Lesson 12 of 20 · Episode 12

Action on Visibility

Defer work until it's actually seen — IntersectionObserver-driven lazy images, components, and data.

IntersectionObserverLazy loadingVisibility
Watch on YouTube ↗

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.

The concept

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.

See it live

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:

InteractiveCards that load as they enter view
Loaded so far
1/6
Deferred (not yet fetched)
600 KB
↓ Scroll inside this box — each card fetches only as it enters view.
Card 1 · data loaded (120 KB)
Card 2 · loads when visible
Card 3 · loads when visible
Card 4 · loads when visible
Card 5 · loads when visible
Card 6 · loads when visible
This uses a genuine IntersectionObserver scoped to the scroll box. Off-screen cards stay unfetched until you scroll to them.
The API

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:

Interactivethreshold: how much must be visible?
viewport (root)
visible ✓
Visible: 75% callback fires (load it!)
threshold: 0 fires at the first pixel; 1.0 waits until the whole element is on screen. rootMargin can extend the root to pre-load slightly early.
threshold 0 fires at the first pixel; 1.0 waits for the whole element. rootMargin can pre-load just before the element arrives.
In code

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:

load a heavy component (and its deps) on visibility
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"));
Frameworks make it one line
You rarely write the observer by hand. React has React.lazy + Suspense; Vue has defineAsyncComponent; native loading="lazy" on <img> handles images for free. All of them lean on the same visibility idea.
The menu

What you can defer

“Action” is open-ended — anything expensive can be tied to visibility:

Q1Multiple choice
Why use IntersectionObserver instead of a scroll event listener?
Q2Multiple choice
What does threshold: 0 mean?
Q3Multiple choice
You want a section's API data to load only when the user scrolls to it. Best approach?
Q4Multiple choice
Why call observer.unobserve() after a one-time load?
Key takeaways
  • 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.
  • unobserve after one-time loads to drop overhead; frameworks wrap all this for you.
← Previous
11. Bundle Splitting
Next →
13. Resource Hints — dns-prefetch & preconnect