JavaScriptIntersectionObserverFrontendPerformance

IntersectionObserver Guide: Lazy Loading and Scroll Triggers

Use IntersectionObserver for lazy loading, infinite scroll, reveal animations, and view tracking without constantly calculating scroll position by hand.

·Updated ·9 min read·Counting...
IntersectionObserver Guide: Lazy Loading and Scroll Triggers

Many front-end features need to know whether an element is visible: lazy-loaded images, infinite scrolling, reveal animations, sticky navigation, and view tracking. You can build all of these with scroll events and getBoundingClientRect(), but it is easy to create too many calculations on every scroll.

IntersectionObserver gives the browser a better way to notify you when a target enters or leaves a viewport or scroll container.

What IntersectionObserver does

IntersectionObserver watches the intersection between a target element and a root area.

It is useful because it is:

  • More efficient than manually checking every scroll event.
  • Easier than repeatedly calculating element rectangles.
  • Configurable through root, rootMargin, and threshold.

Common use cases include image lazy loading, infinite scroll, animation triggers, ad view tracking, and "section active" navigation.

Basic usage

Create an observer:

CODE
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    console.log(entry.isIntersecting)
  })
})

Observe an element:

CODE
observer.observe(targetElement)

Stop observing:

CODE
observer.unobserve(targetElement)
observer.disconnect()

Use unobserve for one target. Use disconnect when the whole observer is no longer needed.

Options

CODE
const observer = new IntersectionObserver(callback, {
  root: null,
  rootMargin: "100px",
  threshold: [0, 0.5, 1],
})
OptionMeaningDefault
rootThe viewport or scroll container used for checking visibilitynull
rootMarginExtra margin around the root, useful for triggering early"0px"
thresholdVisibility ratio or ratios that trigger callbacks0

rootMargin: "100px" can start loading before the element actually reaches the viewport. threshold: 0.5 triggers when about half of the target is visible.

Callback entries

Each IntersectionObserverEntry includes useful data:

PropertyMeaning
isIntersectingWhether the target currently intersects the root
intersectionRatioVisible ratio from 0 to 1
boundingClientRectTarget element rectangle
rootBoundsRoot rectangle

For most UI behavior, isIntersecting is enough. For analytics or progressive effects, use intersectionRatio and thresholds.

Lazy-load images

CODE
const images = document.querySelectorAll("img[data-src]")

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return

      const image = entry.target
      image.src = image.dataset.src
      observer.unobserve(image)
    })
  },
  { rootMargin: "100px" },
)

images.forEach((image) => observer.observe(image))

The positive rootMargin starts loading before the image reaches the viewport, reducing visible blank time.

Infinite scroll

Use a sentinel element near the end of the list:

CODE
const sentinel = document.querySelector("#load-more")

const observer = new IntersectionObserver(
  (entries) => {
    if (entries[0].isIntersecting) {
      loadMoreItems()
    }
  },
  { rootMargin: "50px" },
)

observer.observe(sentinel)

Add request guards so multiple callbacks do not trigger overlapping loads.

Reveal animations

CODE
const elements = document.querySelectorAll(".reveal")

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add("is-visible")
      observer.unobserve(entry.target)
    }
  })
})

elements.forEach((element) => observer.observe(element))

Pair this with transform and opacity animation when possible:

CODE
.reveal {
  opacity: 0;
  transform: translateY(16px);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 200ms ease, transform 200ms ease;
}

For the rendering side of that decision, read Reflow and Repaint: Why CSS Animations Get Janky.

IntersectionObserver vs scroll events

NeedIntersectionObserverScroll event
Know whether a target is visibleBetter defaultRequires manual calculation
Trigger lazy loadingBetter defaultPossible but noisier
Track exact scroll position continuouslyNot suitableBetter fit
Animate based on every scroll pixelNot suitableUse scroll-driven logic carefully

Do not replace every scroll event with IntersectionObserver. Use it when your real question is visibility, not exact scroll distance.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.