La guida completa al Lazy loading delle immagini

LA guida completa al Lazy Loading delle immagini

Le immagini costituiscono l’elemento fondamentale di ogni pagina web. Il loro peso, tuttavia, può incidere notevolmente sulle prestazioni di un sito, oltre che sui consumi di banda. Impariamo dunque ad utilizzare la tecnica del Lazy Loading, e scopriamo come può esserci utile per migliorare le performance e la User Experience.

1. Che cos’è il Lazy Loading?

Il Lazy Loading è una tecnica che permette il caricamento deferenziato delle immagini in una pagina web. Nello specifico, consente di caricare un asset grafico, e quindi effettuarne la singola chiamata HTTP, solamente quando questo è visibile sul browser; quando, cioè, l’utente effettua lo scrolling e l’immagine entra nel viewport dello schermo.

Questa metodologia nasce fondamentalmente dalla necessità di mantenere sotto controllo la considerevole mole di chiamate e caricamenti che oggigiorno una pagina mediamente sostiene. Basti pensare infatti che, secondo le ultime analisi dell’HTTP Archive, il peso medio è di 1544.7 kB su desktop e 1270.3 kB su mobile. Di questi, le immagini costituiscono rispettivamente 657.2 kB e 498.3 kB; parliamo dunque del 42.5% e 39.2% dei bytes complessivi: una misura tale da dover essere mantenuta sempre sotto stretto controllo e, per quanto possibile, domata al fine di garantire delle performance accettabili.

Va inoltre considerato il fatto che molto spesso gran parte dei siti web fanno riferimento ad immagini non ottimizzate, caricate magari in formato jpg originale senza alcun tipo di processo di ottimizzazione (vedi JPEGmini, reSmushit.it o EWWW )

Il Lazy Loading è pertanto diventato uno strumento quasi indispensabile per garantire un’esperienza utente di livello e mantenere il web una piattaforma veloce e usufruibile per tutti.

2. I vantaggi del Lazy Loading

2.1 Migliori tempi di caricamento

Caricare un’immagine solo quando questa entra nel viewport dello schermo consente di risparmiare parecchie chiamate HTTP, e quindi bytes, al momento del loading iniziale. Immaginiamo un sito di E-Commerce: se venissero caricati tutti insieme gli asset grafici dei prodotti, ci ritroveremmo con un peso in bytes considerevole, una mole di chiamate HTTP non indifferente e sicuramente la nostra pagina costituirebbe un’esperienza controproducente sia in termini di performance, SEO che di usabilità.

2.2 Benefici sui costi

Molte delle CDN per il delivery dei contenuti multimediali, così come gran parte delle soluzioni di hosting (condiviso e non), calcolano i loro costi per byte servito. Questo significa che senza il Lazy Loading delle immagini le nostre pagine web caricano sempre e comunque tutti gli asset grafici, anche laddove l’utente non effettui mai lo scrolling oppure atterri ed esca immediatamente (high bounce-rate).

Se ne deduce pertanto come questa tecnica diventi essenziale per limitare i bytes trasferiti alle sole immagini realmente visibili dall’utente, andando quindi ad incidere (e alla lunga anche considerevolmente) sui costi complessivi.

Sì, ma quanto posso risparmiare?
Beh, per scoprirlo dobbiamo innanzitutto capire quali sono le immagini off-screen, ovvero quelle candidate al Lazy Loading, e per farlo possiamo usare un tool presente nei Developer Tools di Google Chrome: il  Google Lighthouse audit tool con la sua  sezione dedicata alle immagini offscreen.

3. Tecniche di implementazione del Lazy Loading per le immagini

Ci sono fondamentalmente due modi per caricare le immagini in una pagina web: tramite il tag HTML <img>, oppure utilizzando la direttiva background-image via CSS.

Lazy Loading usando il tag <img>:

Per caricare un’immagine tramite HTML si utilizza la seguente forma base:

<img src="path/to/some/image.png" />

Ora, ogni qualvolta il browser incontra un tag <img> tenta immediatamente il caricamento dell’asset specificato nell’attributo src. Dobbiamo quindi innanzitutto prevenire questo comportamento, e per farlo basta semplicemente spostare il percorso dell’immagine in un altro attributo che non sia src, ma ad esempio data-src:

<img data-src="path/to/some/image.png" />

Così facendo, il browser non effettua alcun caricamento poiché riconosce l’attributo src come vuoto. Spetta a noi, a questo punto, eseguire l’azione, e lo faremo solamente quando l’asset in questione entrerà nel viewport della finestra. Come sempre, Javascript ci viene in aiuto con ben 2 metodi:

Metodo 1: Lazy Loading tramite eventi Javascript

Questa è la tecnica base; la prima ad esser stata utilizzata, poiché sfrutta gli event listener scroll, resize e orientation change del browser. I trigger di tutti e tre questi eventi avvengono ogni volta che c’è un cambiamento nel viewport, pertanto è necessario intervenire per ricalcolare il numero di immagini visibili sullo schermo ed eventualmente rendere visibili quelle che prima non lo erano.

Per fare questo, è necessario andare a calcolare l’offset di ogni singola immagine dal top del viewport, e in più ricavare l’altezza della finestra. Grazie a questi parametri siamo in grado di capire se una determinata immagine sia entrata o no nell’area visibile. Se questo avviene, sempre tramite javascript prenderemo il valore dell’attributo data-src e lo inseriremo in quello di src, dando quindi il via al caricamento dell’asset.

È necessario inoltre aggiungere una classe .lazy a tutte quelle immagini che devono essere trattate con questa tecnica. Una volta rese visibili, ci occuperemo poi di rimuoverla, poiché ormai il loro caricamento è stato effettuato, si trovano memorizzate nella cache del browser e pertanto non abbiamo più bisogno di trattarle come elementi di lazy load.

Inoltre, è bene tenere sempre a mente che quando effettuiamo lo scroll di una pagina, l’evento relativo scroll si attiva di continuo: praticamente per ogni pixel avanzato; aggiungiamo pertanto anche un piccolo timeout al nostro script, in modo da ridurre l’esecuzione della funzione di Lazy Loading affinché non blocchi altre attività all’interno dello stesso thread.

Vediamo quanto spiegato con un esempio:

Notate come le prime due immagini vengano caricate direttamente: questo perché si trovano nella parte alta della finestra e quindi non c’è bisogno di posticiparne il caricamento, altrimenti avremmo una User Experience alquanto anomala e controproducente.

Metodo 2: Lazy Loading tramite le Intersection Observer API

Le Intersection Observer API sono un’introduzione relativamente recente ma costituiscono un ottimo strumento per determinare quando un elemento entra nel viewport dello schermo. Il loro utilizzo, comparato con il metodo precedente, garantisce innanzitutto una maggiore semplicità d’implementazione, oltre che migliori performance in termini di image load.

Utilizzando le Intersection Observer API, infatti, non abbiamo più bisogno di impostare un timer per ovviare ai problemi di performance dovuti all’utilizzo dei trigger events di javascript, così come non lo è più calcolare gli offset per determinare se l’elemento in questione si trova o meno nell’area visibile dello schermo.

Di seguito è riportato un esempio di utilizzo delle Intersection Observer API. Effettuiamo un bind dell’observer su tutte le immagini da caricare in Lazy Load dopodiché, una volta che abbiamo determinato quando un elemento è entrato nel viewport, utilizzando la proprietà isIntersecting, spostiamo il valore dell’attributo data-src all’interno di src. Una volta fatto questo possiamo quindi rimuovere la classe .lazy e fare l’unbind dell’observer.

Ricordate che le Intersection Observer API non sono ancora completamente supportate da tutti i browser. È necessario pertanto eseguire il fallback del metodo listener dell’evento nei browser che non le riconoscono, così come potete vedere nell’esempio riportato sopra.

Lazy Loading tramite CSS Background-Image

Il secondo modo per caricare un’immagine all’interno di una pagina web è quello di utilizzare la proprietà background-image via CSS. A differenza però del metodo <img>, dove il browser carica immediatamente l’asset non appena la risorsa specificata nell’attributo src è disponibile, questo metodo è un po’ più articolato.

Innanzitutto il browser deve attendere il completamento del DOM (Document Object Model) della pagina e tramite la composizione del CSSOM (CSS Object Model) determinare se una particolare regola può essere applicata o no ad uno specifico nodo.

Questo meccanismo sta in realtà alla base del funzionamento della tecnica del Lazy Loading; il trucco sta infatti nel forzare il browser a non applicare inizialmente la proprietà background-image finché l’elemento non sia entrato nel viewport.

Vediamo nel dettaglio come funziona:

Qui una cosa importante da notare è che il codice Javascript utilizzato è sempre lo stesso: sfruttiamo le Intersection Observer API con un fallback per il listener degli eventi. Il trucco vero, infatti, sta nel CSS.

L’elemento con ID bg-image ha un’immagine di sfondo specificata tramite CSS. Tuttavia, quando la classe .lazy viene aggiunta a questo elemento, nel CSS sostituiamo la proprietà background-image e la impostiamo sul valore none. Dal momento che la combinazione di #bg-image con la classe .lazy ha una priorità maggiore rispetto a #bg-image, il browser applica per prima la proprietà background-image: none; all’elemento in questione.

Quando effettuiamo lo scroll della pagina verso il basso, l’Intersection Observer (o gli event listener) rileva ogni immagine che entra all’interno del viewport. Non appena questo avviene, rimuoviamo la classe .lazy.  Con questa semplice operazione il browser applicherà quindi la regola CSS per #bg-image, caricando di conseguenza l’immagine specificata nella proprietà background-image.

4. Una migliore User Experience

Il lazy loading offre un grande vantaggio in termini di prestazioni. Per un’azienda di E-commerce che carica decine e decine di immagini di prodotti su una pagina, questa tecnica può costituire un miglioramento significativo per quanto concerne il tempo di caricamento iniziale, riducendo al contempo il consumo della larghezza di banda.

Tuttavia molto spesso non viene implementata poiché si ritiene offra un’esperienza utente mediocre: il placeholder iniziale (prima che avvenga il rendering dell’immagine) è brutto, i tempi di caricamento sono lenti, etc.

Nella prossima sezione cercheremo pertanto di risolvere alcune delle problematiche più comuni.

4.1 Usiamo i placeholder corretti

Un placeholder è ciò che appare nello spazio riservato all’immagine prima che questa stessa sia caricata. Negli esempi precedenti abbiamo utilizzato un colore grigio come placeholder per le nostre immagini. Non è un gran ché, in effetti; vediamo quindi come migliorare questo aspetto analizzando alcune metodologie di implementazione.

A) Colori predominanti come placeholder

Invece di usare un colore fisso, individuiamo il colore dominante dell’immagine originale e lo usiamo come placeholder. Questa tecnica è stata utilizzata per un po’ di tempo nei risultati di ricerca di immagini di Google e Pinterest.

Colori dominanti placeholder

Anche se all’inizio può sembrare complessa, in realtà basta scalare l’immagine originale ad 1px x 1px, e successivamente ri-scalarla alle dimensioni del placeholder. Si tratta di un modo poco ortodosso, ma fa sicuramente funzionante.

Altro modo è quello di utilizzare ImageKit:

<!-- Original image at 400x300 -->
<img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" alt="original image" />

<!-- Dominant colour image with same dimensions -->
<img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-1,h-1:w-400,h-300" alt="dominant color placeholder" />

B) Usare versioni in bassa qualità come placeholder ( LQIP: Low quality image placeholder)

Questa è una tecnica che è stata utilizzata ampiamente sia da Facebook che da Medium negli anni passati. Si tratta fondamentalmente di utilizzare la stessa immagine ma in una versione a ridotta qualità e sfocata. L’effetto finale induce l’utente a pensare che l’assset si stia caricando e dà al tempo stesso una sorta di anticipazione di come sarà l’immagine definitiva.

È sicuramente una delle tecniche più interessanti e gradevoli anche dal punto di vista della User Experience. Ecco come gestire le LQIP tramite ImageKit

<!-- Original image at 400x300 -->
<img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" alt="original image" />

<!-- Low quality image placeholder with same dimensions -->
<img src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300,bl-30,q-50" alt="dominant color placeholder" />

4.2 Utilizzare il buffer per il caricamento delle immagini

Quando abbiamo discusso i vari metodi per individuare il momento esatto in cui un’immagine entra nel viewport, abbiamo visto come si sia reso necessario calcolare l’offset generato fra il bordo superiore dell’elemento e quello superiore della finestra. Quando questi due valori combaciano, abbiamo la certezza che l’asset è visibile.

Il problema

Spesso gli utenti effettuano lo scroll della pagina molto velocemente, e le immagini, una volta entrate nel viewport, potrebbero metterci un po’ prima di essere renderizzate a schermo. Questo comportamento, unito al fatto che a volte, a causa di lag di rete o lentezza nel caricamento, il trigger dell’evento di image load avviene con ritardo, l’effetto finale è quello di vedere dapprima entrare nel viewport i placeholder (magari con un background-color uniforme) e dopo un po’ vedere invece apparire le immagini.

Certamente si possono utilizzare le Intersection Observer API o la tecnica LQIP per migliorare e velocizzare il tutto, ma c’è anche un altro metodo che può tornare utile in questi casi:

La soluzione

Invece di caricare le immagini quando si trovano esattamente al bordo del viewport, basta caricarle quando sono, ad esempio, a 500px da esso. Questo significa usufruire di una specie di buffering iniziale in grado di gestire gli asset grafici molto prima, facendo in modo che siano già visualizzati all’interno della finestra.

Con le Intersection Observer API possiamo utilizzare il parametro root assieme al rootMargin per incrementare l’area di intersezione (bounding box) dell’immagine. Con gli event listener, invece di controllare quando il valore dell’offset è a zero, utilizziamo invece un valore positivo per aggiungere del margine di controllo.

4.3 Evitare lo slittamento del layout (Content Shifting)

Il problema

Quando il browser tenta di caricare un’immagine che non esiste, o per qualche motivo non è raggiungibile, renderizza comunque a schermo il suo container ma con dimensioni 1px x 1px. Questo provoca un fastidioso effetto di slittamento degli elementi adiacenti all’interno del layout chiamato Content Shifting. Per saperne di più, consultate questo articolo di Smashing Magazine.

La soluzione

Questo comportamento può essere evitato semplicemente specificando le dimensioni dell’immagine direttamente al suo container. In questo modo il browser, a prescindere dall’esito del caricamento, andrà ad allocare le giuste dimensioni senza “rompere” il layout.

4.4 Capire a quali immagini applicare il  Lazy Loading

Si tratta di un comune errore che gli sviluppatori spesso commettono: applicare la tecnica del Lazy Loading a tutte le immagini presenti nella pagina. Ciò potrebbe ridurre il caricamento iniziale complessivo, ma comporterebbe una User Experience negativa in quanto molte immagini, soprattutto nella parte superiore del layout, non verrebbero visualizzate fino all’esecuzione di Javascript.

In generale quindi, possiamo seguire alcuni principi per capire quali asset devono essere caricati tramite la tecnica del Lazy Loading:

  • Le immagini presenti nel viewport iniziale, o all’inizio della pagina (banner, loghi, icone di menù etc), devono sempre essere caricate direttamente. Il motivo è essenzialmente uno: l’utente deve vederle non appena la pagina viene caricata. Inoltre, tenete a mente che i dispositivi mobile e desktop hanno spesso schermi di dimensioni diverse, pertanto un numero diverso di immagini che sono inizialmente visibili sullo schermo. È necessario allora prendere in considerazione il tipo di dispositivo per decidere quali risorse caricare in anticipo e quali, invece, caricare tramite la tecnica del Lazy Loading..
  • Non tutte le immagini che si trovano fuori dal viewport devono essere caricate tramite il Lazy Loading. È bene piuttosto inserire un offset di caricamento, così come descritto precedentemente in questa guida. Ogni immagine posizionata, ad esempio, 500px o a uno scroll intero dalla parte inferiore del viewport, può quindi essere tranquillamente caricata in maniera diretta.
  • Se la pagina non è molto lunga, perché magari ci sono solo un paio di scroll da fare oltre il margine inferiore del viewport, oppure ci sono solo una decina di immagini in totale sulla pagina, allora non è affatto necessario utilizzare la tecnica del Lazy Loading anzi, potrebbe perfino essere controproducente. Meglio caricarle tutte insieme e garantire così una User Experience fluida e regolare.

5. La dipendenza da Javascript del Lazy Loading

Tutto il discorso sul Lazy Loading prescinde dal supporto e dall’esecuzione di Javascript all’interno del browser. Sebbene la maggior parte degli utenti non ha problemi nell’eseguire script web, poiché la loro presenza è quanto mai essenziale al giorno d’oggi, è comunque buona pratica prevedere un’eccezione e quindi saper gestire anche quelle situazioni in cui il browser utente non permette l’esecuzione di codice Javascript all’interno delle pagine.

In questi casi potreste mostrare loro un messaggio che indica perché le immagini non verranno caricate e che devono passare a un browser più moderno o abilitare Javascript per poter usufruire di una navigazione ottimale. Oppure potete usare il tag noscript, ma badate bene: ci sono alcuni trucchi specifici per usare questa alternativa nel migliore dei modi.

Questo thread su Stack Overflow è  un ottimo punto di partenza ed è una lettura consigliata per tutti coloro che desiderano gestire le situazioni in cui Javascript è disabilitato.

6. Come capire se il lazy loading sta funzionando sul vostro sito?

Una volta implementato il Lazy Loading, è necessario verificarne il funzionamento corretto. Il modo più semplice è quello di utilizzare gli strumenti per sviluppatori nel browser Chrome o l’equivalente pannello in Firefox.

Andando sulla scheda Network > Immagini potete monitorare gli asset grafici che vengono caricati progressivamente sulla pagina web. A questo punto, se tutto funziona a dovere, vedrete caricate solamente le immagini presenti nella parte superiore della pagina. Iniziando a scrollare verso il basso, incominceranno a caricarsi tutte le altre.

Un altro metodo è quello di utilizzare il Google Lighthouse audit tool, monitorando la sezione delle immagini offscreen.

7. Le migliori librerie disponibili per il Lazy Loading

Ecco un elenco delle migliori risorse attualmente a disposizione per implementare la tecnica del Lazy Loading.

yall.js (Yet Another Lazy Loader) – Utilizza le Intersection Observer APIs e mette a disposizione un fallback agli event listener di Javascript. Funziona con la maggior parte degli elementi HTML ma non supporta la tecnica del background-image via CSS. Funziona anche su IE11+.

lazysizes – Una delle librerie più popolari. Supporta le immagini responsive e gli attributi srcset. Garantisce alte performance anche senza utilizzare le Intersection Observer APIs.

jQuery Lazy – Una libreria molto semplice basata su jQuery.

WeltPixel – Estensione per Magento 2

Magento Lazy Image Loader – Estensione per Magento 1.x

Shopify Lazy Image Plugin – Estensione per Shopify a pagamento.

WordPress A3 Lazy Load – Plugin WordPress per il Lazy Loading.