Ora mi vedi: come differire, caricare pigro e agire con IntersectionObserver

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Le informazioni sull'intersezione sono necessarie per molte ragioni, come il caricamento lento delle immagini. Ma c'è molto di più. È tempo di ottenere una migliore comprensione e prospettive diverse sull'API di Intersection Observer. Pronto?

C'era una volta uno sviluppatore web che convinse con successo i suoi clienti che i siti non dovrebbero avere lo stesso aspetto in tutti i browser, si preoccupava dell'accessibilità ed è stato uno dei primi ad adottare le griglie CSS. Ma nel profondo del suo cuore era la performance la sua vera passione: ha costantemente ottimizzato, minimizzato, monitorato e persino impiegato trucchi psicologici nei suoi progetti.

Poi, un giorno, ha appreso del caricamento lento delle immagini e di altre risorse che non sono immediatamente visibili agli utenti e non sono essenziali per il rendering di contenuti significativi sullo schermo. Era l'inizio dell'alba: lo sviluppatore è entrato nel mondo malvagio dei plug-in jQuery a caricamento lento (o forse nel mondo non così malvagio degli attributi async e defer ). Alcuni dicono addirittura che sia entrato direttamente nel cuore di tutti i mali: il mondo degli ascoltatori di eventi di scroll . Non sapremo mai con certezza dove sia finito, ma ancora una volta questo sviluppatore è assolutamente fittizio e qualsiasi somiglianza con qualsiasi sviluppatore è semplicemente casuale.

uno sviluppatore web
Il fittizio sviluppatore web

Bene, ora puoi dire che il vaso di Pandora è stato aperto e che il nostro sviluppatore fittizio non rende il problema meno reale. Al giorno d'oggi, dare la priorità ai contenuti above-the-fold è diventato estremamente importante per le prestazioni dei nostri progetti web sia dal punto di vista della velocità che del peso della pagina.

Altro dopo il salto! Continua a leggere sotto ↓

In questo articolo, usciremo dall'oscurità della scroll e parleremo del modo moderno di caricare le risorse. Non solo il caricamento lento delle immagini, ma anche il caricamento di qualsiasi risorsa. Inoltre, la tecnica di cui parleremo oggi è in grado di fare molto di più del semplice caricamento lento delle risorse: saremo in grado di fornire qualsiasi tipo di funzionalità differita in base alla visibilità degli elementi agli utenti.

Osservatore di intersezione: ora mi vedi

Signore e signori, parliamo dell'Intersection Observer API. Ma prima di iniziare, diamo un'occhiata al panorama degli strumenti moderni che ci ha portato a IntersectionObserver .

Il 2017 è stato un anno molto positivo per gli strumenti integrati nei nostri browser, che ci hanno aiutato a migliorare la qualità e lo stile della nostra base di codice senza troppi sforzi. In questi giorni, il web sembra allontanarsi da soluzioni sporadiche basate su soluzioni molto diverse da quelle tipiche a un approccio più ben definito delle interfacce di Observer (o semplicemente "Osservatori"): MutationObserver ben supportato ha ottenuto nuovi membri della famiglia che sono stati rapidamente adottato nei browser moderni:

  • IntersezioneOsservatore e
  • PerformanceObserver (come parte della specifica Performance Timeline Livello 2).

Un altro potenziale membro della famiglia, FetchObserver, è un work in progress e ci guida maggiormente nelle terre di un proxy di rete, ma oggi vorrei invece parlare di più del front-end.

IntersectionObserver e PerformanceObserver sono i nuovi membri della famiglia Observers.
IntersectionObserver e PerformanceObserver sono i nuovi membri della famiglia Observers.

PerformanceObserver e IntersectionObserver mirano ad aiutare gli sviluppatori front-end a migliorare le prestazioni dei loro progetti in diversi punti. Il primo ci fornisce lo strumento per il monitoraggio dell'utente reale, mentre il secondo è lo strumento, fornendoci un miglioramento tangibile delle prestazioni. Come accennato in precedenza, questo articolo darà uno sguardo dettagliato esattamente a quest'ultimo: IntersectionObserver . Per comprendere in particolare i meccanismi di IntersectionObserver , dovremmo dare un'occhiata a come dovrebbe funzionare un generico Observer nel web moderno.

Suggerimento per professionisti : puoi saltare la teoria e tuffarti subito nella meccanica di IntersectionObserver o, anche oltre, direttamente alle possibili applicazioni di IntersectionObserver .

Osservatore vs. Evento

Un "Osservatore", come suggerisce il nome, ha lo scopo di osservare qualcosa che accade nel contesto di una pagina. Gli osservatori possono guardare qualcosa che accade su una pagina, come le modifiche al DOM. Possono anche guardare gli eventi del ciclo di vita della pagina. Gli osservatori possono anche eseguire alcune funzioni di callback. Ora un lettore attento potrebbe immediatamente individuare il problema qui e chiedere: "Allora, qual è il punto? Non abbiamo già eventi per questo scopo? Cosa rende gli osservatori diversi?" Ottimo punto! Diamo un'occhiata più da vicino e risolviamo.

Osservatore vs. Evento: qual è la differenza?
Osservatore vs. Evento: qual è la differenza?

La differenza cruciale tra Evento normale e Observer è che per impostazione predefinita, il primo reagisce in modo sincrono per ogni occorrenza dell'evento, influenzando la reattività del thread principale, mentre il secondo dovrebbe reagire in modo asincrono senza influire così tanto sulle prestazioni. Almeno, questo è vero per gli osservatori attualmente presentati: tutti si comportano in modo asincrono e non credo che questo cambierà in futuro.

Ciò porta alla principale differenza nella gestione dei callback degli osservatori che potrebbe confondere i principianti: la natura asincrona degli osservatori potrebbe comportare il passaggio di più osservabili a una funzione di callback contemporaneamente. Per questo motivo, la funzione di callback dovrebbe aspettarsi non una singola voce ma un Array di voci (anche se a volte l'array conterrà solo una voce al suo interno).

Inoltre, alcuni osservatori (in particolare quello di cui stiamo parlando oggi) forniscono proprietà precalcolate molto utili, che altrimenti usavamo per calcolare noi stessi usando metodi e proprietà costosi (dal punto di vista delle prestazioni) quando usavamo eventi regolari. Per chiarire questo punto, vedremo un esempio più avanti nell'articolo.

Quindi, se è difficile per qualcuno allontanarsi dal paradigma degli eventi, direi che gli osservatori sono eventi sotto steroidi. Un'altra descrizione potrebbe essere: Gli osservatori sono un nuovo livello di approssimazione in cima agli eventi. Ma indipendentemente dalla definizione che preferisci, dovrebbe essere ovvio che gli osservatori non sono destinati a sostituire gli eventi (almeno non ancora); ci sono abbastanza casi d'uso per entrambi e possono vivere felicemente fianco a fianco.

Gli osservatori non intendono sostituire gli eventi: entrambi possono vivere insieme felicemente.
Gli osservatori non intendono sostituire gli eventi: entrambi possono vivere insieme felicemente.

Struttura dell'osservatore generico

La struttura generica di un Observer (qualsiasi di quelli disponibili al momento della scrittura) è simile a questa:

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

Ancora una volta, nota che entries sono una Array di valori, non una singola voce.

Questa è la struttura generica: le implementazioni di particolari osservatori differiscono per gli argomenti passati al suo observe() e gli argomenti passati al suo callback. Ad esempio MutationObserver dovrebbe anche ottenere un oggetto di configurazione per saperne di più su quali modifiche nel DOM osservare. PerformanceObserver non osserva i nodi nel DOM, ma ha invece il set dedicato di tipi di voci che può osservare.

Concludiamo qui la parte "generica" ​​di questa discussione e approfondiamo l'argomento dell'articolo di oggi — IntersectionObserver .

Decostruire IntersectionObserver

Decostruire IntersectionObserver
Decostruire IntersectionObserver

Prima di tutto, risolviamo cos'è IntersectionObserver .

Secondo MDN:

L'API Intersection Observer fornisce un modo per osservare in modo asincrono i cambiamenti nell'intersezione di un elemento di destinazione con un elemento antenato o con il viewport di un documento di primo livello.

In poche parole, IntersectionObserver osserva in modo asincrono la sovrapposizione di un elemento con un altro elemento. Parliamo a cosa servono questi elementi in IntersectionObserver .

Inizializzazione dell'osservatore di intersezione

In uno dei paragrafi precedenti abbiamo visto la struttura di un Osservatore generico. IntersectionObserver estende un po' questa struttura. Innanzitutto, questo tipo di Observer richiede una configurazione con tre elementi principali:

  • root : Questo è l'elemento radice utilizzato per l'osservazione. Definisce il "frame di cattura" di base per gli elementi osservabili. Per impostazione predefinita, la root è il viewport del tuo browser ma può essere davvero qualsiasi elemento nel tuo DOM (quindi imposti root su qualcosa come document.getElementById('your-element') ). Tieni presente però che gli elementi che vuoi osservare devono "vivere" nell'albero DOM di root in questo caso.
la proprietà di root della configurazione di IntersectionObserver
la proprietà root definisce la base per 'catturare frame' per i nostri elementi.
  • rootMargin : Definisce il margine attorno all'elemento root che estende o rimpicciolisce il "frame di acquisizione" quando le dimensioni della root non forniscono sufficiente flessibilità. Le opzioni per i valori di questa configurazione sono simili a quelle di margin in CSS, come rootMargin: '50px 20px 10px 40px' (in alto, in basso a destra, a sinistra). I valori possono essere abbreviati (come rootMargin: '50px' ) e possono essere espressi in px o % . Per impostazione predefinita, rootMargin: '0px' .
rootMargin della configurazione di IntersectionObserver
La proprietà rootMargin espande/contrae il 'fotogramma di acquisizione' che è definito da root .
  • threshold : Non è sempre necessario reagire istantaneamente quando un elemento osservato interseca un bordo del "fotogramma di acquisizione" (definito come una combinazione di root e rootMargin ). threshold definisce la percentuale di tale intersezione alla quale l'Osservatore dovrebbe reagire. Può essere definito come un valore singolo o come una matrice di valori. Per comprendere meglio l'effetto della threshold (so che a volte potrebbe creare confusione), ecco alcuni esempi:
    • threshold: 0 : Il valore predefinito IntersectionObserver dovrebbe reagire quando il primo o l'ultimo pixel di un elemento osservato interseca uno dei bordi del "fotogramma di acquisizione". Tieni presente che IntersectionObserver è indipendente dalla direzione, il che significa che reagirà in entrambi gli scenari: a) quando l'elemento entra e b) quando lascia il "frame di acquisizione".
    • threshold: 0.5 : Observer dovrebbe essere attivato quando il 50% di un elemento osservato interseca il “cattura frame”;
    • threshold: [0, 0.2, 0.5, 1] ​​: L'osservatore dovrebbe reagire in 4 casi:
      • Il primo pixel di un elemento osservato entra nel "frame di acquisizione": l'elemento non è ancora realmente all'interno di quel frame, oppure l'ultimo pixel dell'elemento osservato lascia il "frame di acquisizione": l'elemento non è più all'interno del frame;
      • Il 20% dell'elemento è all'interno del "frame di acquisizione" (di nuovo, la direzione non ha importanza per IntersectionObserver );
      • Il 50% dell'elemento è all'interno del “frame di cattura”;
      • Il 100% dell'elemento si trova all'interno del "frame di acquisizione". Questo è esattamente opposto alla threshold: 0 .
threshold della configurazione di IntersectionObserver
la proprietà threshold definisce di quanto l'elemento deve intersecare il nostro "frame di acquisizione" prima che l'Observer venga attivato.

Per informare il nostro IntersectionObserver della nostra configurazione desiderata, passiamo semplicemente il nostro oggetto di config nel costruttore del nostro Observer insieme alla nostra funzione di callback in questo modo:

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

Ora, dovremmo fornire a IntersectionObserver l'elemento effettivo da osservare. Questo viene fatto semplicemente passando l'elemento alla funzione observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

Un paio di cose da notare su questo elemento osservato:

  • È stato menzionato in precedenza, ma vale la pena menzionarlo di nuovo: nel caso in cui imposti root come elemento nel DOM, l'elemento osservato dovrebbe trovarsi all'interno dell'albero DOM di root .
  • IntersectionObserver può accettare un solo elemento per l'osservazione alla volta e non supporta la fornitura batch per le osservazioni. Ciò significa che se hai bisogno di osservare più elementi (diciamo più immagini su una pagina), devi scorrere su tutti loro e osservarli separatamente:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • Quando si carica una pagina con Observer attivo, è possibile notare che il callback di IntersectionObserver è stato attivato per tutti gli elementi osservati contemporaneamente. Anche quelli che non corrispondono alla configurazione fornita. "Beh... non proprio quello che mi aspettavo", è il solito pensiero quando si sperimenta questo per la prima volta. Ma non confonderti qui: questo non significa necessariamente che quegli elementi osservati in qualche modo intersecano il "frame di cattura" durante il caricamento della pagina.
Screenshot di DevTools con IntersectionObserver attivato per tutti gli elementi contemporaneamente.
IntersectionObserver verrà attivato per tutti gli elementi osservati una volta registrati, ma ciò non significa che tutti intersechino il nostro "frame di acquisizione".

Ciò significa, tuttavia, che la voce per questo elemento è stata inizializzata ed è ora controllata dal tuo IntersectionObserver . Tuttavia, ciò potrebbe aggiungere rumore non necessario alla tua funzione di callback e diventa tua responsabilità rilevare quali elementi intersecano effettivamente il "frame di acquisizione" e di cui non dobbiamo ancora tenere conto. Per capire come eseguire tale rilevamento, approfondiamo un po' l'anatomia della nostra funzione di callback e diamo un'occhiata a in cosa consistono tali voci.

Richiamata dell'osservatore di intersezione

Prima di tutto, la funzione di callback per un IntersectionObserver accetta due argomenti e ne parleremo in ordine inverso a partire dal secondo argomento. Insieme alla summenzionata Array di voci osservate, che intersecano il nostro "frame di acquisizione", la funzione di callback ottiene lo stesso Observer come secondo argomento.

Riferimento all'osservatore stesso

 new IntersectionObserver(function(entries, SELF) {…});

Ottenere il riferimento all'Observer stesso è utile in molti scenari quando si desidera interrompere l'osservazione di un elemento dopo che è stato rilevato IntersectionObserver per la prima volta. Scenari come il caricamento lento delle immagini, il recupero differito di altre risorse, ecc. sono di questo tipo. Quando si desidera interrompere l'osservazione di un elemento, IntersectionObserver fornisce un metodo unobserve(element-to-stop-observing) che può essere eseguito nella funzione di callback dopo aver eseguito alcune azioni sull'elemento osservato (come il caricamento lento effettivo di un'immagine, ad esempio ).

Alcuni di questi scenari verranno esaminati ulteriormente nell'articolo, ma con questa seconda argomentazione fuori dal nostro modo, arriviamo agli attori principali di questo gioco di callback.

IntersectionObserverEntry

 new IntersectionObserver(function(ENTRIES, self) {…});

Le entries che otteniamo nella nostra funzione di callback come Array sono del tipo speciale: IntersectionObserverEntry . Questa interfaccia ci fornisce un insieme predefinito e precalcolato di proprietà riguardanti ogni particolare elemento osservato. Diamo un'occhiata a quelli più interessanti.

Innanzitutto, le voci di tipo IntersectionObserverEntry informazioni su tre diversi rettangoli, che definiscono le coordinate e i limiti degli elementi coinvolti nel processo:

  • rootBounds : un rettangolo per il "frame di acquisizione" ( root + rootMargin );
  • boundingClientRect : un rettangolo per l'elemento osservato stesso;
  • intersectionRect : un'area della "fotogramma di cattura" intersecata dall'elemento osservato.
Rettangoli di IntersectionObserverEntry
Tutti i rettangoli di delimitazione coinvolti in IntersectionObserverEntry vengono calcolati per te.

La cosa davvero interessante di questi rettangoli calcolati per noi in modo asincrono è che ci fornisce informazioni importanti relative al posizionamento dell'elemento senza che noi chiamiamo getBoundingClientRect() , offsetTop , offsetLeft e altre proprietà e metodi di posizionamento costosi che attivano il thrashing del layout. Pura vittoria per le prestazioni!

Un'altra proprietà dell'interfaccia IntersectionObserverEntry che è interessante per noi è isIntersecting . Questa è una proprietà di convenienza che indica se l'elemento osservato sta attualmente intersecando il "frame di acquisizione" o meno. Ovviamente potremmo ottenere queste informazioni guardando l' intersectionRect (se questo rettangolo non è 0×0, l'elemento sta intersecando il "fotogramma di cattura") ma avere questo precalcolato per noi è abbastanza conveniente.

isIntersecting può essere utilizzato per scoprire se l'elemento osservato sta appena entrando nel "fotogramma di cattura" o se lo sta già lasciando. Per scoprirlo, salva il valore di questa proprietà come flag globale e quando la nuova voce per questo elemento arriva alla tua funzione di callback, confronta il suo nuovo isIntersecting con quel flag globale:

  • Se era false e ora è true , allora l'elemento sta entrando nel “frame di cattura”;
  • Se è l'opposto ed è false ora mentre era true prima, l'elemento sta lasciando il "fotogramma di cattura".

isIntersecting è esattamente la proprietà che ci aiuta a risolvere il problema di cui abbiamo discusso in precedenza, ovvero separare le voci per gli elementi che intersecano realmente il "frame di acquisizione" dal rumore di quelli che sono solo l'inizializzazione della voce.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

NOTA : in Microsoft Edge 15, la proprietà isIntersecting non è stata implementata, restituendo undefined nonostante il supporto completo per IntersectionObserver in caso contrario. Questo problema è stato risolto a luglio 2017 ed è disponibile da Edge 16.

L'interfaccia IntersectionObserverEntry fornisce un'altra proprietà di convenienza precalcolata: intersectionRatio . Questo parametro può essere utilizzato per gli stessi scopi di isIntersecting ma fornisce un controllo più granulare poiché è un numero a virgola mobile anziché un valore booleano. Il valore di intersectionRatio indica quanta parte dell'area dell'elemento osservato interseca il "riquadro di acquisizione" (il rapporto tra l'area di intersectionRect e l'area di boundingClientRect ). Anche in questo caso, potremmo fare questo calcolo noi stessi usando le informazioni da quei rettangoli, ma è bene averlo fatto per noi.

Non ti sembra già familiare? Sì, la proprietà <code>intersectionRatio</code> è simile alla proprietà <code>threshold</code> della configurazione di Observer. La differenza è che quest'ultimo definisce <em>quando</em> attivare Observer, il primo indica la situazione dell'intersezione reale (che è leggermente diversa da <code>soglia</code> a causa della natura asincrona di Observer).
Non ti sembra già familiare? Sì, la proprietà di intersectionRatio è simile alla proprietà di threshold della configurazione di Observer. La differenza è che quest'ultimo definisce *quando* accendere Observer, il primo indica la situazione dell'intersezione reale (che è leggermente diversa dalla threshold a causa della natura asincrona di Observer).

target è un'altra proprietà dell'interfaccia IntersectionObserverEntry a cui potresti dover accedere abbastanza spesso. Ma non c'è assolutamente alcuna magia qui: è solo l'elemento originale che era stato passato alla funzione observe() del tuo osservatore. Proprio come event.target a cui sei abituato quando lavori con gli eventi.

Per ottenere l'elenco completo delle proprietà per l'interfaccia IntersectionObserverEntry , controllare le specifiche.

Possibili applicazioni

Mi rendo conto che molto probabilmente sei arrivato a questo articolo proprio a causa di questo capitolo: chi se ne frega della meccanica quando dopo tutto abbiamo frammenti di codice per il copia e incolla? Quindi non ti disturberò con ulteriori discussioni ora: stiamo entrando nel paese del codice e degli esempi. Spero che i commenti inclusi nel codice rendano le cose più chiare.

Funzionalità differita

Prima di tutto, esaminiamo un esempio che rivela i principi di base alla base dell'idea di IntersectionObserver . Diciamo che hai un elemento che deve fare molti calcoli una volta che è sullo schermo. Ad esempio, il tuo annuncio dovrebbe registrare una visualizzazione solo quando è stato effettivamente mostrato a un utente. Ma ora, immaginiamo di avere un elemento carosello riprodotto automaticamente da qualche parte sotto la prima schermata della tua pagina.

Carosello sotto la prima schermata dell'applicazione
Quando abbiamo un carosello o qualsiasi altra funzionalità di sollevamento pesante sotto l'ovile della nostra applicazione, è uno spreco di risorse iniziare subito il bootstrap/caricarlo.

Gestire una giostra, in generale, è un compito pesante. Di solito, coinvolge timer JavaScript, calcoli per scorrere automaticamente gli elementi, ecc. Tutte queste attività caricano il thread principale e, quando viene eseguito in modalità di riproduzione automatica, è difficile per noi sapere quando il nostro thread principale ottiene questo successo. Quando si parla di dare la priorità ai contenuti sul nostro primo schermo e si desidera ottenere la prima vernice significativa e Time To Interactive il prima possibile, il thread principale bloccato diventa un collo di bottiglia per le nostre prestazioni.

Per risolvere il problema, potremmo rinviare la riproduzione di un tale carosello fino a quando non entra nella finestra del browser. In questo caso, utilizzeremo le nostre conoscenze e l'esempio per il parametro isIntersecting dell'interfaccia IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

Qui, riproduciamo il carosello solo quando entra nel nostro viewport. Si noti l'assenza dell'oggetto config passato all'inizializzazione di IntersectionObserver : questo significa che ci affidiamo alle opzioni di configurazione predefinite. Quando il carosello esce dal nostro viewport, dovremmo smettere di riprodurlo per non spendere risorse per gli elementi non importanti.

Caricamento pigro delle risorse

Questo è, probabilmente, il caso d'uso più ovvio per IntersectionObserver : non vogliamo spendere risorse per scaricare qualcosa di cui l'utente non ha bisogno in questo momento. Ciò darà un enorme vantaggio ai tuoi utenti: gli utenti non dovranno scaricare e i loro dispositivi mobili non avranno bisogno di analizzare e compilare molte informazioni inutili di cui non hanno bisogno al momento. Non sorprende affatto, aiuterà anche le prestazioni della tua app.

Immagini a caricamento lento below the fold
Risorse a caricamento lento come le immagini che si trovano sotto il primo schermo: l'applicazione più ovvia di IntersectionObserver.

In precedenza, per rinviare il download e l'elaborazione delle risorse fino al momento in cui l'utente poteva visualizzarle sullo schermo, avevamo a che fare con listener di eventi su eventi come scroll . Il problema è ovvio: questo ha innescato gli ascoltatori troppo spesso. Quindi abbiamo dovuto elaborare l'idea di limitare o eliminare il rimbalzo dell'esecuzione del callback. Ma tutto questo ha aggiunto molta pressione sul nostro thread principale bloccandolo quando ne avevamo più bisogno.

Quindi, tornando a IntersectionObserver in uno scenario di caricamento lento, cosa dovremmo tenere d'occhio? Esaminiamo un semplice esempio di immagini a caricamento lento.

Guarda il caricamento Pen Lazy in IntersectionObserver di Denys Mishunov (@mishunov) su CodePen.

Guarda il caricamento Pen Lazy in IntersectionObserver di Denys Mishunov (@mishunov) su CodePen.

Prova a scorrere lentamente quella pagina fino al "terzo schermo" e guarda la finestra di monitoraggio nell'angolo in alto a destra: ti farà sapere quante immagini sono state scaricate finora.

Al centro del markup HTML per questa attività si trova una semplice sequenza di immagini:

 … <img data-src="https://blah-blah.com/foo.jpg"> …

Come puoi vedere, le immagini dovrebbero arrivare senza tag src : una volta che un browser vede l'attributo src , inizierà subito a scaricare quell'immagine che è opposta alle nostre intenzioni. Quindi non dovremmo inserire quell'attributo sulle nostre immagini in HTML e, invece, potremmo fare affidamento su alcuni attributi di data- come data-src qui.

Un'altra parte di questa soluzione è, ovviamente, JavaScript. Concentriamoci sui bit principali qui:

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

Dal punto di vista della struttura, non c'è nulla di nuovo qui: abbiamo già trattato tutto questo in precedenza:

  • Riceviamo tutti i messaggi con i nostri attributi data-src ;
  • Set config : per questo scenario si vuole espandere il proprio “fotogramma di cattura” per rilevare elementi un po' più in basso rispetto alla parte inferiore del viewport;
  • Registra IntersectionObserver con quella configurazione;
  • Scorri le nostre immagini e aggiungile tutte per essere osservate da questo IntersectionObserver ;

La parte interessante avviene all'interno della funzione di callback invocata sulle voci. Ci sono tre passaggi essenziali coinvolti.

  1. Prima di tutto, elaboriamo solo gli elementi che intersecano realmente la nostra "fotogramma di cattura". Questo frammento dovrebbe esserti familiare ormai.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. Quindi, in qualche modo elaboriamo la voce convertendo la nostra immagine con data-src in un vero <img src="…"> .

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Ciò attiverà il browser per scaricare finalmente l'immagine. preloadImage() è una funzione molto semplice che non vale la pena menzionare qui. Basta leggere la fonte.

  3. Passaggio successivo e finale: poiché il caricamento lento è un'azione una tantum e non è necessario scaricare l'immagine ogni volta che l'elemento entra nel nostro "frame di acquisizione", dovremmo unobserve l'immagine già elaborata. Allo stesso modo in cui dovremmo farlo con element.removeEventListener() per i nostri eventi regolari quando non sono più necessari per prevenire perdite di memoria nel nostro codice.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

Nota. Invece di unobserve(event.target) potremmo anche chiamare disconnect() : disconnette completamente il nostro IntersectionObserver e non osserverebbe più le immagini. Questo è utile se l'unica cosa che ti interessa è il primo successo in assoluto per il tuo Observer. Nel nostro caso, abbiamo bisogno dell'Observer per continuare a monitorare le immagini, quindi non dovremmo disconnetterci ancora.

Sentiti libero di fare un fork dell'esempio e giocare con diverse impostazioni e opzioni. C'è una cosa interessante da menzionare quando vuoi caricare in modo pigro le immagini in particolare. Dovresti sempre tenere a mente la scatola, generata dall'elemento osservato! Se controlli l'esempio, noterai che il CSS per le immagini alle righe 41–47 contiene stili presumibilmente ridondanti, incl. min-height: 100px . Questo viene fatto per dare ai segnaposto dell'immagine ( <img> senza attributo src ) una certa dimensione verticale. Per che cosa?

  • Senza le dimensioni verticali, tutti i tag <img> genererebbero 0×0 box;
  • Poiché il tag <img> genera una sorta di riquadro inline-block per impostazione predefinita, tutti quei riquadri 0×0 sarebbero allineati fianco a fianco sulla stessa riga;
  • Ciò significa che il tuo IntersectionObserver registrerà tutte (o, a seconda della velocità con cui scorri, quasi tutte) le immagini contemporaneamente, probabilmente non proprio quello che vuoi ottenere.

Evidenziazione della sezione corrente

IntersectionObserver è molto più di un semplice caricamento lento, ovviamente. Ecco un altro esempio di sostituzione dell'evento di scroll con questa tecnologia. In questo abbiamo uno scenario abbastanza comune: sulla barra di navigazione fissa dovremmo evidenziare la sezione corrente in base alla posizione di scorrimento del documento.

Vedi la sezione corrente di Evidenziazione della penna in IntersectionObserver di Denys Mishunov (@mishunov) su CodePen.

Vedi la sezione corrente di Evidenziazione della penna in IntersectionObserver di Denys Mishunov (@mishunov) su CodePen.

Strutturalmente, è simile all'esempio per il caricamento lento delle immagini e ha la stessa struttura di base con le seguenti eccezioni:

  • Ora non vogliamo osservare le immagini, ma le sezioni della pagina;
  • Ovviamente, abbiamo anche una funzione diversa per elaborare le voci nella nostra callback ( intersectionHandler(entry) ). Ma questo non è interessante: tutto ciò che fa è attivare o disattivare la classe CSS.

Ciò che è interessante qui è l'oggetto config però:

 const config = { rootMargin: '-50px 0px -55% 0px' };

Perché non il valore predefinito di 0px per rootMargin , chiedi? Bene, semplicemente perché l'evidenziazione della sezione corrente e il caricamento lento di un'immagine sono abbastanza diversi in ciò che cerchiamo di ottenere. Con il caricamento lento, vogliamo iniziare a caricare prima che l'immagine entri nella vista. Quindi, a tale scopo, abbiamo esteso il nostro "fotogramma di acquisizione" di 50 px in basso. Al contrario, quando vogliamo evidenziare la sezione corrente, dobbiamo essere sicuri che la sezione sia effettivamente visibile sullo schermo. E non solo: dobbiamo essere sicuri che l'utente stia, effettivamente, leggendo o leggendo esattamente questa sezione. Quindi, vogliamo che una sezione vada un po' più della metà della finestra dal basso prima di poterla dichiarare la sezione attiva. Inoltre, vogliamo tenere conto dell'altezza della barra di navigazione, quindi rimuoviamo l'altezza della barra dal "riquadro di acquisizione".

Cattura fotogramma per la sezione corrente
Vogliamo che l'Observer rilevi solo gli elementi che entrano nel "frame di acquisizione" tra 50px dall'alto e il 55% del viewport dal basso.

Inoltre, tieni presente che in caso di evidenziazione dell'elemento di navigazione corrente, non vogliamo interrompere l'osservazione. Qui dovremmo sempre mantenere IntersectionObserver in carica, quindi qui non troverai né disconnect()unobserve() .

Sommario

IntersectionObserver è una tecnologia molto semplice. Ha un supporto abbastanza buono nei browser moderni e se vuoi implementarlo per i browser che lo supportano ancora (o non lo supportano affatto, ovviamente, c'è un polyfill per quello. Ma tutto sommato, questa è una grande tecnologia che ci consente di fare ogni sorta di cose relative al rilevamento di elementi in una finestra, aiutandoci a ottenere un ottimo aumento delle prestazioni.

Perché IntersectionObserver è buono per te?

  • IntersectionObserver è un'API asincrona non bloccante!
  • IntersectionObserver sostituisce i nostri costosi listener sugli eventi di scroll o resize .
  • IntersectionObserver esegue tutti i calcoli costosi come getClientBoundingRect() per te in modo che tu non sia necessario.
  • IntersectionObserver segue il modello strutturale di altri osservatori là fuori, quindi, in teoria, dovrebbe essere facile da capire se hai familiarità con il funzionamento degli altri osservatori.

Cose da tenere a mente

Se confrontiamo le capacità di IntersectionObserver con il mondo di window.addEventListener('scroll') da dove tutto è venuto, sarà difficile vedere dei contro in questo Observer. Quindi, notiamo solo alcune cose da tenere a mente invece:

  • Sì, IntersectionObserver è un'API asincrona non bloccante. Questo è fantastico da sapere! Ma è ancora più importante capire che il codice in esecuzione nei callback non verrà eseguito in modo asincrono per impostazione predefinita anche se l'API stessa è asincrona. Quindi c'è ancora la possibilità di eliminare tutti i vantaggi che ottieni da IntersectionObserver se i calcoli della tua funzione di callback rendono il thread principale non rispondente. Ma questa è una storia diversa.
  • Se stai usando IntersectionObserver per caricare in modo lento le risorse (come le immagini, ad esempio), esegui .unobserve(asset) dopo che la risorsa è stata caricata.
  • IntersectionObserver può rilevare le intersezioni solo per gli elementi che appaiono nella struttura di formattazione del documento. Per chiarire: gli elementi osservabili dovrebbero generare una scatola e in qualche modo influenzare il layout. Ecco solo alcuni esempi per darti una migliore comprensione:

    • Elementi con display: none è escluso;
    • opacity: 0 o visibility:hidden crea il riquadro (anche se invisibile) in modo che vengano rilevati;
    • Elementi assolutamente posizionati con width:0px; height:0px width:0px; height:0px vanno bene. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.