All topics
DOMintermediate

Intersection Observer for Lazy Loading

An efficient, asynchronous browser API for detecting when an element enters or exits the viewport, commonly used for lazy loading and infinite scroll.

IntersectionObserver tells you when an element enters or leaves the viewport, asynchronously and without scroll handlers.

IntersectionObserver is like a lifeguard using peripheral vision to notice the instant someone crosses into the deep end (a threshold), rather than manually re-measuring everyone's distance from the deep-end line every single second (polling on scroll) — the lifeguard is only actively alerted exactly when it actually matters.

Key Concepts

1
    const obs = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          e.target.src = e.target.dataset.src;
          obs.unobserve(e.target);        // once is enough
        }
      });
    }, { rootMargin: '200px' });
2
    document.querySelectorAll('img[data-src]').forEach(el => obs.observe(el));
3
What it replaced. A scroll listener calling getBoundingClientRect on every element, on every scroll event — which forces layout, runs hundreds of times a second, and is the standard cause of janky scrolling.
getBoundingClientRect
4
The observer runs off the main thread's critical path and reports only when the state changes.
5
rootMargin is the option that matters. '200px' starts loading 200 pixels before the element is visible, so the image is there by the time the user reaches it.
rootMargin'200px'
6
Without it, loading begins when the element is already on screen and the user watches it appear.
7
threshold decides how much must be visible.
    threshold: 0       any part (the default)
    threshold: 0.5     half
    threshold: [0, 0.5, 1]   fire at each
threshold
8
For an analytics "was this seen" event, 0.5 with a dwell time is closer to what was meant than 0.
0.50
9
Infinite scroll is a sentinel.
    const sentinel = document.querySelector('#load-more');
    new IntersectionObserver(([e]) => {
      if (e.isIntersecting) loadNextPage();
    }).observe(sentinel);
10
An empty element after the last item. When it appears, fetch more.
11
Native lazy loading covers the simple case.
    <img loading="lazy" src="...">
12
For images and iframes that is enough, and no JavaScript is needed. The observer is for everything else: analytics, animations on entry, video autoplay, and infinite scroll.
13
Two cautions. Unobserve elements you are done with, or the observer holds references to nodes that have been removed. And the callback receives all changed entries in a batch, so it must handle several at once — including elements that just left the viewport, where isIntersecting is false.
isIntersecting
14
What the interviewer is probing.1. "Why is this better than a scroll handler?" Probing: the cost. Stalls: "It is newer." Moves up: it is handled off the main thread and fires only on threshold crossings, where a scroll handler runs constantly and forces layout reads.
15
2. "What does rootMargin do?" Probing: the option. Stalls: "Sets the area." Moves up: it grows or shrinks the trigger area, so you can start loading before the element is actually visible.
16
3. "How do you stop observing once loaded?" Probing: cleanup. Stalls: "It stops itself." Moves up: call unobserve on that element in the callback, or the entry keeps firing on every crossing.
17
4. "What else is it good for besides images?" Probing: breadth. Stalls: "Nothing." *Moves up:* infinite scroll, analytics impressions, and pausing animations or video that scroll out of view.