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?
- Performance: It doesn’t flood the main thread with useless events. Your code runs only when necessary.
- Efficiency: You delegate a task to the browser that it knows how to do much better and in an optimized way.
- Simplicity: The code is often cleaner and easier to read than complex position calculations based on scroll events.
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:
root
: The container element that acts as the “viewport”. If not specified, it’s the browser’s viewport (document.documentElement
).rootMargin
: Allows you to expand or shrink the intersection area of theroot
. It’s useful for triggering an action before the element is actually visible. Think of it like a CSS margin, but for the detection area.
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
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.
0
: As soon as the first pixel becomes visible.0.5
: When the element is 50% visible.1
: When the element is fully visible.
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
Thresholds
- 100%
- 75%
- 50%
- 25%
- 0%
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.
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.
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()
.