使用嵌入式圖像預覽更快地加載圖像

已發表: 2022-03-10
快速總結↬本文介紹的嵌入式圖像預覽 (EIP) 技術允許我們在延遲加載期間使用漸進式 JPEG、Ajax 和 HTTP 範圍請求加載預覽圖像,而無需傳輸額外數據。

低質量圖像預覽 (LQIP) 和基於 SVG 的變體 SQIP 是延遲圖像加載的兩種主要技術。 兩者的共同點是您首先生成低質量的預覽圖像。 這將顯示模糊,稍後被原始圖像替換。 如果您可以向網站訪問者展示預覽圖像而無需加載其他數據會怎樣?

大多數使用延遲加載的 JPEG 文件有可能根據規範以先顯示粗略然後顯示詳細圖像內容的方式存儲其中包含的數據。 不是在加載過程中從上到下構建圖像(基線模式),而是可以非常快速地顯示模糊的圖像,逐漸變得越來越清晰(漸進模式)。

以基線模式表示 JPEG 的時間結構
基線模式(大預覽)
以漸進模式表示 JPEG 的時間結構
漸進模式(大預覽)

除了通過更快地顯示的外觀提供更好的用戶體驗之外,漸進式 JPEG 通常也比其基線編碼的對應物小。 根據 Yahoo 開發團隊的 Stoyan Stefanov 的說法,對於大於 10 kB 的文件,當使用漸進模式時,有 94% 的可能性是較小的圖像。

如果您的網站包含許多 JPEG,您會注意到即使是漸進式 JPEG 也會一個接一個地加載。 這是因為現代瀏覽器只允許六個同時連接到一個域。 因此,僅靠漸進式 JPEG 並不是給用戶最快的頁面印象的解決方案。 在最壞的情況下,瀏覽器會在開始加載下一張圖片之前完全加載一張圖片。

此處提出的想法現在是從服務器僅加載這麼多字節的漸進式 JPEG,以便您可以快速獲得圖像內容的印象。 稍後,在我們定義的時間(例如,噹噹前視口中的所有預覽圖像都已加載時),應加載圖像的其餘部分,而無需再次請求已請求預覽的部分。

顯示 EIP(嵌入式圖像預覽)技術在兩個請求中加載圖像數據的方式。
使用兩個請求加載漸進式 JPEG(大預覽)

不幸的是,您無法告訴屬性中的img標記應該在什麼時間加載多少圖像。 但是,使用 Ajax,這是可能的,前提是提供圖像的服務器支持 HTTP 範圍請求。

使用 HTTP 範圍請求,客戶端可以在 HTTP 請求標頭中通知服務器,請求文件的哪些字節將包含在 HTTP 響應中。 每個較大的服務器(Apache、IIS、nginx)都支持此功能,主要用於視頻播放。 如果用戶跳到視頻的結尾,那麼在用戶最終看到想要的部分之前加載完整的視頻不是很有效。 因此,服務器隻請求用戶請求的時間前後的視頻數據,以便用戶盡可能快地觀看視頻。

我們現在面臨以下三個挑戰:

  1. 創建漸進式 JPEG
  2. 確定第一個 HTTP 範圍請求必須加載預覽圖像的字節偏移量
  3. 創建前端 JavaScript 代碼
跳躍後更多! 繼續往下看↓

1. 創建漸進式 JPEG

漸進式 JPEG 由幾個所謂的掃描段組成,每個掃描段都包含最終圖像的一部分。 第一次掃描僅非常粗略地顯示圖像,而文件後面的掃描將越來越詳細的信息添加到已加載的數據中,最終形成最終外觀。

單個掃描的外觀如何由生成 JPEG 的程序確定。 在mozjpeg項目中的 cjpeg 等命令行程序中,您甚至可以定義這些掃描包含哪些數據。 但是,這需要更深入的知識,這超出了本文的範圍。 為此,我想參考我的文章“終於了解 JPG”,它講授了 JPEG 壓縮的基礎知識。 mozjpeg 項目的wizard.txt 中解釋了必須在掃描腳本中傳遞給程序的確切參數。 在我看來,mozjpeg 默認使用的掃描腳本(七次掃描)的參數是快速漸進結構和文件大小之間的一個很好的折衷,因此可以採用。

為了將我們的初始 JPEG 轉換為漸進式 JPEG,我們使用了jpegtran項目中的 jpegtran。 這是對現有 JPEG 進行無損更改的工具。 可在此處獲得適用於 Windows 和 Linux 的預編譯版本:https://mozjpeg.codelove.de/binaries.html。 如果您出於安全原因更喜歡安全地使用它,最好自己構建它們。

我們現在從命令行創建漸進式 JPEG:

 $ jpegtran input.jpg > progressive.jpg

我們想要構建漸進式 JPEG 的事實由 jpegtran 假設,不需要明確指定。 圖像數據不會以任何方式更改。 僅更改文件內圖像數據的排列。

與圖像外觀無關的元數據(例如 Exif、IPTC 或 XMP 數據)最好從 JPEG 中刪除,因為只有在圖像內容之前,元數據解碼器才能讀取相應的片段。 由於這個原因我們不能將它們移動到文件中的圖像數據後面,它們已經與預覽圖像一起交付並相應地放大第一個請求。 使用命令行程序exiftool ,您可以輕鬆刪除這些元數據:

 $ exiftool -all= progressive.jpg

如果您不想使用命令行工具,您還可以使用在線壓縮服務 compress-or-die.com 生成沒有元數據的漸進式 JPEG。

2. 確定第一個 HTTP 範圍請求必須加載預覽圖像的字節偏移量

JPEG 文件分為不同的段,每個段包含不同的組件(圖像數據、IPTC、Exif 和 XMP 等元數據、嵌入的顏色配置文件、量化表等)。 這些段中的每一個都以十六進制FF字節引入的標記開始。 後面跟著一個字節,指示段的類型。 例如, D8將標記完成到 SOI 標記FF D8 (圖像開始),每個 JPEG 文件都以此開始。

每次掃描的開始都由 SOS 標記(掃描開始,十六進制FF DA )標記。 由於 SOS 標記後面的數據是熵編碼的(JPEG 使用 Huffman 編碼),因此在 SOS 段之前還有另一個段需要解碼,其中包含 Huffman 表(DHT,十六進制FF C4 )。 因此,我們在漸進式 JPEG 文件中感興趣的區域由交替的霍夫曼表/掃描數據段組成。 因此,如果我們想要顯示圖像的第一次非常粗略的掃描,我們必須從服務器請求直到第二次出現 DHT 段(十六進制FF C4 )的所有字節。

在 JPEG 文件中顯示 SOS 標記
JPEG 文件的結構(大預覽)

在 PHP 中,我們可以使用以下代碼將所有掃描所需的字節數讀入一個數組:

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

我們必須將值 2 添加到找到的位置,因為瀏覽器僅在遇到新標記時才呈現預覽圖像的最後一行(它由剛才提到的兩個字節組成)。

由於我們對本示例中的第一張預覽圖像感興趣,因此我們在$positions[1]中找到了正確的位置,我們必須通過 HTTP 範圍請求來請求文件。 要請求具有更好分辨率的圖像,我們可以使用數組中較晚的位置,例如$positions[3]

3. 創建前端 JavaScript 代碼

首先,我們定義一個img標籤,我們給它剛剛評估的字節位置:

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

與延遲加載庫的常見情況一樣,我們不直接定義src屬性,這樣瀏覽器在解析 HTML 代碼時不會立即開始從服務器請求圖像。

使用以下 JavaScript 代碼,我們現在加載預覽圖像:

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

此代碼創建一個 Ajax 請求,該請求在 HTTP 範圍標頭中告訴服務器將文件從開頭返回到data-bytes中指定的位置……僅此而已。 如果服務器理解 HTTP 範圍請求,它會在 HTTP-206 響應(HTTP 206 = 部分內容)中以 blob 的形式返回二進製圖像數據,我們可以使用createObjectURL從中生成瀏覽器內部 URL。 我們將此 URL 用作img標籤的src 。 因此,我們已經加載了預覽圖像。

我們將 blob 額外存儲在屬性src_part中的 DOM 對像中,因為我們將立即需要這些數據。

在開發者控制台的網絡選項卡中,您可以檢查我們沒有加載完整的圖像,而只是加載了一小部分。 此外,blob URL 的加載應該以 0 字節的大小顯示。

顯示網絡控制台和 HTTP 請求的大小
加載預覽圖像時的網絡控制台(大預覽)

由於我們已經加載了原始文件的 JPEG 標頭,因此預覽圖像具有正確的大小。 因此,根據應用程序,我們可以省略img標籤的高度和寬度。

替代方案:內聯加載預覽圖像

出於性能原因,也可以直接在 HTML 源代碼中將預覽圖像的數據作為數據 URI 傳輸。 這為我們節省了傳輸 HTTP 標頭的開銷,但 base64 編碼使圖像數據增大了三分之一。 如果您使用gzipbrotli等內容編碼交付 HTML 代碼,這是相對化的,但您仍應將數據 URI 用於小型預覽圖像。

更重要的是預覽圖像立即可用,並且用戶在構建頁面時沒有明顯的延遲。

首先,我們必須創建數據 URI,然後在img標記中將其用作src 。 為此,我們通過 PHP 創建數據 URI,此代碼基於剛剛創建的代碼,它確定了 SOS 標記的字節偏移量:

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

創建的數據 URI 現在直接作為src插入到 `img` 標記中:

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

當然,JavaScript 代碼也必須進行適配:

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

不是通過 Ajax 請求來請求數據,在這種情況下我們會立即收到一個 blob,在這種情況下,我們必須自己從數據 URI 中創建 blob。 為此,我們從不包含圖像數據的部分中釋放 data-URI: data:image/jpeg;base64 。 我們使用atob命令解碼剩餘的 base64 編碼數據。 為了從現在的二進製字符串數據創建 blob,我們必須將數據傳輸到 Uint8 數組中,以確保數據不會被視為 UTF-8 編碼文本。 從這個數組中,我們現在可以使用預覽圖像的圖像數據創建一個二進制 blob。

因此我們不必為這個內聯版本修改以下代碼,我們在img標記上添加屬性data-bytes ,在前面的示例中包含必須加載圖像第二部分的字節偏移量.

在開發者控制台的網絡選項卡中,您還可以在這裡查看加載預覽圖像不會產生額外的請求,而 HTML 頁面的文件大小有所增加。

顯示網絡控制台和 HTTP 請求的大小
將預覽圖像加載為數據 URI 時的網絡控制台(大預覽)

加載最終圖像

在第二步中,我們以兩秒後加載圖像文件的其餘部分為例:

 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 頭中指定我們要請求從預覽圖的結束位置到文件末尾的圖像。 第一個請求的答案存儲在 DOM 對象的屬性src_part中。 我們使用兩個請求的響應為每個new Blob()創建一個新的 Blob,其中包含整個圖像的數據。 由此生成的 blob URL 再次用作 DOM 對象的src 。 現在圖像已完全加載。

此外,現在我們可以再次在開發者控制台的網絡選項卡中檢查加載的大小..

顯示網絡控制台和 HTTP 請求的大小
加載整個圖像時的網絡控制台 (31.7 kB) (大預覽)

原型

在以下 URL 中,我提供了一個原型,您可以在其中試驗不同的參數:https://embedded-image-preview.cerdmann.com/prototype/

可以在此處找到原型的 GitHub 存儲庫:https://github.com/McSodbrenner/embedded-image-preview

最後的考慮

使用此處介紹的嵌入式圖像預覽 (EIP) 技術,我們可以在 Ajax 和 HTTP 範圍請求的幫助下從漸進式 JPEG 加載質量不同的預覽圖像。 這些預覽圖像中的數據不會被丟棄,而是重新用於顯示整個圖像。

此外,無需創建預覽圖像。 在服務器端,只需確定並保存預覽圖像結束的字節偏移量。 在 CMS 系統中,應該可以將此數字保存為圖像上的屬性,並在img標籤中輸出時將其考慮在內。 甚至可以設想工作流,其通過偏移量補充圖片的文件名,例如progressive-8343.jpg ,以便不必將偏移量與圖片文件分開保存。 這個偏移量可以由 JavaScript 代碼提取。

由於預覽圖像數據被重用,這種技術可能是加載預覽圖像然後是 WebP(並為不支持 WebP 的瀏覽器提供 JPEG 後備)的通常方法的更好替代方法。 預覽圖往往會破壞 WebP 的存儲優勢,不支持漸進模式。

目前,普通 LQIP 中的預覽圖像質量較差,因為假設加載預覽數據需要額外的帶寬。 正如 Robin Osborne 在 2018 年的一篇博文中已經明確指出的那樣,顯示無法讓您了解最終圖像的佔位符沒有多大意義。 通過使用此處建議的技術,我們可以毫不猶豫地向用戶展示漸進式 JPEG 的後續掃描,從而將更多最終圖像顯示為預覽圖像。

如果用戶的網絡連接較弱,根據應用程序的不同,不加載整個 JPEG,但例如省略最後兩次掃描可能是有意義的。 這會產生一個小得多的 JPEG,其質量只會略微降低。 用戶會感謝我們,我們不必在服務器上存儲額外的文件。

現在,我希望您在試用原型時玩得開心,並期待您的評論。