Caricamento delle immagini più veloce con le anteprime delle immagini incorporate

Pubblicato: 2022-03-10
Riepilogo rapido ↬ La tecnica Embedded Image Preview (EIP) introdotta in questo articolo ci consente di caricare immagini di anteprima durante il caricamento lento utilizzando JPEG progressivi, richieste di intervalli Ajax e HTTP senza dover trasferire dati aggiuntivi.

L'anteprima dell'immagine di bassa qualità (LQIP) e la variante basata su SVG SQIP sono le due tecniche predominanti per il caricamento lento delle immagini. Ciò che entrambi hanno in comune è che prima generi un'immagine di anteprima di bassa qualità. Questo verrà visualizzato sfocato e successivamente sostituito dall'immagine originale. E se potessi presentare un'immagine di anteprima al visitatore del sito web senza dover caricare dati aggiuntivi?

I file JPEG, per i quali viene utilizzato principalmente il lazy loading, hanno la possibilità, secondo le specifiche, di memorizzare i dati in essi contenuti in modo tale da visualizzare prima il contenuto dell'immagine grossolana e poi quello dettagliato. Invece di creare l'immagine dall'alto verso il basso durante il caricamento (modalità linea di base), è possibile visualizzare molto rapidamente un'immagine sfocata, che diventa gradualmente sempre più nitida (modalità progressiva).

Rappresentazione della struttura temporale di un JPEG in modalità baseline
Modalità base (Anteprima grande)
Rappresentazione della struttura temporale di un JPEG in modo progressivo
Modalità progressiva (Anteprima grande)

Oltre alla migliore esperienza utente fornita dall'aspetto che viene visualizzato più rapidamente, i JPEG progressivi sono generalmente anche più piccoli delle loro controparti con codifica di base. Per file di dimensioni superiori a 10 kB, c'è una probabilità del 94% di un'immagine più piccola quando si utilizza la modalità progressiva secondo Stoyan Stefanov del team di sviluppo di Yahoo.

Se il tuo sito web è composto da molti JPEG, noterai che anche i JPEG progressivi vengono caricati uno dopo l'altro. Questo perché i browser moderni consentono solo sei connessioni simultanee a un dominio. I JPEG progressivi da soli non sono quindi la soluzione per dare all'utente l'impressione più veloce possibile della pagina. Nel peggiore dei casi, il browser caricherà un'immagine completamente prima che inizi a caricare quella successiva.

L'idea qui presentata è ora di caricare dal server solo tanti byte di un JPEG progressivo da poter ottenere rapidamente un'idea del contenuto dell'immagine. Successivamente, in un momento da noi definito (es. quando tutte le immagini di anteprima nella vista corrente sono state caricate), il resto dell'immagine dovrebbe essere caricato senza richiedere nuovamente la parte già richiesta per l'anteprima.

Mostra il modo in cui la tecnica EIP (Embedded image preview) carica i dati dell'immagine in due richieste.
Caricamento di un JPEG progressivo con due richieste (Anteprima grande)

Sfortunatamente, non puoi dire a un tag img in un attributo quanta parte dell'immagine dovrebbe essere caricata a che ora. Con Ajax, tuttavia, ciò è possibile, a condizione che il server che fornisce l'immagine supporti le richieste di intervallo HTTP.

Utilizzando le richieste di intervallo HTTP, un client può informare il server in un'intestazione di richiesta HTTP quali byte del file richiesto devono essere contenuti nella risposta HTTP. Questa funzione, supportata da ciascuno dei server più grandi (Apache, IIS, nginx), viene utilizzata principalmente per la riproduzione video. Se un utente salta alla fine di un video, non sarebbe molto efficiente caricare il video completo prima che l'utente possa finalmente vedere la parte desiderata. Pertanto, solo i dati video nell'intervallo di tempo richiesto dall'utente vengono richiesti dal server, in modo che l'utente possa guardare il video il più velocemente possibile.

Ora affrontiamo le seguenti tre sfide:

  1. Creazione del JPEG progressivo
  2. Determina l'offset del byte fino al quale la prima richiesta di intervallo HTTP deve caricare l'immagine di anteprima
  3. Creazione del codice JavaScript frontend
Altro dopo il salto! Continua a leggere sotto ↓

1. Creazione del JPEG progressivo

Un JPEG progressivo è costituito da diversi cosiddetti segmenti di scansione, ognuno dei quali contiene una parte dell'immagine finale. La prima scansione mostra l'immagine solo in modo molto approssimativo, mentre quelle successive nel file aggiungono informazioni sempre più dettagliate ai dati già caricati e infine formano l'aspetto finale.

L'aspetto esatto delle singole scansioni è determinato dal programma che genera i JPEG. Nei programmi da riga di comando come cjpeg dal progetto mozjpeg, puoi persino definire quali dati contengono queste scansioni. Tuttavia, ciò richiede una conoscenza più approfondita, che andrebbe oltre lo scopo di questo articolo. Per questo, vorrei fare riferimento al mio articolo "Finally Understanding JPG", che insegna le basi della compressione JPEG. I parametri esatti che devono essere passati al programma in uno script di scansione sono spiegati nel wizard.txt del progetto mozjpeg. A mio avviso, i parametri dello script di scansione (sette scansioni) utilizzati da mozjpeg di default sono un buon compromesso tra struttura progressiva veloce e dimensione del file e possono quindi essere adottati.

Per trasformare il nostro JPEG iniziale in un JPEG progressivo, utilizziamo jpegtran dal progetto mozjpeg. Questo è uno strumento per apportare modifiche senza perdita di dati a un JPEG esistente. Le build precompilate per Windows e Linux sono disponibili qui: https://mozjpeg.codelove.de/binaries.html. Se preferisci giocare sul sicuro per motivi di sicurezza, è meglio costruirli da solo.

Dalla riga di comando ora creiamo il nostro JPEG progressivo:

 $ jpegtran input.jpg > progressive.jpg

Il fatto di voler costruire un JPEG progressivo è presupposto da jpegtran e non ha bisogno di essere specificato esplicitamente. I dati dell'immagine non verranno modificati in alcun modo. Viene modificata solo la disposizione dei dati dell'immagine all'interno del file.

I metadati irrilevanti per l'aspetto dell'immagine (come i dati Exif, IPTC o XMP), dovrebbero idealmente essere rimossi dal JPEG poiché i segmenti corrispondenti possono essere letti dai decodificatori di metadati solo se precedono il contenuto dell'immagine. Poiché per questo motivo non possiamo spostarli dietro i dati dell'immagine nel file, verrebbero già consegnati con l'immagine di anteprima e ingrandirebbero di conseguenza la prima richiesta. Con il programma a riga di comando exiftool puoi rimuovere facilmente questi metadati:

 $ exiftool -all= progressive.jpg

Se non desideri utilizzare uno strumento da riga di comando, puoi anche utilizzare il servizio di compressione online compress-or-die.com per generare un JPEG progressivo senza metadati.

2. Determinare l'offset di byte fino al quale la prima richiesta di intervallo HTTP deve caricare l'immagine di anteprima

Un file JPEG è suddiviso in diversi segmenti, ciascuno contenente componenti diversi (dati immagine, metadati come IPTC, Exif e XMP, profili colore incorporati, tabelle di quantizzazione, ecc.). Ciascuno di questi segmenti inizia con un marker introdotto da un byte FF esadecimale. Questo è seguito da un byte che indica il tipo di segmento. Ad esempio, D8 completa il marker fino al marker SOI FF D8 (Start Of Image), con il quale inizia ogni file JPEG.

Ogni inizio di una scansione è contrassegnato dal marker SOS (Start Of Scan, esadecimale FF DA ). Poiché i dati dietro il marcatore SOS sono codificati entropia (i JPEG usano la codifica Huffman), c'è un altro segmento con le tabelle di Huffman (DHT, esadecimale FF C4 ) richieste per la decodifica prima del segmento SOS. L'area di interesse per noi all'interno di un file JPEG progressivo, quindi, consiste nell'alternanza di tabelle di Huffman/segmenti di dati di scansione. Pertanto, se vogliamo visualizzare la prima scansione molto approssimativa di un'immagine, dobbiamo richiedere tutti i byte fino alla seconda occorrenza di un segmento DHT (esadecimale FF C4 ) dal server.

Mostra i marcatori SOS in un file JPEG
Struttura di un file JPEG (Anteprima grande)

In PHP, possiamo usare il seguente codice per leggere il numero di byte richiesti per tutte le scansioni in un array:

 <?php $img = "progressive.jpg"; $jpgdata = file_get_contents($img); $positions = []; $offset = 0; while ($pos = strpos($jpgdata, "\xFF\xC4", $offset)) { $positions[] = $pos+2; $offset = $pos+2; }

Dobbiamo aggiungere il valore di due alla posizione trovata perché il browser esegue il rendering dell'ultima riga dell'immagine di anteprima solo quando incontra un nuovo marker (che consiste di due byte come appena accennato).

Poiché siamo interessati alla prima immagine di anteprima in questo esempio, troviamo la posizione corretta in $positions[1] fino a cui dobbiamo richiedere il file tramite HTTP Range Request. Per richiedere un'immagine con una risoluzione migliore, potremmo utilizzare una posizione successiva nell'array, ad esempio $positions[3] .

3. Creazione del codice JavaScript frontend

Per prima cosa definiamo un tag img , a cui assegniamo la posizione del byte appena valutato:

 <img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">

Come spesso accade con le librerie di caricamento lento, non definiamo direttamente l'attributo src in modo che il browser non inizi immediatamente a richiedere l'immagine dal server durante l'analisi del codice HTML.

Con il seguente codice JavaScript ora carichiamo l'immagine di anteprima:

 var $img = document.querySelector("img[data-src]"); var URL = window.URL || window.webkitURL; var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ $img.src_part = this.response; $img.src = URL.createObjectURL(this.response); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes=0-" + $img.getAttribute('data-bytes')); xhr.responseType = 'blob'; xhr.send();

Questo codice crea una richiesta Ajax che dice al server in un'intestazione di intervallo HTTP di restituire il file dall'inizio alla posizione specificata in data-bytes ... e non di più. Se il server comprende le richieste di intervallo HTTP, restituisce i dati dell'immagine binaria in una risposta HTTP-206 (HTTP 206 = contenuto parziale) sotto forma di blob, da cui possiamo generare un URL interno al browser utilizzando createObjectURL . Usiamo questo URL come src per il nostro tag img . Così abbiamo caricato la nostra immagine di anteprima.

Archiviamo il BLOB in aggiunta nell'oggetto DOM nella proprietà src_part , perché avremo bisogno di questi dati immediatamente.

Nella scheda rete della console dello sviluppatore puoi verificare che non abbiamo caricato l'immagine completa, ma solo una piccola parte. Inoltre, il caricamento dell'URL del BLOB dovrebbe essere visualizzato con una dimensione di 0 byte.

Mostra la console di rete e le dimensioni delle richieste HTTP
Console di rete durante il caricamento dell'immagine di anteprima (anteprima grande)

Poiché abbiamo già caricato l'intestazione JPEG del file originale, l'immagine di anteprima ha le dimensioni corrette. Pertanto, a seconda dell'applicazione, possiamo omettere l'altezza e la larghezza del tag img .

Alternativa: caricamento dell'immagine di anteprima in linea

Per motivi di prestazioni, è anche possibile trasferire i dati dell'immagine di anteprima come URI di dati direttamente nel codice sorgente HTML. Questo ci risparmia il sovraccarico del trasferimento delle intestazioni HTTP, ma la codifica base64 rende i dati dell'immagine un terzo più grandi. Questo è relativizzato se fornisci il codice HTML con una codifica del contenuto come gzip o brotli , ma dovresti comunque usare URI di dati per piccole immagini di anteprima.

Molto più importante è il fatto che le immagini di anteprima sono immediatamente disponibili e non vi è alcun ritardo evidente per l'utente durante la creazione della pagina.

Prima di tutto, dobbiamo creare l'URI dei dati, che poi usiamo nel tag img come src . Per questo, creiamo l'URI di dati tramite PHP, per cui questo codice si basa sul codice appena creato, che determina gli offset di byte dei marker SOS:

 <?php … $fp = fopen($img, 'r'); $data_uri = 'data:image/jpeg;base64,'. base64_encode(fread($fp, $positions[1])); fclose($fp);

L'URI di dati creato viene ora inserito direttamente nel tag `img` come src :

 <img src="<?= $data_uri ?>" data-src="progressive.jpg" alt="">

Naturalmente, anche il codice JavaScript deve essere adattato:

 <script> var $img = document.querySelector("img[data-src]"); var binary = atob($img.src.slice(23)); var n = binary.length; var view = new Uint8Array(n); while(n--) { view[n] = binary.charCodeAt(n); } $img.src_part = new Blob([view], { type: 'image/jpeg' }); $img.setAttribute('data-bytes', $img.src_part.size - 1); </script>

Invece di richiedere i dati tramite richiesta Ajax, dove riceveremmo immediatamente un blob, in questo caso dobbiamo creare noi stessi il blob dall'URI dei dati. Per fare ciò, liberiamo il data-URI dalla parte che non contiene i dati dell'immagine: data:image/jpeg;base64 . Decodifichiamo i restanti dati codificati in base64 con il comando atob . Per creare un blob dai dati della stringa ora binaria, dobbiamo trasferire i dati in un array Uint8, che garantisce che i dati non vengano trattati come un testo codificato UTF-8. Da questa matrice, ora possiamo creare un BLOB binario con i dati dell'immagine dell'immagine di anteprima.

Per non dover adattare il codice seguente per questa versione inline, aggiungiamo l'attributo data-bytes sul tag img , che nell'esempio precedente contiene l'offset di byte da cui deve essere caricata la seconda parte dell'immagine .

Nella scheda Rete della console per sviluppatori, puoi anche verificare qui che il caricamento dell'immagine di anteprima non generi una richiesta aggiuntiva, mentre la dimensione del file della pagina HTML è aumentata.

Mostra la console di rete e le dimensioni delle richieste HTTP
Console di rete durante il caricamento dell'immagine di anteprima come URI di dati (anteprima grande)

Caricamento dell'immagine finale

In un secondo passaggio carichiamo il resto del file immagine dopo due secondi come esempio:

 setTimeout(function(){ var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ var blob = new Blob([$img.src_part, this.response], { type: 'image/jpeg'} ); $img.src = URL.createObjectURL(blob); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes="+ (parseInt($img.getAttribute('data-bytes'), 10)+1) +'-'); xhr.responseType = 'blob'; xhr.send(); }, 2000);

Nell'intestazione Range questa volta specifichiamo che vogliamo richiedere l'immagine dalla posizione finale dell'immagine di anteprima alla fine del file. La risposta alla prima richiesta è memorizzata nella proprietà src_part dell'oggetto DOM. Usiamo le risposte di entrambe le richieste per creare un nuovo BLOB per new Blob() , che contiene i dati dell'intera immagine. L'URL del BLOB generato da questo viene utilizzato di nuovo come src dell'oggetto DOM. Ora l'immagine è completamente caricata.

Inoltre ora possiamo controllare di nuovo le dimensioni caricate nella scheda di rete della console per sviluppatori..

Mostra la console di rete e le dimensioni delle richieste HTTP
Console di rete durante il caricamento dell'intera immagine (31,7 kB) (Anteprima grande)

Prototipo

Al seguente URL ho fornito un prototipo in cui è possibile sperimentare diversi parametri: https://embedded-image-preview.cerdmann.com/prototype/

Il repository GitHub per il prototipo può essere trovato qui: https://github.com/McSodbrenner/embedded-image-preview

Considerazioni alla fine

Utilizzando la tecnologia Embedded Image Preview (EIP) presentata qui, possiamo caricare immagini di anteprima qualitativamente diverse da JPEG progressive con l'aiuto di Ajax e HTTP Range Requests. I dati di queste immagini di anteprima non vengono scartati ma vengono invece riutilizzati per visualizzare l'intera immagine.

Inoltre, non è necessario creare immagini di anteprima. Sul lato server, è necessario determinare e salvare solo l'offset di byte a cui termina l'immagine di anteprima. In un sistema CMS, dovrebbe essere possibile salvare questo numero come attributo su un'immagine e tenerne conto durante l'output nel tag img . Sarebbe anche ipotizzabile un flusso di lavoro, che integri il nome del file dell'immagine con l'offset, ad esempio progressive-8343.jpg , per non dover salvare l'offset separatamente dal file dell'immagine. Questo offset potrebbe essere estratto dal codice JavaScript.

Poiché i dati dell'immagine di anteprima vengono riutilizzati, questa tecnica potrebbe essere un'alternativa migliore al solito approccio di caricare un'immagine di anteprima e quindi un WebP (e fornire un fallback JPEG per i browser che non supportano WebP). L'immagine di anteprima spesso distrugge i vantaggi di archiviazione del WebP, che non supporta la modalità progressiva.

Attualmente, le immagini di anteprima in LQIP normale sono di qualità inferiore, poiché si presume che il caricamento dei dati di anteprima richieda una larghezza di banda aggiuntiva. Come ha già chiarito Robin Osborne in un post sul blog nel 2018, non ha molto senso mostrare segnaposto che non danno un'idea dell'immagine finale. Utilizzando la tecnica qui suggerita, possiamo mostrare un po' più dell'immagine finale come immagine di anteprima senza esitazione presentando all'utente una scansione successiva del JPEG progressivo.

In caso di connessione di rete debole dell'utente, potrebbe avere senso, a seconda dell'applicazione, non caricare l'intero JPEG, ma ad esempio omettere le ultime due scansioni. Questo produce un JPEG molto più piccolo con una qualità solo leggermente ridotta. L'utente ci ringrazierà e non dobbiamo memorizzare un file aggiuntivo sul server.

Ora ti auguro buon divertimento a provare il prototipo e attendo con ansia i tuoi commenti.