Intersection Observer: Smooth Animations Without Blocking the Page

Intersection Observer: Smooth Animations Without Blocking the Page

Written by Francesco Di Donato July 9, 2025 4 minutes reading

The Problem: Jank When You Scroll

You know when a scroll-triggered animation makes the page feel choppy? The fault is (almost never) your animation, but how you’re triggering it.

The problem is JavaScript’s scroll event. Listening to this event is inefficient because it forces the browser to run your code for every single pixel of scrolling.

This overloads the main thread, the “brain” of the web page that handles everything: from rendering graphics and managing layout to running your JavaScript. When the main thread is too busy processing hundreds of scroll events, it doesn’t have time for anything else. The result? A blocked interface, laggy animations, and a terrible user experience.


The Solution: The Intersection Observer

The Intersection Observer is the modern, efficient alternative. Instead of constantly asking “where are you?”, you tell the browser: “Notify me only when this element enters the screen.”

It’s an asynchronous approach that doesn’t block the main thread. The browser does the dirty work for you, in an optimized way, and invokes your function (callback) only at the exact moments it’s needed.

Why is it better?


How It Works

To use the Intersection Observer, you create an “observer” and tell it what to watch and when to react.

You can pass it options which can contain:

const htmlElement = document.querySelector("div");

const observer = new IntersectionObserver(() => {
  // Do something
});
observer.observe(htmlElement);

You are not required to pass options. In this case, the callback is called as soon as the <div /> appears on the screen.

If you want to pass options, you do so as a second argument.

new IntersectionObserver(callback, {
  root: someWrapper,
  rootMargin: "10px 20px"
});

In the lab below, try using values like 100px (triggers the event 100px early) or -50px (triggers the event 50px after it has entered).

isIntersecting

FALSE

...scorri...
TARGET

Another useful option is threshold. This is one or more numeric values (from 0.0 to 1.0) that define at what percentage of the element’s visibility the observer should react.

You can also define an array of thresholds to receive notifications at different stages of visibility.

const observer = new IntersectionObserver(callback, {
  // Notify me when the element:
  // - enters (0)
  // - is a quarter visible (0.25)
  // - is halfway visible (0.5)
  // etc.
  threshold: [0, 0.25, 0.5, 0.75, 1],
});

// This function is called every time a threshold is crossed.
function callback([entry]) {
  updateColor(entry.intersectionRatio)
}

Observe how different thresholds trigger the callback. To understand which threshold you’re at, you can query entry.intersectionRatio.

Multiple Thresholds

Visibility0.0%

Thresholds

  • 100%
  • 75%
  • 50%
  • 25%
  • 0%
25%
50%
75%

Practical Examples

Let’s look at two use cases where the Intersection Observer shines.

1. Infinite Scroll

Load new content as the user gets close to the end of the page. With the Intersection Observer, you just need to observe a “trigger” element at the bottom of the list. When it becomes visible, you load more content. Simple and performant.

List Item 1
List Item 2
List Item 3
List Item 4
List Item 5
List Item 6
List Item 7
List Item 8
List Item 9
List Item 10
List Item 11
List Item 12
List Item 13
List Item 14
List Item 15
List Item 16
List Item 17
List Item 18
List Item 19
List Item 20

2. Lazy Loading Images

Why waste bandwidth and slow down the initial load for images the user might never see? With “lazy loading,” the image is downloaded only when it approaches the viewport.

Here, we observe the image and set a 50% threshold. When the image is halfway visible, we start its download and immediately stop observing it, because its job is done.

const image = document.getElementById("my-image");

const observer = new IntersectionObserver(([entry], obs) => {
  // Has the image entered the observation area?
  if (entry.isIntersecting) {
    // Load the image (e.g., by changing the src attribute)
    image.src = image.dataset.src;

    // Stop observing. The job is done.
    obs.unobserve(image);
  }
}, {
  threshold: 0.5 // Trigger at 50% visibility
});

observer.observe(image);

Scorri verso il basso all'interno di quest'area...

C'è molto testo qui per creare spazio di scorrimento. L'obiettivo è dimostrare che l'immagine sottostante non verrà caricata finché non sarà visibile per almeno la metà.

Continuando a scorrere, ti avvicinerai all'immagine. Tieni d'occhio la scheda "Network" negli strumenti per sviluppatori del tuo browser per vedere la richiesta partire solo al momento giusto.



Segnaposto sfocato


Ecco l'immagine! Se hai visto un segnaposto sfocato per un istante, significa che il lazy loading ha funzionato correttamente.

Questo approccio è estremamente utile per pagine con molte immagini, come gallerie o feed di prodotti, perché riduce drasticamente il tempo di caricamento iniziale della pagina e il consumo di dati.

Cleanup

Always remember to remove the observer when it’s no longer needed. You can use observer.disconnect().