La fiducia è buona, l'osservazione è meglio: Intersection Exampler v2

Intersection Observer 2 aggiunge la possibilità non solo di osservare le intersezioni in sé, ma anche di rilevare se l'elemento in intersezione era visibile al momento dell'intersezione.

Intersection Observer v1 è una di quelle API probabilmente amate universalmente e, ora che Safari la supporta, è finalmente utilizzabile universalmente in tutti i principali browser. Per un rapido ripasso dell'API, consiglio di guardare il micro-suggerimento superpotenziato di Surma su Intersection Observer v1, incorporato di seguito. Puoi anche leggere l'articolo approfondito di Surma. Intersection Observer v1 è stato utilizzato per una vasta gamma di casi d'uso, come il caricamento lento di immagini e video, l'invio di notifiche quando gli elementi raggiungono position: sticky, l'attivazione di eventi di analisi, e molti altri.

Per i dettagli completi, consulta la documentazione su Intersection Observer su MDN, ma come breve promemoria, questo è l'aspetto dell'API Intersection Observer v1 nel caso più basilare:

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

Quali problemi sono presenti nella versione 1 di Intersection Observer?

Per essere chiari, Intersection Observer v1 è fantastico, ma non è perfetto. Esistono alcuni casi limite in cui l'API non è sufficiente. Diamo un'occhiata più da vicino. L'API Intersection Observer 1.0 può indicare quando un elemento viene visualizzato nel viewport della finestra, ma non indica se l'elemento è coperto da altri contenuti della pagina (ovvero quando è occluso) o se la sua visualizzazione è stata modificata da effetti visivi come transform, opacity, filter e così via, che possono effettivamente renderlo invisibile.

Per un elemento nel documento di primo livello, queste informazioni possono essere determinate analizzando il DOM tramite JavaScript, ad esempio tramite DocumentOrShadowRoot.elementFromPoint(), quindi analizzando più in dettaglio. Al contrario, non è possibile ottenere le stesse informazioni se l'elemento in questione si trova in un iframe di terze parti.

Perché la visibilità effettiva è così importante?

Purtroppo, internet è un luogo che attira malintenzionati con intenzioni peggiori. Ad esempio, un publisher losco che pubblica annunci pay-per-click su un sito di contenuti potrebbe essere incentivato a indurre con l'inganno le persone a fare clic sui suoi annunci per aumentare il pagamento dell'annuncio del publisher (almeno per un breve periodo, fino a quando la rete pubblicitaria non li individua). In genere, questi annunci vengono pubblicati in iframe. Ora, se il publisher volesse indurre gli utenti a fare clic su questi annunci, potrebbe rendere gli iframe degli annunci completamente trasparenti applicando una regola CSS iframe { opacity: 0; } e sovrapponendoli a qualcosa di accattivante, ad esempio un video di un gatto carino su cui gli utenti vorrebbero fare clic. Questo tipo di attacco si chiama clickjacking. Puoi vedere un attacco di clickjacking in azione nella sezione superiore di questa demo (prova a "guardare" il video del gatto e attiva la "modalità trucco"). Noterai che l'annuncio nell'iframe "ritiene" di aver ricevuto clic legittimi, anche se era completamente trasparente quando hai fatto (fingendo involontariamente) di averlo fatto.

Indurre un utente a fare clic su un annuncio impostandolo come trasparente e sovrapponendolo a qualcosa di accattivante.

In che modo Intersection Observer v2 risolve il problema?

Intersection Observer v2 introduce il concetto di monitoraggio della "visibilità" effettiva di un elemento di destinazione come la definirebbe un essere umano. Se imposti un'opzione nel costruttore IntersectionObserver, le istanze di IntersectionObserverEntry intersecanti conterranno un nuovo campo booleano denominato isVisible. Un valore true per isVisible è una garanzia solida dell'implementazione di base che l'elemento target non è completamente ostruito da altri contenuti e non sono stati applicati effetti visivi che potrebbero alterare o distorcere la sua visualizzazione sullo schermo. Al contrario, un valore false indica che l'implementazione non può fornire questa garanzia.

Un dettaglio importante della specifica è che l'implementazione è autorizzata a segnalare falsi negativi (ovvero, l'impostazione di isVisible su false anche quando l'elemento target è completamente visibile e non modificato). Per motivi di prestazioni o altri, i browser si limitano a lavorare con caselle delimitanti e geometria rettangolare; non cercano di ottenere risultati perfetti a livello di pixel per modifiche come border-radius.

Detto questo, i falsi positivi non sono consentiti in nessuna circostanza (ad esempio, l'impostazione di isVisible su true quando l'elemento target non è completamente visibile e non modificato).

Che aspetto ha il nuovo codice in pratica?

Il costruttore IntersectionObserver ora accetta due proprietà di configurazione aggiuntive: delay e trackVisibility. delay è un numero che indica il ritardo minimo in millisecondi tra le notifiche da parte dell'osservatore per un determinato obiettivo. trackVisibility è un valore booleano che indica se l'osservatore monitorerà le modifiche alla visibilità di un target.

È importante notare che quando trackVisibility è true, delay deve essere almeno 100 (ovvero non più di una notifica ogni 100 ms). Come accennato in precedenza, il calcolo della visibilità è costoso e questo requisito è una precauzione contro il degrado delle prestazioni e il consumo della batteria. Lo sviluppatore responsabile utilizzerà il valore massimo tollerabile per il ritardo.

In base alle attuali specifiche, la visibilità viene calcolata nel seguente modo:

  • Se l'attributo trackVisibility dell'osservatore è false, il target è considerato visibile. Questo corrisponde al comportamento attuale della versione 1.

  • Se il target ha una matrice di trasformazione efficace diversa da una traduzione 2D o da un upscaling 2D proporzionale, il target è considerato invisibile.

  • Se il target, o qualsiasi elemento nella sua catena di blocchi che la contiene, ha un'opacità effettiva diversa da 1,0, il target è considerato invisibile.

  • Se al target o a un elemento della blockchain contenente sono stati applicati filtri, il target è considerato invisibile.

  • Se l'implementazione non può garantire che il target sia completamente libero da altri contenuti della pagina, il target è considerato invisibile.

Ciò significa che le implementazioni attuali sono piuttosto conservative per quanto riguarda la garanzia della visibilità. Ad esempio, l'applicazione di un filtro di scala di grigi quasi impercettibile come filter: grayscale(0.01%) o l'impostazione di una trasparenza quasi invisibile con opacity: 0.99 renderebbe tutti invisibile l'elemento.

Di seguito è riportato un breve esempio di codice che illustra le nuove funzionalità dell'API. Puoi vedere la logica di monitoraggio dei clic in azione nella seconda sezione della demo (per ora, prova a "guardare" il video del cucciolo). Assicurati di attivare nuovamente la "modalità trucco" per convertirti immediatamente in un publisher losco e scopri come Intersection Observer v2 impedisce il monitoraggio dei clic non legittimi sugli annunci. Questa volta, Intersection Observer v2 è dalla nostra parte. 🎉

Intersection Observer v2 impedisce il clic involontario su un annuncio.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

Ringraziamenti

Grazie a Simeon Vincent, Yoav Weiss e Mathias Bynens per aver letto questo articolo, e a Stefan Zager anche per aver esaminato e implementato la funzionalità in Chrome. Immagine hero di Sergey Semin su Unsplash.