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:

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

Observe an element:

observer.observe(targetElement)

Stop observing:

observer.unobserve(targetElement)
observer.disconnect()

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

Options

const observer = new IntersectionObserver(callback, {
  root: null,
  rootMargin: "100px",
  threshold: [0, 0.5, 1],
})
Option Meaning Default
root The viewport or scroll container used for checking visibility null
rootMargin Extra margin around the root, useful for triggering early "0px"
threshold Visibility ratio or ratios that trigger callbacks 0

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:

Property Meaning
isIntersecting Whether the target currently intersects the root
intersectionRatio Visible ratio from 0 to 1
boundingClientRect Target element rectangle
rootBounds Root rectangle

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

Lazy-load images

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:

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

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:

.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

Need IntersectionObserver Scroll event
Know whether a target is visible Better default Requires manual calculation
Trigger lazy loading Better default Possible but noisier
Track exact scroll position continuously Not suitable Better fit
Animate based on every scroll pixel Not suitable Use 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.