Încărcare mai rapidă a imaginii cu previzualizările imaginilor încorporate

Publicat: 2022-03-10
Rezumat rapid ↬ Tehnica de previzualizare a imaginii încorporate (EIP) introdusă în acest articol ne permite să încărcăm imagini de previzualizare în timpul încărcării lene, utilizând solicitări JPEG progresive, Ajax și HTTP fără a fi nevoie să transferăm date suplimentare.

Low Quality Image Preview (LQIP) și varianta SQIP bazată pe SVG sunt cele două tehnici predominante pentru încărcarea leneșă a imaginii. Ceea ce ambele au în comun este că mai întâi generați o imagine de previzualizare de calitate scăzută. Aceasta va fi afișată neclar și ulterior înlocuită cu imaginea originală. Ce se întâmplă dacă ați putea prezenta o imagine de previzualizare vizitatorului site-ului web fără a fi nevoie să încărcați date suplimentare?

Fișierele JPEG, pentru care se folosește cel mai mult lazy loading, au posibilitatea, conform specificației, de a stoca datele conținute în ele în așa fel încât să fie afișat mai întâi conținutul grosier și apoi detaliat al imaginii. În loc să aibă imaginea construită de sus în jos în timpul încărcării (modul de bază), o imagine neclară poate fi afișată foarte rapid, care devine treptat din ce în ce mai clară (modul progresiv).

Reprezentarea structurii temporale a unui JPEG în modul de bază
Modul de bază (previzualizare mare)
Reprezentarea structurii temporale a unui JPEG în modul progresiv
Modul progresiv (previzualizare mare)

Pe lângă experiența de utilizare mai bună oferită de aspectul care este afișat mai rapid, JPEG-urile progresive sunt, de obicei, mai mici decât omologii lor codificați la linia de bază. Pentru fișierele mai mari de 10 kB, există o probabilitate de 94% ca o imagine mai mică atunci când se utilizează modul progresiv, conform lui Stoyan Stefanov de la echipa de dezvoltare Yahoo.

Dacă site-ul dvs. este format din mai multe JPEG, veți observa că chiar și JPEG-urile progresive se încarcă unul după altul. Acest lucru se datorează faptului că browserele moderne permit doar șase conexiuni simultane la un domeniu. Prin urmare, numai JPEG-urile progresive nu sunt soluția pentru a oferi utilizatorului cea mai rapidă impresie posibilă a paginii. În cel mai rău caz, browserul va încărca complet o imagine înainte de a începe să încarce următoarea.

Ideea prezentată aici este acum să încărcați doar atât de mulți octeți ai unui JPEG progresiv de pe server, încât să puteți obține rapid o impresie asupra conținutului imaginii. Mai târziu, la un moment definit de noi (de exemplu, când toate imaginile de previzualizare din fereastra curentă au fost încărcate), restul imaginii ar trebui să fie încărcate fără a solicita din nou partea deja solicitată pentru previzualizare.

Afișează modul în care tehnica EIP (previzualizare imagine încorporată) încarcă datele imaginii în două solicitări.
Încărcarea unui JPEG progresiv cu două solicitări (previzualizare mare)

Din păcate, nu puteți spune unei etichete img dintr-un atribut cât de mult din imagine ar trebui să fie încărcată la ce oră. Cu Ajax, totuși, acest lucru este posibil, cu condiția ca serverul care livrează imaginea să accepte solicitări HTTP Range.

Folosind cererile de interval HTTP, un client poate informa serverul într-un antet de solicitare HTTP care octeți ai fișierului solicitat trebuie să fie conținut în răspunsul HTTP. Această caracteristică, acceptată de fiecare dintre serverele mai mari (Apache, IIS, nginx), este utilizată în principal pentru redarea video. Dacă un utilizator sare la sfârșitul unui videoclip, nu ar fi foarte eficient să încărcați videoclipul complet înainte ca utilizatorul să poată vedea în sfârșit partea dorită. Prin urmare, doar datele video din jurul orei solicitate de utilizator sunt solicitate de server, astfel încât utilizatorul să poată viziona videoclipul cât mai repede posibil.

Acum ne confruntăm cu următoarele trei provocări:

  1. Crearea JPEG progresiv
  2. Determinați decalajul de octeți până la care prima solicitare de interval HTTP trebuie să încarce imaginea de previzualizare
  3. Crearea codului JavaScript Frontend
Mai multe după săritură! Continuați să citiți mai jos ↓

1. Crearea JPEG progresiv

Un JPEG progresiv constă din mai multe așa-numite segmente de scanare, fiecare dintre ele conține o parte din imaginea finală. Prima scanare arată imaginea doar foarte aproximativ, în timp ce cele care urmează mai târziu în fișier adaugă informații din ce în ce mai detaliate la datele deja încărcate și formează în final aspectul final.

Cum arată exact scanările individuale este determinat de programul care generează fișierele JPEG. În programele de linie de comandă, cum ar fi cjpeg din proiectul mozjpeg, puteți chiar defini ce date conțin aceste scanări. Cu toate acestea, acest lucru necesită cunoștințe mai aprofundate, care ar depăși scopul acestui articol. Pentru aceasta, aș dori să mă refer la articolul meu „Finally Understanding JPG”, care învață elementele de bază ale compresiei JPEG. Parametrii exacti care trebuie trecuți programului într-un script de scanare sunt explicați în wizard.txt al proiectului mozjpeg. În opinia mea, parametrii scriptului de scanare (șapte scanări) utilizați implicit de mozjpeg sunt un compromis bun între structura rapidă progresivă și dimensiunea fișierului și, prin urmare, pot fi adoptați.

Pentru a transforma JPEG-ul nostru inițial într-un JPEG progresiv, folosim jpegtran din proiectul mozjpeg. Acesta este un instrument pentru a face modificări fără pierderi la un JPEG existent. Build-urile precompilate pentru Windows și Linux sunt disponibile aici: https://mozjpeg.codelove.de/binaries.html. Dacă preferați să jucați în siguranță din motive de securitate, este mai bine să le construiți singur.

Din linia de comandă creăm acum JPEG-ul nostru progresiv:

 $ jpegtran input.jpg > progressive.jpg

Faptul că dorim să construim un JPEG progresiv este presupus de jpegtran și nu trebuie să fie specificat în mod explicit. Datele imaginii nu vor fi modificate în niciun fel. Se modifică doar aranjarea datelor imaginii în cadrul fișierului.

Metadatele irelevante pentru aspectul imaginii (cum ar fi datele Exif, IPTC sau XMP) ar trebui în mod ideal eliminate din JPEG, deoarece segmentele corespunzătoare pot fi citite de decodoarele de metadate numai dacă preced conținutul imaginii. Deoarece nu le putem muta în spatele datelor de imagine din fișier din acest motiv, ar fi deja livrate cu imaginea de previzualizare și ar mări prima solicitare în consecință. Cu programul de linie de comandă exiftool puteți elimina cu ușurință aceste metadate:

 $ exiftool -all= progressive.jpg

Dacă nu doriți să utilizați un instrument de linie de comandă, puteți utiliza și serviciul de compresie online compress-or-die.com pentru a genera un JPEG progresiv fără metadate.

2. Determinați decalajul de octeți până la care prima solicitare de interval HTTP trebuie să încarce imaginea de previzualizare

Un fișier JPEG este împărțit în diferite segmente, fiecare conținând diferite componente (date de imagine, metadate precum IPTC, Exif și XMP, profiluri de culoare încorporate, tabele de cuantizare etc.). Fiecare dintre aceste segmente începe cu un marker introdus de un octet FF hexazecimal. Acesta este urmat de un octet care indică tipul de segment. De exemplu, D8 completează markerul la marcatorul SOI FF D8 (Start Of Image), cu care începe fiecare fișier JPEG.

Fiecare început al unei scanări este marcat de marcatorul SOS (Start Of Scan, hexazecimal FF DA ). Deoarece datele din spatele markerului SOS sunt codificate cu entropie (JPEG-urile folosesc codarea Huffman), există un alt segment cu tabelele Huffman (DHT, hexazecimal FF C4 ) necesare pentru decodare înainte de segmentul SOS. Zona de interes pentru noi în cadrul unui fișier JPEG progresiv constă, prin urmare, în alternarea tabelelor Huffman/segmente de date scanate. Astfel, dacă dorim să afișăm prima scanare foarte grosieră a unei imagini, trebuie să solicităm toți octeții până la a doua apariție a unui segment DHT (hexazecimal FF C4 ) de la server.

Afișează marcatorii SOS într-un fișier JPEG
Structura unui fișier JPEG (previzualizare mare)

În PHP, putem folosi următorul cod pentru a citi numărul de octeți necesari pentru toate scanările într-o matrice:

 <?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; }

Trebuie să adăugăm valoarea de doi la poziția găsită, deoarece browserul redă ultimul rând al imaginii de previzualizare doar atunci când întâlnește un nou marker (care este format din doi octeți, așa cum tocmai am menționat).

Deoarece suntem interesați de prima imagine de previzualizare din acest exemplu, găsim poziția corectă în $positions[1] până la care trebuie să solicităm fișierul prin HTTP Range Request. Pentru a solicita o imagine cu o rezoluție mai bună, am putea folosi o poziție ulterioară în matrice, de exemplu $positions[3] .

3. Crearea Codului JavaScript Frontend

În primul rând, definim o etichetă img , căreia îi dăm poziția octetului tocmai evaluat:

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

Așa cum se întâmplă adesea cu bibliotecile lazy load, nu definim atributul src direct, astfel încât browserul să nu înceapă imediat să solicite imaginea de la server atunci când parsează codul HTML.

Cu următorul cod JavaScript încărcăm acum imaginea de previzualizare:

 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();

Acest cod creează o solicitare Ajax care îi spune serverului dintr-un antet de interval HTTP să returneze fișierul de la început la poziția specificată în data-bytes ... și nu mai mult. Dacă serverul înțelege HTTP Range Requests, returnează datele de imagine binare într-un răspuns HTTP-206 (HTTP 206 = Partial Content) sub forma unui blob, din care putem genera o adresă URL internă a browserului folosind createObjectURL . Folosim această adresă URL ca src pentru eticheta noastră img . Astfel, ne-am încărcat imaginea de previzualizare.

Stocăm blob-ul suplimentar la obiectul DOM din proprietatea src_part , deoarece vom avea nevoie imediat de aceste date.

În fila de rețea a consolei dezvoltatorului puteți verifica că nu am încărcat imaginea completă, ci doar o mică parte. În plus, încărcarea URL-ului blob-ului ar trebui să fie afișată cu o dimensiune de 0 octeți.

Afișează consola de rețea și dimensiunile solicitărilor HTTP
Consola de rețea la încărcarea imaginii de previzualizare (previzualizare mare)

Deoarece încărcăm deja antetul JPEG al fișierului original, imaginea de previzualizare are dimensiunea corectă. Astfel, în funcție de aplicație, putem omite înălțimea și lățimea etichetei img .

Alternativă: încărcarea imaginii de previzualizare în linie

Din motive de performanță, este de asemenea posibil să transferați datele imaginii de previzualizare ca URI de date direct în codul sursă HTML. Acest lucru ne scutește de suprasolicitarea transferului antetelor HTTP, dar codarea base64 face datele imaginii cu o treime mai mari. Acest lucru este relativizat dacă livrați codul HTML cu o codificare de conținut precum gzip sau brotli , dar ar trebui să utilizați totuși URI-uri de date pentru imagini de previzualizare mici.

Mult mai important este faptul că imaginile de previzualizare sunt disponibile imediat și nu există nicio întârziere vizibilă pentru utilizator la construirea paginii.

În primul rând, trebuie să creăm URI de date, pe care apoi îl folosim în eticheta img ca src . Pentru aceasta, creăm URI-ul de date prin PHP, prin care acest cod se bazează pe codul tocmai creat, care determină offset-urile de octeți ale markerilor SOS:

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

URI-ul de date creat este acum inserat direct în eticheta `img` ca src :

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

Desigur, și codul JavaScript trebuie adaptat:

 <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>

În loc să solicităm datele prin cererea Ajax, unde am primi imediat un blob, în ​​acest caz trebuie să creăm singuri blob-ul din URI-ul de date. Pentru a face acest lucru, eliberăm data-URI-ul de partea care nu conține date de imagine: data:image/jpeg;base64 . Decodificăm datele rămase codificate base64 cu comanda atob . Pentru a crea un blob din șirul de date acum binar, trebuie să transferăm datele într-o matrice Uint8, care asigură că datele nu sunt tratate ca un text codificat UTF-8. Din această matrice, acum putem crea un blob binar cu datele imaginii imaginii de previzualizare.

Pentru a nu fi nevoiți să adaptăm următorul cod pentru această versiune inline, adăugăm atributul data-bytes pe eticheta img , care în exemplul anterior conține offset-ul de octeți din care trebuie încărcată a doua parte a imaginii. .

În fila de rețea a consolei pentru dezvoltatori, puteți verifica și aici că încărcarea imaginii de previzualizare nu generează o solicitare suplimentară, în timp ce dimensiunea fișierului paginii HTML a crescut.

Afișează consola de rețea și dimensiunile solicitărilor HTTP
Consola de rețea la încărcarea imaginii de previzualizare ca URI de date (previzualizare mare)

Se încarcă imaginea finală

Într-un al doilea pas, încărcăm restul fișierului imagine după două secunde ca exemplu:

 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);

În antetul Range precizăm de data aceasta că dorim să solicităm imaginea de la poziția finală a imaginii de previzualizare până la sfârșitul fișierului. Răspunsul la prima solicitare este stocat în proprietatea src_part a obiectului DOM. Folosim răspunsurile de la ambele solicitări pentru a crea un nou blob per new Blob() , care conține datele întregii imagini. URL-ul blob generat din aceasta este folosit din nou ca src al obiectului DOM. Acum imaginea este complet încărcată.

De asemenea, acum putem verifica din nou dimensiunile încărcate în fila de rețea a consolei pentru dezvoltatori.

Afișează consola de rețea și dimensiunile solicitărilor HTTP
Consola de rețea la încărcarea întregii imagini (31,7 kB) (Previzualizare mare)

Prototip

La următoarea adresă URL am furnizat un prototip unde puteți experimenta diferiți parametri: https://embedded-image-preview.cerdmann.com/prototype/

Depozitul GitHub pentru prototip poate fi găsit aici: https://github.com/McSodbrenner/embedded-image-preview

Considerații la sfârșit

Folosind tehnologia Embedded Image Preview (EIP) prezentată aici, putem încărca imagini de previzualizare calitativ diferite din JPEG progresiv cu ajutorul Ajax și HTTP Range Requests. Datele din aceste imagini de previzualizare nu sunt eliminate, ci reutilizate pentru a afișa întreaga imagine.

În plus, nu trebuie create imagini de previzualizare. Pe partea de server, trebuie determinat și salvat doar offset-ul de octeți la care se termină imaginea de previzualizare. Într-un sistem CMS, ar trebui să fie posibil să se salveze acest număr ca atribut pe o imagine și să se țină cont de el atunci când îl scoateți în eticheta img . Chiar și un flux de lucru ar fi de imaginat, care completează numele fișierului imaginii cu offset, de exemplu progressive-8343.jpg , pentru a nu fi nevoie să salvezi offset-ul în afară de fișierul imagine. Această compensare ar putea fi extrasă de codul JavaScript.

Deoarece datele imaginii de previzualizare sunt reutilizate, această tehnică ar putea fi o alternativă mai bună la abordarea obișnuită de încărcare a unei imagini de previzualizare și apoi a unui WebP (și furnizarea unei rezervă JPEG pentru browserele care nu acceptă WebP). Imaginea de previzualizare distruge adesea avantajele de stocare ale WebP, care nu acceptă modul progresiv.

În prezent, imaginile de previzualizare în LQIP normal sunt de calitate inferioară, deoarece se presupune că încărcarea datelor de previzualizare necesită lățime de bandă suplimentară. După cum a spus deja Robin Osborne într-o postare pe blog din 2018, nu are prea mult sens să arăți substituenți care nu îți dau o idee despre imaginea finală. Folosind tehnica sugerată aici, putem arăta mai mult din imaginea finală ca imagine de previzualizare fără ezitare, prezentând utilizatorului o scanare ulterioară a JPEG progresiv.

În cazul unei conexiuni slabe la rețea a utilizatorului, ar putea avea sens, în funcție de aplicație, să nu încărcați întregul JPEG, ci de exemplu să omiteți ultimele două scanări. Acest lucru produce un JPEG mult mai mic, cu o calitate doar puțin redusă. Utilizatorul ne va mulțumi pentru asta și nu trebuie să stocăm un fișier suplimentar pe server.

Acum vă doresc multă distracție încercând prototipul și aștept cu nerăbdare comentariile voastre.