Gömülü Görüntü Önizlemeleri ile Daha Hızlı Görüntü Yükleme

Yayınlanan: 2022-03-10
Hızlı özet ↬ Bu makalede tanıtılan Gömülü Görüntü Önizleme (EIP) tekniği, ilerici JPEG'ler, Ajax ve HTTP aralığı isteklerini kullanarak yavaş yükleme sırasında ek veri aktarmak zorunda kalmadan önizleme görüntülerini yüklememize olanak tanır.

Düşük Kaliteli Görüntü Önizleme (LQIP) ve SVG tabanlı varyant SQIP, tembel görüntü yükleme için iki baskın tekniktir. Her ikisinin de ortak noktası, önce düşük kaliteli bir önizleme görüntüsü oluşturmanızdır. Bu, bulanık olarak gösterilecek ve daha sonra orijinal görüntü ile değiştirilecektir. Ek veri yüklemek zorunda kalmadan web sitesi ziyaretçisine bir önizleme resmi sunabilseydiniz ne olurdu?

Çoğunlukla tembel yüklemenin kullanıldığı JPEG dosyaları, spesifikasyona göre, içlerinde bulunan verileri, önce kaba, sonra ayrıntılı görüntü içeriği görüntülenecek şekilde saklama olanağına sahiptir. Yükleme sırasında (temel mod) görüntünün yukarıdan aşağıya oluşturulması yerine, giderek daha keskin hale gelen (aşamalı mod) bulanık bir görüntü çok hızlı bir şekilde görüntülenebilir.

Temel modda bir JPEG'nin zamansal yapısının temsili
Temel mod (Geniş önizleme)
Aşamalı modda bir JPEG'nin zamansal yapısının temsili
Aşamalı mod (Geniş önizleme)

Daha hızlı görüntülenen görünümün sağladığı daha iyi kullanıcı deneyimine ek olarak, aşamalı JPEG'ler genellikle temel kodlu benzerlerinden daha küçüktür. Yahoo geliştirme ekibinden Stoyan Stefanov'a göre, aşamalı mod kullanılırken 10 kB'den büyük dosyalar için daha küçük bir görüntünün yüzde 94 olasılığı vardır.

Web siteniz birçok JPEG'den oluşuyorsa, aşamalı JPEG'lerin bile birbiri ardına yüklendiğini fark edeceksiniz. Bunun nedeni, modern tarayıcıların bir etki alanına yalnızca altı eşzamanlı bağlantıya izin vermesidir. Bu nedenle, tek başına aşamalı JPEG'ler, kullanıcıya sayfanın mümkün olan en hızlı izlenimini vermenin çözümü değildir. En kötü durumda, tarayıcı bir sonrakini yüklemeye başlamadan önce bir resmi tamamen yükler.

Burada sunulan fikir, sunucudan yalnızca görüntü içeriği hakkında hızlı bir şekilde bir izlenim edinebileceğiniz kadar çok sayıda aşamalı JPEG bayt yüklemektir. Daha sonra tarafımızca belirlenen bir zamanda (örn. geçerli görünüm penceresindeki tüm önizleme görüntüleri yüklendiğinde), görüntünün geri kalanı, önceden önizleme için talep edilen kısım tekrar talep edilmeden yüklenmelidir.

EIP (Gömülü görüntü önizleme) tekniğinin görüntü verilerini iki istekte nasıl yüklediğini gösterir.
İki istekle aşamalı bir JPEG yükleme (Büyük önizleme)

Ne yazık ki, bir öznitelikteki bir img etiketine, görüntünün ne kadarının hangi zamanda yüklenmesi gerektiğini söyleyemezsiniz. Ancak Ajax ile bu, görüntüyü sunan sunucunun HTTP Aralık İsteklerini desteklemesi koşuluyla mümkündür.

İstemci, HTTP aralığı isteklerini kullanarak, HTTP yanıtında istenen dosyanın hangi baytlarının bulunacağını bir HTTP istek başlığında sunucuya bildirebilir. Daha büyük sunucuların (Apache, IIS, nginx) her biri tarafından desteklenen bu özellik, esas olarak video oynatma için kullanılır. Bir kullanıcı bir videonun sonuna atlarsa, kullanıcı nihayet istenen kısmı göremeden tüm videoyu yüklemek çok verimli olmaz. Bu nedenle, kullanıcının videoyu olabildiğince hızlı izleyebilmesi için sunucu tarafından yalnızca kullanıcının istediği zaman civarındaki video verileri talep edilir.

Şimdi aşağıdaki üç zorlukla karşı karşıyayız:

  1. Aşamalı JPEG Oluşturma
  2. İlk HTTP Aralığı İsteğinin Önizleme Görüntüsünü Yüklemesi Gereken Bayt Ofsetini Belirleme
  3. Ön Uç JavaScript Kodunu Oluşturma
Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

1. Aşamalı JPEG Oluşturma

Aşamalı bir JPEG, her biri son görüntünün bir bölümünü içeren birkaç sözde tarama bölümünden oluşur. İlk tarama, görüntüyü yalnızca çok kabaca gösterirken, dosyada daha sonra gelenler, önceden yüklenmiş verilere giderek daha ayrıntılı bilgiler ekler ve nihayet nihai görünümü oluşturur.

Tek tek taramaların tam olarak nasıl göründüğü, JPEG'leri oluşturan program tarafından belirlenir. Mozjpeg projesinden cjpeg gibi komut satırı programlarında, bu taramaların hangi verileri içerdiğini bile tanımlayabilirsiniz. Ancak, bu, bu makalenin kapsamını aşan daha derinlemesine bilgi gerektirir. Bunun için JPEG sıkıştırmanın temellerini öğreten "Son Olarak JPG'yi Anlamak" başlıklı makaleme başvurmak istiyorum. Bir tarama komut dosyasında programa iletilmesi gereken kesin parametreler mozjpeg projesinin sihirbaz.txt dosyasında açıklanmıştır. Benim düşünceme göre, mozjpeg tarafından varsayılan olarak kullanılan tarama komut dosyasının (yedi tarama) parametreleri hızlı aşamalı yapı ve dosya boyutu arasında iyi bir uzlaşmadır ve bu nedenle kabul edilebilir.

İlk JPEG'imizi aşamalı bir jpegtran için mozjpeg projesinden jpegtran kullanıyoruz. Bu, mevcut bir JPEG'de kayıpsız değişiklikler yapmak için bir araçtır. Windows ve Linux için önceden derlenmiş yapılar burada mevcuttur: https://mozjpeg.codelove.de/binaries.html. Güvenlik nedeniyle güvenli oynamayı tercih ediyorsanız, bunları kendiniz oluşturmak daha iyidir.

Komut satırından şimdi aşamalı JPEG'imizi oluşturuyoruz:

 $ jpegtran input.jpg > progressive.jpg

Aşamalı bir JPEG oluşturmak istediğimiz gerçeği jpegtran tarafından varsayılır ve açıkça belirtilmesi gerekmez. Görüntü verileri hiçbir şekilde değiştirilmeyecektir. Yalnızca dosya içindeki görüntü verilerinin düzeni değiştirilir.

Görüntünün görünümüyle ilgisi olmayan meta veriler (Exif, IPTC veya XMP verileri gibi), ideal olarak JPEG'den çıkarılmalıdır, çünkü karşılık gelen bölümler yalnızca görüntü içeriğinden önce geliyorlarsa meta veri kod çözücüleri tarafından okunabilir. Bu nedenle dosyadaki görüntü verilerinin arkasına taşıyamadığımız için, zaten önizleme görüntüsü ile teslim edilecekler ve ilk isteği buna göre büyüteceklerdi. Komut satırı programı exiftool ile bu meta verileri kolayca kaldırabilirsiniz:

 $ exiftool -all= progressive.jpg

Bir komut satırı aracı kullanmak istemiyorsanız, meta veriler olmadan aşamalı bir JPEG oluşturmak için compres-or-die.com çevrimiçi sıkıştırma hizmetini de kullanabilirsiniz.

2. İlk HTTP Aralığı İsteğinin Önizleme Görüntüsünü Yüklemesi Gereken Bayt Ofsetini Belirleyin

Bir JPEG dosyası, her biri farklı bileşenler (görüntü verileri, IPTC, Exif ve XMP gibi meta veriler, gömülü renk profilleri, niceleme tabloları vb.) içeren farklı bölümlere ayrılır. Bu bölümlerin her biri, onaltılık bir FF baytı tarafından tanıtılan bir işaretleyici ile başlar. Bunu, segmentin türünü belirten bir bayt takip eder. Örneğin, D8 , her bir JPEG dosyasının başladığı SOI işaretçisi FF D8 (Görüntünün Başlangıcı) işaretleyiciyi tamamlar.

Bir taramanın her başlangıcı, SOS işaretiyle işaretlenir (Taramanın Başlangıcı, onaltılık FF DA ). SOS işaretçisinin arkasındaki veriler entropi kodlu olduğundan (JPEG'ler Huffman kodlamasını kullanır), SOS segmentinden önce kodun çözülmesi için gereken Huffman tablolarına (DHT, onaltılık FF C4 ) sahip başka bir segment vardır. Bu nedenle, aşamalı bir JPEG dosyasındaki ilgi alanımız, değişen Huffman tablolarından/tarama veri bölümlerinden oluşur. Bu nedenle, bir görüntünün ilk çok kaba taramasını görüntülemek istiyorsak, sunucudan bir DHT segmentinin (onaltılık FF C4 ) ikinci oluşumuna kadar tüm baytları talep etmeliyiz.

Bir JPEG dosyasındaki SOS işaretçilerini gösterir
JPEG dosyasının yapısı (Geniş önizleme)

PHP'de, bir dizideki tüm taramalar için gereken bayt sayısını okumak için aşağıdaki kodu kullanabiliriz:

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

Bulunan konuma iki değerini eklemeliyiz, çünkü tarayıcı yeni bir işaretçiyle karşılaştığında (az önce bahsedildiği gibi iki bayttan oluşan) yalnızca önizleme görüntüsünün son satırını oluşturur.

Bu örnekteki ilk önizleme görüntüsüyle ilgilendiğimiz için, dosyayı HTTP Aralık İsteği aracılığıyla talep etmemiz gereken $positions[1] içinde doğru konumu buluyoruz. Daha iyi çözünürlüğe sahip bir görüntü istemek için dizide daha sonraki bir konumu kullanabiliriz, örneğin $positions[3] .

3. Ön Uç JavaScript Kodunu Oluşturma

Her şeyden önce, yeni değerlendirilen bayt konumunu verdiğimiz bir img etiketi tanımlarız:

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

Tembel yük kitaplıklarında sıklıkla olduğu gibi, tarayıcının HTML kodunu ayrıştırırken sunucudan hemen görüntü istemeye başlamaması için src niteliğini doğrudan tanımlamayız.

Aşağıdaki JavaScript koduyla şimdi önizleme görüntüsünü yüklüyoruz:

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

Bu kod, bir HTTP aralık başlığındaki sunucuya dosyayı baştan data-bytes belirtilen konuma döndürmesini söyleyen bir Ajax isteği oluşturur ... ve daha fazlasını değil. Sunucu HTTP Aralığı İsteklerini anlarsa, ikili görüntü verilerini bir HTTP-206 yanıtında (HTTP 206 = Kısmi İçerik) bir blob biçiminde döndürür ve buradan createObjectURL kullanarak bir tarayıcı dahili URL'si oluşturabiliriz. Bu URL'yi img için src olarak kullanıyoruz. Böylece önizleme resmimizi yükledik.

Blobu ek olarak src_part özelliğindeki DOM nesnesinde saklarız çünkü bu verilere hemen ihtiyacımız olacak.

Geliştirici konsolunun ağ sekmesinde, görüntünün tamamını değil, yalnızca küçük bir bölümünü yüklediğimizi kontrol edebilirsiniz. Ayrıca, blob URL'sinin yüklenmesi 0 bayt boyutunda görüntülenmelidir.

Ağ konsolunu ve HTTP isteklerinin boyutlarını gösterir
Önizleme görüntüsünü yüklerken ağ konsolu (Büyük önizleme)

Orijinal dosyanın JPEG başlığını zaten yüklediğimiz için önizleme görüntüsü doğru boyuta sahip. Böylece uygulamaya bağlı olarak img etiketinin yüksekliğini ve genişliğini atlayabiliriz.

Alternatif: Önizleme görüntüsünü satır içi yükleme

Performans nedenleriyle, önizleme görüntüsünün verilerini doğrudan HTML kaynak kodunda veri URI'si olarak aktarmak da mümkündür. Bu, bizi HTTP başlıklarını aktarma ek yükünden kurtarır, ancak base64 kodlaması, görüntü verilerini üçte bir oranında büyütür. HTML kodunu gzip veya brotli gibi bir içerik kodlamasıyla teslim ederseniz bu görecelidir, ancak yine de küçük önizleme görüntüleri için veri URI'lerini kullanmalısınız.

Daha da önemlisi, önizleme resimlerinin hemen kullanılabilir olması ve kullanıcı için sayfayı oluştururken gözle görülür bir gecikme olmamasıdır.

Her şeyden önce, daha sonra img etiketinde src olarak kullanacağımız veri URI'sini oluşturmalıyız. Bunun için, PHP aracılığıyla veri URI'sini oluşturuyoruz, bu kod, SOS işaretçilerinin bayt ofsetlerini belirleyen yeni oluşturulan kodu temel alır:

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

Oluşturulan veri URI'si artık doğrudan 'img' etiketine src olarak eklenir:

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

Elbette JavaScript kodu da uyarlanmalıdır:

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

Hemen bir blob alacağımız Ajax isteği aracılığıyla verileri talep etmek yerine, bu durumda blob'u veri URI'sinden kendimiz oluşturmamız gerekir. Bunu yapmak için data-URI'yi image data içermeyen kısımdan kurtarıyoruz: data:image/jpeg;base64 . Kalan base64 kodlu datayı atob komutu ile deşifre ediyoruz. Artık ikili dize verilerinden bir blob oluşturmak için, verileri bir Uint8 dizisine aktarmamız gerekir, bu da verilerin UTF-8 kodlu metin olarak ele alınmamasını sağlar. Bu diziden, şimdi önizleme görüntüsünün görüntü verileriyle ikili bir blob oluşturabiliriz.

Aşağıdaki kodu bu satır içi sürüme uyarlamak zorunda kalmamamız için, önceki örnekte görüntünün ikinci bölümünün yüklenmesi gereken bayt ofsetini içeren img etiketine data-bytes niteliğini ekleriz. .

Geliştirici konsolunun ağ sekmesinde, HTML sayfasının dosya boyutu artarken önizleme görüntüsünün yüklenmesinin ek bir istek oluşturmadığını da buradan kontrol edebilirsiniz.

Ağ konsolunu ve HTTP isteklerinin boyutlarını gösterir
Önizleme görüntüsünü veri URI'si olarak yüklerken ağ konsolu (Büyük önizleme)

Son görüntünün yüklenmesi

İkinci adımda, örnek olarak iki saniye sonra görüntü dosyasının geri kalanını yüklüyoruz:

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

Range başlığında bu sefer önizleme görüntüsünün bitiş konumundan dosyanın sonuna kadar görüntü istemek istediğimizi belirtiyoruz. İlk isteğin yanıtı, DOM nesnesinin src_part özelliğinde saklanır. Her new Blob() için tüm görüntünün verilerini içeren yeni bir blob oluşturmak için her iki istekten gelen yanıtları kullanırız. Bundan oluşturulan blob URL'si, DOM nesnesinin src olarak tekrar kullanılır. Şimdi görüntü tamamen yüklendi.

Ayrıca artık geliştirici konsolunun ağ sekmesinde yüklenen boyutları tekrar kontrol edebiliriz..

Ağ konsolunu ve HTTP isteklerinin boyutlarını gösterir
Tüm görüntüyü yüklerken ağ konsolu (31,7 kB) (Geniş önizleme)

Prototip

Aşağıdaki URL'de farklı parametrelerle deney yapabileceğiniz bir prototip sağladım: https://embedded-image-preview.cerdmann.com/prototype/

Prototip için GitHub deposu burada bulunabilir: https://github.com/McSodbrenner/embedded-image-preview

Sondaki Hususlar

Burada sunulan Gömülü Görüntü Önizleme (EIP) teknolojisini kullanarak, Ajax ve HTTP Aralık İsteklerinin yardımıyla aşamalı JPEG'lerden niteliksel olarak farklı önizleme görüntüleri yükleyebiliriz. Bu önizleme görüntülerinden elde edilen veriler atılmaz, bunun yerine görüntünün tamamını görüntülemek için yeniden kullanılır.

Ayrıca, önizleme resimlerinin oluşturulmasına gerek yoktur. Sunucu tarafında, yalnızca önizleme görüntüsünün bittiği bayt ofseti belirlenmeli ve kaydedilmelidir. Bir CMS sisteminde, bu numarayı bir görüntü üzerinde bir öznitelik olarak kaydetmek ve img etiketinde çıktı alırken dikkate almak mümkün olmalıdır. Ofseti resim dosyasından ayrı olarak kaydetmek zorunda kalmamak için, örneğin progresif-8343.jpg gibi resmin dosya adını ofsetle tamamlayan bir iş akışı bile düşünülebilir. Bu ofset, JavaScript kodu tarafından çıkarılabilir.

Önizleme görüntüsü verileri yeniden kullanıldığından, bu teknik, bir önizleme görüntüsünün ve ardından bir WebP'nin yüklenmesi (ve WebP'yi desteklemeyen tarayıcılar için bir JPEG yedeği sağlanması) şeklindeki olağan yaklaşıma daha iyi bir alternatif olabilir. Önizleme görüntüsü genellikle WebP'nin aşamalı modu desteklemeyen depolama avantajlarını yok eder.

Şu anda, normal LQIP'deki önizleme görüntüleri, önizleme verilerinin yüklenmesinin ek bant genişliği gerektirdiği varsayıldığından düşük kalitededir. Robin Osborne'un 2018'deki bir blog yazısında zaten açıklığa kavuşturduğu gibi, size nihai görüntü hakkında bir fikir vermeyen yer tutucuları göstermenin pek bir anlamı yok. Burada önerilen tekniği kullanarak, kullanıcıya aşamalı JPEG'in daha sonraki bir taramasını sunarak, son görüntünün bir kısmını tereddüt etmeden bir önizleme görüntüsü olarak gösterebiliriz.

Kullanıcının ağ bağlantısının zayıf olması durumunda, uygulamaya bağlı olarak tüm JPEG'i yüklemek değil, örneğin son iki taramayı atlamak mantıklı olabilir. Bu, yalnızca biraz azaltılmış kalitede çok daha küçük bir JPEG üretir. Kullanıcı bunun için bize teşekkür edecek ve sunucuda ek bir dosya saklamamız gerekmeyecek.

Şimdi size prototipi denerken iyi eğlenceler diliyorum ve yorumlarınızı bekliyorum.