W3docs

API Intersection Observer di JavaScript

Scopri l'API Intersection Observer di JavaScript per rilevare in modo efficiente quando un elemento entra o esce dal viewport — per lazy loading, scroll infinito e animazioni.

L'API Intersection Observer consente di chiedere al browser di segnalare quando un elemento entra o esce dalla parte visibile della pagina. Funziona in modo efficiente e asincrono, senza il costo prestazionale derivante dal monitoraggio manuale degli eventi di scroll. È lo strumento giusto per il lazy loading delle immagini, la costruzione di scroll infinito, l'attivazione di animazioni al comparire dei contenuti e la misurazione della visibilità effettiva di annunci o banner.

Il Problema che Risolve

Prima che esistesse questa API, rispondere alla semplice domanda "questo elemento è attualmente sullo schermo?" era sorprendentemente complicato. Bisognava collegare un listener all'evento scroll (e spesso anche resize), quindi chiamare getBoundingClientRect() su ogni elemento monitorato per confrontarne la posizione con il viewport.

// The old, expensive way — runs on every scroll tick.
window.addEventListener('scroll', () => {
  const rect = element.getBoundingClientRect();
  const inView = rect.top < window.innerHeight && rect.bottom > 0;
  if (inView) {
    // do something
  }
});

Gli eventi di scroll vengono attivati decine di volte al secondo, e getBoundingClientRect() costringe il browser a ricalcolare il layout (un "reflow"). Eseguire questo lavoro in modo sincrono sul thread principale durante uno scroll è una classica fonte di rallentamenti. (Vedi Gestione degli eventi nel DOM e Scrolling in JavaScript per il comportamento di questi eventi.)

IntersectionObserver ribalta il modello. Invece di eseguire il polling delle posizioni, è il browser a monitorare gli elementi e a richiamare il callback solo quando la visibilità cambia effettivamente. Il lavoro avviene al di fuori del thread principale, senza bloccare lo scroll. Per capire perché questo è importante, leggi Ottimizzazione delle prestazioni del DOM. È un fratello stretto dell'API MutationObserver, che monitora le modifiche alla struttura del DOM anziché alla visibilità.

Utilizzo di Base

Si crea un observer con un callback, poi si indicano gli elementi da monitorare con observe().

// 1. Create an observer with a callback and (optional) options.
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Element is now visible:', entry.target);
    } else {
      console.log('Element left the viewport:', entry.target);
    }
  });
});

// 2. Start watching a target element.
const target = document.querySelector('#box');
observer.observe(target);

Il callback riceve un array di entry, una per ogni elemento osservato la cui visibilità è cambiata. Un singolo observer può monitorare molti elementi, ed è il pattern consigliato — creare un observer e chiamare observe() per ogni target, anziché creare un observer per elemento.

Informazione

Il callback viene eseguito in modo asincrono e le modifiche vengono raggruppate — il browser può segnalare più entry in una singola chiamata. Viene inoltre attivato una volta subito dopo l'inizio dell'osservazione, così si ottiene lo stato di visibilità iniziale dell'elemento senza attendere uno scroll. Il supporto dei browser è eccellente in tutti i browser moderni.

Configurazione dell'Observer

Il secondo argomento del costruttore è un object di opzioni con tre proprietà.

root

L'elemento usato come viewport per la verifica della visibilità. Il target deve essere un discendente del root. Quando root è null (valore predefinito), viene usato il viewport del browser.

const observer = new IntersectionObserver(callback, {
  root: document.querySelector('#scroll-container'),
});

rootMargin

Un margine attorno al root, scritto come un valore CSS margin. Espande o riduce il rettangolo usato per i controlli di intersezione. Un trucco comune è un margine inferiore positivo, in modo che gli elementi vengano segnalati come "visibili" prima di scorrere effettivamente nel viewport — utile per caricare i contenuti in anticipo.

const observer = new IntersectionObserver(callback, {
  // Trigger 200px before the element reaches the bottom edge.
  rootMargin: '0px 0px 200px 0px',
});

threshold

Un numero da 0 a 1, o un array di numeri, che indica all'observer a quali rapporti di visibilità attivarsi. 0 significa "attiva non appena un singolo pixel è visibile," 1 significa "attiva solo quando l'elemento è completamente visibile." Un array si attiva ad ogni rapporto elencato.

const observer = new IntersectionObserver(callback, {
  // Fire at 0%, 50%, and 100% visibility.
  threshold: [0, 0.5, 1],
});

Cosa Contiene una Entry

Ogni object nell'array entries descrive la visibilità di un elemento nel momento in cui viene eseguito il callback. Le proprietà più utili sono:

  • isIntersecting — un boolean: true se l'elemento è attualmente visibile nel root.
  • intersectionRatio — quanto dell'elemento è visibile, da 0 a 1.
  • target — l'elemento osservato.
  • boundingClientRect — dimensioni e posizione del target.
  • intersectionRect — la porzione visibile del target.
  • rootBounds — il rettangolo del root (rettificato da rootMargin).
  • time — un timestamp di quando la modifica è stata registrata.
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.target.id, 'visible:', entry.isIntersecting);
    console.log('ratio:', entry.intersectionRatio.toFixed(2));
  }
});

Metodi: observe, unobserve, disconnect

Un'istanza di observer fornisce tre metodi:

  • observe(element) — inizia a monitorare un elemento.
  • unobserve(element) — smette di monitorare un elemento.
  • disconnect() — smette di monitorare tutti gli elementi contemporaneamente.

Una buona pratica fondamentale: una volta che un elemento ha completato il suo lavoro una tantum — ad esempio, un'immagine che ha terminato il lazy loading — chiama unobserve() su di esso, in modo che il browser smetta di tracciare qualcosa che non cambierà mai più.

Caso d'Uso 1 — Lazy Loading delle Immagini

Il lazy loading rinvia il download delle immagini finché non stanno per essere visualizzate. Inserisci l'URL reale in un attributo data-src, osserva ogni immagine e spostala in src quando diventa visibile — poi smetti di osservarla.

<img data-src="photo-1.jpg" alt="First photo" width="600" height="400" />
<img data-src="photo-2.jpg" alt="Second photo" width="600" height="400" />
<img data-src="photo-3.jpg" alt="Third photo" width="600" height="400" />
const images = document.querySelectorAll('img[data-src]');

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

    const img = entry.target;
    img.src = img.dataset.src;        // load the real image
    img.removeAttribute('data-src');
    observer.unobserve(img);          // job done — stop watching it
  });
}, { rootMargin: '0px 0px 200px 0px' }); // start loading a little early

images.forEach((img) => imageObserver.observe(img));
Nota

I browser moderni supportano anche l'attributo nativo loading="lazy" su <img> e <iframe>, che non richiede JavaScript. Ricorri a IntersectionObserver quando hai bisogno di comportamenti personalizzati — la sostituzione di un placeholder, una dissolvenza, o il caricamento di contenuti non-immagine.

Caso d'Uso 2 — Scroll Infinito

Per lo scroll infinito, posiziona un elemento "sentinella" vuoto in fondo alla lista. Quando quella sentinella scorre nel viewport, carica la pagina successiva di dati e aggiungila. Poiché la sentinella rimane in fondo, lo stesso observer continua ad attivarsi man mano che l'utente scorre.

<ul id="list"></ul>
<div id="sentinel"></div>
const list = document.querySelector('#list');
const sentinel = document.querySelector('#sentinel');
let page = 1;
let loading = false;

async function loadMore() {
  if (loading) return;            // guard against overlapping loads
  loading = true;

  const res = await fetch('/api/items?page=' + page);
  const items = await res.json();

  items.forEach((item) => {
    const li = document.createElement('li');
    li.textContent = item.title;
    list.appendChild(li);
  });

  page += 1;
  loading = false;
}

const scrollObserver = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMore();
  }
});

scrollObserver.observe(sentinel);

Caso d'Uso 3 — Animazioni al Comparire durante lo Scroll

Un effetto molto diffuso è quello di far apparire gli elementi con una dissolvenza o uno scorrimento quando entrano nel viewport. Mantieni l'animazione nel CSS e lascia che JavaScript aggiunga una classe al momento giusto.

.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}
const revealItems = document.querySelectorAll('.reveal');

const revealObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target); // animate only once
    }
  });
}, { threshold: 0.15 }); // fire when ~15% is showing

revealItems.forEach((el) => revealObserver.observe(el));

Caso d'Uso 4 — Tracciamento delle Impression / Visibilità

L'analisi statistica spesso richiede di sapere se i contenuti sono stati effettivamente visti, non solo presenti nel DOM. Una threshold più alta consente di registrare un'impression solo quando una porzione significativa di un elemento è visibile.

const adObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.intersectionRatio >= 0.5) {
      sendImpression(entry.target.dataset.adId);
      adObserver.unobserve(entry.target); // count each ad once
    }
  });
}, { threshold: 0.5 }); // at least 50% visible

document.querySelectorAll('.ad').forEach((ad) => adObserver.observe(ad));

È possibile estendere questo meccanismo con un timer che richieda, ad esempio, un intero secondo di visibilità al 50% prima di contare un'impression — uno standard comune per gli annunci "viewable".

L'API Intersection Observer sostituisce i listener di scroll fragili e dispendiosi in termini di prestazioni con un modo pulito e asincrono di reagire alla visibilità degli elementi. Crea un observer, puntalo sui target con observe(), leggi isIntersecting e intersectionRatio nel callback, e chiama unobserve() quando il lavoro di un elemento è terminato. Con root, rootMargin e threshold puoi regolare con precisione quando si attiva — rendendo il lazy loading, lo scroll infinito, le animazioni allo scroll e il tracciamento delle impression semplici e fluidi.

Metti alla Prova le Tue Conoscenze

Pratica
Quale proprietà di entry indica se il target è attualmente visibile nel root?
Quale proprietà di entry indica se il target è attualmente visibile nel root?
Pratica
Qual è il motivo principale per cui IntersectionObserver è preferibile a un listener di scroll che chiama getBoundingClientRect()?
Qual è il motivo principale per cui IntersectionObserver è preferibile a un listener di scroll che chiama getBoundingClientRect()?
Pratica
Quali affermazioni sulle opzioni del costruttore sono corrette?
Quali affermazioni sulle opzioni del costruttore sono corrette?
Was this page helpful?