Ora mi vedi: come differire, caricare pigro e agire con IntersectionObserver
Pubblicato: 2022-03-10C'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.
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.
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.
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.
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.
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.
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
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, laroot
è il viewport del tuo browser ma può essere davvero qualsiasi elemento nel tuo DOM (quindi impostiroot
su qualcosa comedocument.getElementById('your-element')
). Tieni presente però che gli elementi che vuoi osservare devono "vivere" nell'albero DOM diroot
in questo caso.
-
rootMargin
: Definisce il margine attorno all'elementoroot
che estende o rimpicciolisce il "frame di acquisizione" quando le dimensioni dellaroot
non forniscono sufficiente flessibilità. Le opzioni per i valori di questa configurazione sono simili a quelle dimargin
in CSS, comerootMargin: '50px 20px 10px 40px'
(in alto, in basso a destra, a sinistra). I valori possono essere abbreviati (comerootMargin: '50px'
) e possono essere espressi inpx
o%
. Per impostazione predefinita,rootMargin: '0px'
.
-
threshold
: Non è sempre necessario reagire istantaneamente quando un elemento osservato interseca un bordo del "fotogramma di acquisizione" (definito come una combinazione diroot
erootMargin
).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 dellathreshold
(so che a volte potrebbe creare confusione), ecco alcuni esempi:-
threshold: 0
: Il valore predefinitoIntersectionObserver
dovrebbe reagire quando il primo o l'ultimo pixel di un elemento osservato interseca uno dei bordi del "fotogramma di acquisizione". Tieni presente cheIntersectionObserver
è 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
.
-
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 diroot
. -
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.
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.
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 eratrue
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.
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.
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.
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.
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.
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) { … } });
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); … }
preloadImage()
è una funzione molto semplice che non vale la pena menzionare qui. Basta leggere la fonte.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 conelement.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 riquadroinline-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.
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".
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()
né 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 discroll
oresize
. -
IntersectionObserver
esegue tutti i calcoli costosi comegetClientBoundingRect()
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 daIntersectionObserver
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
ovisibility: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 negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Elementi con
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.