HTTP Archive 在 2022 年關於多媒體的報告中指出,目前大概有 99.9% 的網站或多或少都會包含點圖像。
並且高達 70% 的移動頁面和 80% 的桌面頁面的 LCP 指標會受圖像的影響。
通過這些數據可知,圖像在網頁中佔據着舉足輕重的地位,優化圖像,對於網頁性能可以達到立竿見影的效果。
優化的核心是控制圖像的尺寸,提前、延遲或減少圖像的請求,以及降低對核心 Web 指標的影響。
本文所用的示例代碼已上傳至 Github。
一、請求
以我目前的公司爲例,活動頁中圖像的請求數佔比最高可達 64%。
若不做優化處理,那麼將直接拉長頁面的白屏時間,體驗將會及其糟糕。
1)懶加載
懶加載就是延遲請求的時機,在觸發某個特定條件後,再請求。
常用的條件是當圖像出現在屏幕內時,觸發請求。
當頁面很長時,並不需要在頁面首屏加載時就請求所有圖像,而是滾動到圖像所在位置後,再將圖像顯示,如下圖所示。
目前有 3 種方式來實現懶加載。那麼在正式講解之前,需要先了解一下視口的概念。
視口(viewport)就是下圖中的灰色部分,也就是文檔內容的可視區域,圖中用粗線框住的是瀏覽器的外殼部分(如標籤頁、書籤欄、調試工具等)。
先來講解第 1 種懶加載:傳統的 JavaScript 實現,原理就是計算圖像頂部到視口頂部的距離,包括頁面隱藏部分。
若此距離大於等於當前滾動條的位置(即可視區域),那麼就可以認爲滿足條件,需要顯示圖像。
假設滾動條是在body中,那麼當前可視區域的範圍如下所示。
const viewTop = window.pageYOffset;
const viewBottom = window.innerHeight + viewTop;
window.pageYOffset 表示視口上邊的距離,如果沒有出現垂直方向的滾動條,那麼對應屬性的值爲 0。window.innerHeight 表示視口的高度。
而圖像到視口頂部的距離可以通過 getBoundingClientRect() 的 DOMRect.top 屬性得到,如下所示。
const nodeTop = node.getBoundingClientRect().top + viewTop;
完整的代碼如下所示, blank.gif 是一張 1*1 的空白佔位圖,data-src 是真實的圖像地址,scroll 是滾動條事件。
<img src="blank.gif" data-src="cover.jpg" width="100%" /> <img src="blank.gif" data-src="cover.jpg" width="100%" /> <img src="blank.gif" data-src="cover.jpg" width="100%" /> <script> window.addEventListener('scroll', () => { const viewTop = window.pageYOffset; const viewBottom = window.innerHeight + viewTop; // 查詢包含 data-src 自定義屬性的 img document.querySelectorAll('img[data-src]').forEach(node => { const nodeTop = node.getBoundingClientRect().top + viewTop; if (nodeTop >= viewTop && nodeTop <= viewBottom) { node.src = node.dataset.src; } }); }); </script>
當前只是爲了做演示,兼容性和性能方面並未做深入優化,可以參考市面上成熟的懶加載庫,例如 Layzr.js。
接下來講解第 2 種通過 Intersection Observer 實現懶加載。
Intersection Observer 提供了一種異步的對目標元素與視口是否相交的檢測方法,即檢測目標元素是否在可視區域中。
示例代碼如下,省去了位置計算的邏輯,通過 isIntersecting 屬性就能判斷元素的可見性。
const observer = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { // 不在可視區域內就返回 if (!entry.isIntersecting) return; const img = entry.target; img.src = img.dataset.src; observer.unobserve(img); // 取消監控 }); }); document.querySelectorAll("img[data-src]").forEach((node) => { observer.observe(node); });
最後講解第 3 種懶加載方式:img 元素的 loading 屬性。該屬性會指示瀏覽器當圖像不在可視區域時的加載方式。
這種方式相比較前兩者,最爲簡潔,不需要編寫額外的腳本,示例代碼如下所示。
<img src="cover.jpg" loading="lazy" width="300" height="400"/>
2022 年有 91.47% 的瀏覽器已支持 loading 屬性,並且大概有 24% 的網站在使用它,相比去年有 1.4 倍的增長。
但是其延遲加載的規則,即圖像與視口頂部的距離是多少時開始加載,全部由瀏覽器自行定義。
注意,在 Chrome 中調試發現,頁面打開有兩三屏的圖像就開始請求了,開始滾動後,剩餘的圖像就開始陸續請求。
並不是說到了圖像的可視區域後,纔開始請求。
2)預加載
預加載和懶加載正好相反,它是在圖像還沒出現在可視區域時提前請求。
之前做過一次公司招聘的活動頁,其中會涉及到好多動畫和很多圖像,並且需要在手機中翻頁瀏覽。
一開始將所有圖像地址直接寫在頁面中,在測試環境就發現打開非常慢(如下圖所示),過了幾十秒後纔會出現 Loading 過渡動畫。
於是就對其進行優化,將圖像的默認請求替換成一張空白圖(與之前的懶加載一樣),然後在腳本執行後再將首屏替換成真實地址。
示例代碼如下,初始化 Image 實例,在觸發 load 事件時執行自定義回調,可以是替換 img 元素地址。
function loadImage(url, callback) { const img = new Image(); img.src = url; img.onload = function () { //將回調函數的 this 替換爲Image對象 callback.call(img); }; } document.querySelectorAll("img[data-src]").forEach((node) => { loadImage(node.dataset.src, function () { node.src = this.src; }); });
在翻頁時,可以將後面幾頁的圖像進行預加載,然後在翻到那頁後,不會出現等待圖像加載的情況,並且動畫就會更加絲滑和順暢。
3)Data URI
img 元素的 src 屬性或 CSS 的 background-image 屬性的值都可以是一個經過 Base64 編碼後 Data URI,這樣能減少額外的HTTP請求。
Data URI 由協議、MIME 類型(可選)、Base64 編碼設定(可選)和內容組成,格式如下:
data:[<mime type>][;base64],<data>
在實際使用中的代碼片段如下:
data:image/png;base64,/9j/4AAQSkZJRgAB...
Base64 會以每 6 個比特爲一個單元,對應某個字符,如果要編碼的字節數不能被 3 整除,就用 0 在末尾補足。
例如編碼 PW,最後得到的值是 UFc=,計算過程如下圖所示。
雖然使用 Data URI 減少了一次 HTTP 請求,但它會讓嵌入的文檔體積膨脹四分之三,影響瀏覽器渲染。
並且還會降低 Gzip 的壓縮效率,破壞資源的緩存。
若要使用,需要權衡利弊,儘量考慮小尺寸和低更新頻率的圖像。
二、大小
大多數頁面至少有一張超過 100 KB 的圖像,而在頁面尺寸排行中,前 10% 的頁面至少有一張接近 1 MB 或更大的圖像。
因此,壓縮或降低圖像的大小,可以顯著地提升頁面性能。
1)壓縮
圖像壓縮分爲有損和無損。
前者會改變圖像本身,減少信息量,降低圖像質量,文件無法還原,但是壓縮效率會比較高。
後者會優化數據存儲方式,利用算法描述重複信息,文件可以還原,但是壓縮效率比有損低。
在線壓縮網站 TinyPNG 採用智能有損壓縮技術對圖像進行處理,在我實際使用時,發現最高可壓縮 70% 以上的大小。
原理就是通過合併圖中相似的顏色,將 24 位的 PNG 圖像壓縮成小得多的 8 位色值的圖像,並且去掉了不必要的元數據。
經過壓縮後的圖像,人的肉眼並不會看出與原圖明顯的差異。
若是要用代碼對圖像進行壓縮,可以採用三種觸發時機。
第一種是在圖像上傳到服務器後,通過成熟的第三方 Node 庫(例如 imagemin、node-ffmpeg 等)進行壓縮處理。
第二種是在訪問圖像地址時,帶上各類參數,動態的對圖像進行壓縮或裁剪,例如 cover.png?w/100,按比例裁剪成 100 的寬度。
第三種是在構建過程中對圖像進行壓縮,壓縮後再上傳到服務器中,例如 webpack 的 ImageMinimizerPlugin 插件等。
目前市面上流行的 CDN 服務都應該會提供此類功能。
2)WebP
WebP 是由 Google 提供的一種圖像格式,支持無損和有損兩種壓縮。
官方資料表明,WebP 比 PNG 格式的圖像小 26%,比 JPEG 格式的圖像小 25~34% 。
在可以接受有損壓縮的情況下,有損 WebP 也支持透明度,並且其文件大小通常比 PNG 小 3 倍。
雖然表現如此優秀,但是 2022 年,WebP 格式的使用率只佔 8.9%,如下圖所示,GIF、JPEG 和 PNG 仍然是主流。
阻礙其推廣的一大問題是兼容性,好在目前 iOS 14 以上已經支持 WebP,不考慮 IE 的話,主流的瀏覽器都已支持 WebP 格式。
3)響應式
響應式是指根據屏幕尺寸、像素密度或其它設備特性,動態的請求最符合場景的圖像。
像素密度(PPI)就是每英寸像素,計量設備屏幕的精細程度,值越高圖像越精細,常見的屏幕有 Retina、XHDPI 等。
接下來用一個例子來演示不同尺寸的屏幕顯示不同的圖像,首先爲 img 元素聲明 srcset 屬性。
用逗號分隔多個描述字符串,每一段包含圖像地址和寬度或像素密度描述符,注意,此處的寬度是圖像的原始寬度。
然後再聲明 sizes 屬性,其值就是媒體查詢的條件和圖像的顯示寬度,最後一條描述可以省略條件,如下所示。
<img srcset="cover-small.jpg 375w, cover.jpg 2449w" sizes="(max-width: 375px) 375px, 800px" src="cover.jpg" />
cover-small.jpg 的原始寬度是 375px,cover.jpg 的原始寬度是 2449px。
當設備最大寬度是 375 時,將圖像寬度設爲 375px,在 srcset 中鎖定最接近的那張圖像的描述。
800px 是默認的圖像寬度,當無法滿足條件時,就採用這個值。
如果在做媒體查詢時不清楚各類屏幕尺寸的閾值,那麼可以參考 Bootstrap 的 Containers。
注意,若在 srcset 聲明的是像素密度,那麼就不需要再額外聲明 sizes 屬性了。
在 2022 年,srcset 屬性的使用佔比在 34%,size 屬性的使用佔比在 13%~19%。
如果要同時適配特定的屏幕尺寸和像素密度,那麼可以通過 picture 元素實現響應式。
下面是一個示例,在 source 元素中,media 是媒體查詢條件,srcset 的功能和 img 元素中的相同。
<picture> <source media="(max-width: 375px)" srcset="cover-small.jpg, cover-2x.jpg 2x" /> <img src="cover.jpg" /> </picture>
當都不符合條件時,就會採用默認的 img 元素。在 2022 年,picture 元素的使用佔比是 7.7%。
除了響應式圖像,picture 元素還可以用來選擇不同格式的圖像,如下所示。
當瀏覽器支持 WebP 時,就加載這種格式的圖像,否則就加載後面的默認圖像。
<picture> <source type="image/webp" srcset="cover.webp"> <img src="cover.jpg" /> </picture>
三、其他優化
除了上述兩類比較大的優化之外,還有一些其他的細碎優化,在此節會列舉幾個。
例如對圖像一個比較簡單而有益的優化是預設寬高,提前佔位,就能避免影響 CLS 的計算。
1)延遲解碼
圖像解碼是光柵化過程中一個比較耗時的步驟,當圖像越大時,解碼時間就越長。
那麼非合成動畫(即非 CSS3 動畫)就有可能因主線程被阻塞而卡頓。值得一提的是,CSS3 動畫運行在合成線程中,所以不會受其影響。
HTMLImageElement.decode() 方法可以確保圖像解碼後,再將圖像添加到 DOM 中,如下所示。
const img = new Image(); img.src = "cover.jpg"; img.decode().then(() => { document.body.appendChild(img); });
2)失敗處理
在圖像請求失敗時,對頁面的交互並不會造成影響,但是圖像會裂開,在視覺體驗上比較糟糕。
爲 img 元素註冊 error 事件,就能在錯誤時做糾正處理。
document.querySelector("img").addEventListener("error", function () { this.src = "../assets/img/cover-small.jpg"; });
不過,若要想知道究竟是什麼原因的錯誤,目前還無法做到。
總結
本文對圖像的優化進行了系統性的梳理,首先是對請求做優化。
爲了更科學的對圖像進行請求,列出了懶加載、預加載和 Data URI 三種優化方法。
然後對尺寸做優化,講解了壓縮細節,WebP 格式的特點,以及響應式處理的妙用。
最後再介紹了幾個同樣也能優化圖像的方法,包括佔位、延遲解碼和失敗處理。