Pemuatan Gambar Lebih Cepat Dengan Pratinjau Gambar Tersemat
Diterbitkan: 2022-03-10Pratinjau Gambar Berkualitas Rendah (LQIP) dan varian SQIP berbasis SVG adalah dua teknik utama untuk pemuatan gambar yang lambat. Persamaan keduanya adalah Anda pertama kali menghasilkan gambar pratinjau berkualitas rendah. Ini akan ditampilkan buram dan kemudian diganti dengan gambar asli. Bagaimana jika Anda dapat menampilkan gambar pratinjau kepada pengunjung situs web tanpa harus memuat data tambahan?
File JPEG, yang kebanyakan menggunakan lazy loading, memiliki kemungkinan, menurut spesifikasi, untuk menyimpan data yang ada di dalamnya sedemikian rupa sehingga pertama-tama konten gambar kasar dan kemudian detail gambar ditampilkan. Alih-alih memiliki gambar yang dibangun dari atas ke bawah selama pemuatan (mode dasar), gambar buram dapat ditampilkan dengan sangat cepat, yang secara bertahap menjadi lebih tajam dan lebih tajam (mode progresif).
Selain pengalaman pengguna yang lebih baik yang diberikan oleh tampilan yang ditampilkan lebih cepat, JPEG progresif biasanya juga lebih kecil daripada rekan-rekan yang dikodekan dasar. Untuk file yang lebih besar dari 10 kB, ada kemungkinan 94 persen dari gambar yang lebih kecil saat menggunakan mode progresif menurut Stoyan Stefanov dari tim pengembangan Yahoo.
Jika situs web Anda terdiri dari banyak JPEG, Anda akan melihat bahwa bahkan JPEG progresif memuat satu demi satu. Ini karena browser modern hanya mengizinkan enam koneksi simultan ke domain. Oleh karena itu, JPEG progresif saja bukanlah solusi untuk memberi pengguna kesan halaman secepat mungkin. Dalam kasus terburuk, browser akan memuat gambar sepenuhnya sebelum mulai memuat gambar berikutnya.
Ide yang disajikan di sini sekarang adalah memuat hanya begitu banyak byte JPEG progresif dari server sehingga Anda dapat dengan cepat mendapatkan kesan konten gambar. Kemudian, pada waktu yang ditentukan oleh kami (misalnya ketika semua gambar pratinjau di viewport saat ini telah dimuat), sisa gambar harus dimuat tanpa meminta bagian yang sudah diminta untuk pratinjau lagi.
Sayangnya, Anda tidak dapat memberi tahu tag img
dalam atribut berapa banyak gambar yang harus dimuat pada jam berapa. Namun, dengan Ajax, ini dimungkinkan, asalkan server yang mengirimkan gambar mendukung Permintaan Rentang HTTP.
Menggunakan permintaan rentang HTTP, klien dapat menginformasikan server di header permintaan HTTP byte mana dari file yang diminta yang akan dimuat dalam respons HTTP. Fitur ini, didukung oleh masing-masing server yang lebih besar (Apache, IIS, nginx), terutama digunakan untuk pemutaran video. Jika pengguna melompat ke akhir video, tidak akan efisien untuk memuat video lengkap sebelum pengguna akhirnya dapat melihat bagian yang diinginkan. Oleh karena itu, hanya data video sekitar waktu yang diminta oleh pengguna yang diminta oleh server, sehingga pengguna dapat menonton video secepat mungkin.
Kami sekarang menghadapi tiga tantangan berikut:
- Membuat JPEG Progresif
- Tentukan Offset Byte Hingga Permintaan Rentang HTTP Pertama Yang Harus Memuat Gambar Pratinjau
- Membuat Kode JavaScript Frontend
1. Membuat JPEG Progresif
Sebuah JPEG progresif terdiri dari beberapa segmen yang disebut scan, yang masing-masing berisi bagian dari gambar akhir. Pemindaian pertama hanya menunjukkan gambar secara kasar, sedangkan pemindaian berikutnya dalam file menambahkan informasi yang lebih detail ke data yang sudah dimuat dan akhirnya membentuk tampilan akhir.
Bagaimana tepatnya tampilan pemindaian individu ditentukan oleh program yang menghasilkan JPEG. Dalam program baris perintah seperti cjpeg dari proyek mozjpeg, Anda bahkan dapat menentukan data mana yang berisi pemindaian ini. Namun, ini membutuhkan pengetahuan yang lebih mendalam, yang akan melampaui cakupan artikel ini. Untuk ini, saya ingin merujuk ke artikel saya "Akhirnya Memahami JPG", yang mengajarkan dasar-dasar kompresi JPEG. Parameter persis yang harus diteruskan ke program dalam skrip pemindaian dijelaskan di wizard.txt dari proyek mozjpeg. Menurut pendapat saya, parameter skrip pemindaian (tujuh pemindaian) yang digunakan oleh mozjpeg secara default adalah kompromi yang baik antara struktur progresif cepat dan ukuran file dan oleh karena itu, dapat diadopsi.
Untuk mengubah JPEG awal kami menjadi JPEG progresif, kami menggunakan jpegtran
dari proyek mozjpeg. Ini adalah alat untuk membuat perubahan lossless ke JPEG yang ada. Build yang telah dikompilasi sebelumnya untuk Windows dan Linux tersedia di sini: https://mozjpeg.codelove.de/binary.html. Jika Anda lebih suka memainkannya dengan aman karena alasan keamanan, lebih baik Anda membuatnya sendiri.
Dari baris perintah sekarang kami membuat JPEG progresif kami:
$ jpegtran input.jpg > progressive.jpg
Fakta bahwa kita ingin membangun JPEG progresif diasumsikan oleh jpegtran dan tidak perlu ditentukan secara eksplisit. Data gambar tidak akan diubah dengan cara apa pun. Hanya susunan data gambar di dalam file yang diubah.
Metadata yang tidak relevan dengan tampilan gambar (seperti data Exif, IPTC, atau XMP), idealnya harus dihapus dari JPEG karena segmen terkait hanya dapat dibaca oleh dekoder metadata jika mendahului konten gambar. Karena kami tidak dapat memindahkannya ke belakang data gambar dalam file karena alasan ini, gambar tersebut akan dikirimkan dengan gambar pratinjau dan memperbesar permintaan pertama yang sesuai. Dengan exiftool
program baris perintah Anda dapat dengan mudah menghapus metadata ini:
$ exiftool -all= progressive.jpg
Jika Anda tidak ingin menggunakan alat baris perintah, Anda juga dapat menggunakan layanan kompresi online compress-or-die.com untuk menghasilkan JPEG progresif tanpa metadata.
2. Tentukan Byte Offset Hingga Permintaan Rentang HTTP Pertama Yang Harus Memuat Gambar Pratinjau
File JPEG dibagi menjadi segmen yang berbeda, masing-masing berisi komponen yang berbeda (data gambar, metadata seperti IPTC, Exif dan XMP, profil warna yang disematkan, tabel kuantisasi, dll.). Masing-masing segmen ini dimulai dengan penanda yang diperkenalkan oleh byte FF
heksadesimal. Ini diikuti oleh byte yang menunjukkan jenis segmen. Misalnya, D8
melengkapi penanda ke penanda SOI FF D8
(Mulai Gambar), yang dengannya setiap file JPEG dimulai.
Setiap awal pemindaian ditandai dengan penanda SOS (Start Of Scan, heksadesimal FF DA
). Karena data di belakang penanda SOS dikodekan entropi (JPEG menggunakan pengkodean Huffman), ada segmen lain dengan tabel Huffman (DHT, heksadesimal FF C4
) yang diperlukan untuk decoding sebelum segmen SOS. Area yang menarik bagi kami dalam file JPEG progresif, oleh karena itu, terdiri dari tabel Huffman/segmen data pindaian. Jadi, jika kita ingin menampilkan pemindaian gambar yang sangat kasar pertama, kita harus meminta semua byte hingga kemunculan kedua segmen DHT (heksadesimal FF C4
) dari server.
Di PHP, kita dapat menggunakan kode berikut untuk membaca jumlah byte yang diperlukan untuk semua pemindaian ke dalam 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; }
Kita harus menambahkan nilai dua ke posisi yang ditemukan karena browser hanya merender baris terakhir dari gambar pratinjau ketika menemukan penanda baru (yang terdiri dari dua byte seperti yang baru saja disebutkan).
Karena kami tertarik pada gambar pratinjau pertama dalam contoh ini, kami menemukan posisi yang benar di $positions[1]
hingga kami harus meminta file melalui Permintaan Rentang HTTP. Untuk meminta gambar dengan resolusi yang lebih baik, kita dapat menggunakan posisi selanjutnya dalam larik, misalnya $positions[3]
.
3. Membuat Kode JavaScript Frontend
Pertama-tama, kami mendefinisikan tag img
, yang kami berikan posisi byte yang baru saja dievaluasi:
<img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">
Seperti yang sering terjadi pada library lazy load, kami tidak mendefinisikan atribut src
secara langsung sehingga browser tidak segera mulai meminta gambar dari server saat mengurai kode HTML.
Dengan kode JavaScript berikut sekarang kita memuat gambar pratinjau:
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();
Kode ini membuat permintaan Ajax yang memberi tahu server di header rentang HTTP untuk mengembalikan file dari awal ke posisi yang ditentukan dalam data-bytes
... dan tidak lebih. Jika server memahami Permintaan Rentang HTTP, server akan mengembalikan data gambar biner dalam respons HTTP-206 (HTTP 206 = Konten Sebagian) dalam bentuk gumpalan, yang darinya kita dapat membuat URL internal browser menggunakan createObjectURL
. Kami menggunakan URL ini sebagai src
untuk tag img
kami. Jadi kami telah memuat gambar pratinjau kami.
Kami menyimpan blob tambahan di objek DOM di properti src_part
, karena kami akan membutuhkan data ini segera.
Di tab jaringan konsol pengembang Anda dapat memeriksa bahwa kami belum memuat gambar lengkap, tetapi hanya sebagian kecil. Selain itu, pemuatan URL blob harus ditampilkan dengan ukuran 0 byte.
Karena kita sudah memuat header JPEG dari file asli, gambar pratinjau memiliki ukuran yang benar. Jadi, tergantung pada aplikasinya, kita dapat menghilangkan tinggi dan lebar tag img
.
Alternatif: Memuat gambar pratinjau sebaris
Untuk alasan kinerja, juga dimungkinkan untuk mentransfer data gambar pratinjau sebagai URI data secara langsung dalam kode sumber HTML. Ini menghemat biaya transfer header HTTP, tetapi pengkodean base64 membuat data gambar sepertiga lebih besar. Ini direlatifkan jika Anda mengirimkan kode HTML dengan pengkodean konten seperti gzip atau brotli , tetapi Anda tetap harus menggunakan URI data untuk gambar pratinjau kecil.
Jauh lebih penting adalah kenyataan bahwa gambar pratinjau segera tersedia dan tidak ada penundaan yang nyata bagi pengguna saat membuat halaman.
Pertama-tama, kita harus membuat URI data, yang kemudian kita gunakan di tag img
sebagai src
. Untuk ini, kami membuat URI data melalui PHP, di mana kode ini didasarkan pada kode yang baru saja dibuat, yang menentukan offset byte dari penanda SOS:
<?php … $fp = fopen($img, 'r'); $data_uri = 'data:image/jpeg;base64,'. base64_encode(fread($fp, $positions[1])); fclose($fp);
URI data yang dibuat sekarang langsung dimasukkan ke dalam tag `img` sebagai src
:
<img src="<?= $data_uri ?>" data-src="progressive.jpg" alt="">
Tentu saja, kode JavaScript juga harus disesuaikan:
<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>
Alih-alih meminta data melalui permintaan Ajax, di mana kita akan segera menerima blob, dalam hal ini kita harus membuat blob sendiri dari URI data. Untuk melakukan ini, kami membebaskan data-URI dari bagian yang tidak berisi data gambar: data:image/jpeg;base64
. Kami memecahkan kode data kode base64 yang tersisa dengan perintah atob
. Untuk membuat gumpalan dari data string biner sekarang, kita harus mentransfer data ke dalam larik Uint8, yang memastikan bahwa data tidak diperlakukan sebagai teks yang disandikan UTF-8. Dari larik ini, sekarang kita dapat membuat gumpalan biner dengan data gambar dari gambar pratinjau.
Agar kita tidak perlu mengadaptasi kode berikut untuk versi sebaris ini, kita menambahkan atribut data-bytes
pada tag img
, yang pada contoh sebelumnya berisi byte offset dari mana bagian kedua gambar harus dimuat .
Di tab jaringan konsol pengembang, Anda juga dapat memeriksa di sini bahwa memuat gambar pratinjau tidak menghasilkan permintaan tambahan, sedangkan ukuran file halaman HTML telah meningkat.
Memuat gambar akhir
Pada langkah kedua kita memuat sisa file gambar setelah dua detik sebagai contoh:
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);
Pada header Range kali ini kita tentukan bahwa kita ingin request gambar dari posisi akhir gambar preview sampai akhir file. Jawaban atas permintaan pertama disimpan di properti src_part
dari objek DOM. Kami menggunakan respons dari kedua permintaan untuk membuat blob baru per new Blob()
, yang berisi data seluruh gambar. URL gumpalan yang dihasilkan dari ini digunakan lagi sebagai src
dari objek DOM. Sekarang gambar sudah terisi penuh.
Juga sekarang kami dapat memeriksa ukuran yang dimuat di tab jaringan konsol pengembang lagi..
Prototipe
Di URL berikut saya telah menyediakan prototipe di mana Anda dapat bereksperimen dengan parameter yang berbeda: https://embedded-image-preview.cerdmann.com/prototype/
Repositori GitHub untuk prototipe dapat ditemukan di sini: https://github.com/McSodbrenner/embedded-image-preview
Pertimbangan Di Akhir
Menggunakan teknologi Pratinjau Gambar Tertanam (EIP) yang disajikan di sini, kami dapat memuat gambar pratinjau yang berbeda secara kualitatif dari JPEG progresif dengan bantuan Ajax dan Permintaan Rentang HTTP. Data dari gambar pratinjau ini tidak dibuang tetapi digunakan kembali untuk menampilkan seluruh gambar.
Selanjutnya, tidak ada gambar pratinjau yang perlu dibuat. Di sisi server, hanya offset byte tempat gambar pratinjau berakhir yang harus ditentukan dan disimpan. Dalam sistem CMS, nomor ini harus dapat disimpan sebagai atribut pada gambar dan memperhitungkannya saat mengeluarkannya dalam tag img
. Bahkan alur kerja dapat dibayangkan, yang melengkapi nama file gambar dengan offset, misalnya progressive-8343.jpg , agar tidak harus menyimpan offset selain dari file gambar. Offset ini dapat diekstraksi dengan kode JavaScript.
Karena data gambar pratinjau digunakan kembali, teknik ini bisa menjadi alternatif yang lebih baik daripada pendekatan biasa memuat gambar pratinjau dan kemudian WebP (dan menyediakan fallback JPEG untuk browser yang tidak mendukung WebP). Gambar pratinjau sering kali merusak keunggulan penyimpanan WebP, yang tidak mendukung mode progresif.
Saat ini, gambar pratinjau dalam LQIP normal memiliki kualitas yang lebih rendah, karena diasumsikan bahwa memuat data pratinjau memerlukan bandwidth tambahan. Seperti yang sudah dijelaskan oleh Robin Osborne dalam posting blog pada tahun 2018, tidak masuk akal untuk menunjukkan placeholder yang tidak memberi Anda gambaran tentang gambar akhir. Dengan menggunakan teknik yang disarankan di sini, kami dapat menampilkan lebih banyak lagi gambar akhir sebagai gambar pratinjau tanpa ragu-ragu dengan menghadirkan pemindaian JPEG progresif kepada pengguna nanti.
Dalam kasus koneksi jaringan yang lemah dari pengguna, mungkin masuk akal, tergantung pada aplikasinya, untuk tidak memuat seluruh JPEG, tetapi misalnya untuk menghilangkan dua pemindaian terakhir. Ini menghasilkan JPEG yang jauh lebih kecil dengan kualitas yang hanya sedikit berkurang. Pengguna akan berterima kasih kepada kami untuk itu, dan kami tidak perlu menyimpan file tambahan di server.
Sekarang saya berharap Anda bersenang-senang mencoba prototipe dan menantikan komentar Anda.