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 格式的特點,以及響應式處理的妙用。
最後再介紹了幾個同樣也能優化影象的方法,包括佔位、延遲解碼和失敗處理。