Gömülü Görüntü Önizlemeleri ile Daha Hızlı Görüntü Yükleme
Yayınlanan: 2022-03-10Düşü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.
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.
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:
- Aşamalı JPEG Oluşturma
- İlk HTTP Aralığı İsteğinin Önizleme Görüntüsünü Yüklemesi Gereken Bayt Ofsetini Belirleme
- Ön Uç JavaScript Kodunu Oluşturma
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.
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.
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.
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..
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.