HTTP Archive 在 2022 年關于[多媒體的報告](https://almanac.httparchive.org/en/2022/media)中指出,目前大概有 99.9% 的網站或多或少都會包含點圖像。
  并且高達 70% 的移動頁面和 80% 的桌面頁面的 LCP 指標會受圖像的影響。
  2023-11-27 據統計,合理使用圖像能減少 20% 的帶寬。
  通過這些數據可知,圖像在網頁中占據著舉足輕重的地位,優化圖像,對于網頁性能可以達到立竿見影的效果。
  優化的核心是控制圖像的尺寸,提前、延遲或減少圖像的請求,以及降低對核心 Web 指標的影響。
  本文所用的示例代碼已上傳至[Github](https://github.com/pwstrick/pe)。
## 一、請求
  以我目前的公司為例,活動頁中圖像的請求數占比最高可達 64%。
  若不做優化處理,那么將直接拉長頁面的白屏時間,體驗將會及其糟糕。
**1)懶加載**
  懶加載就是延遲請求的時機,在觸發某個特定條件后,再請求。
  常用的條件是當圖像出現在屏幕內時,觸發請求。
  當頁面很長時,并不需要在頁面首屏加載時就請求所有圖像,而是滾動到圖像所在位置后,再將圖像顯示,如下圖所示。
:-: 
  目前有 3 種方式來實現懶加載。那么在正式講解之前,需要先了解一下視口的概念。
  視口(viewport)就是下圖中的灰色部分,也就是文檔內容的可視區域,圖中用粗線框住的是瀏覽器的外殼部分(如標簽頁、書簽欄、調試工具等)。
:-: 
  先來講解第 1 種懶加載:傳統的 JavaScript 實現,原理就是計算圖像頂部到視口頂部的距離,包括頁面隱藏部分。
  若此距離大于等于當前滾動條的位置(即可視區域),那么就可以認為滿足條件,需要顯示圖像。
  假設滾動條是在body中,那么當前可視區域的范圍如下所示。
~~~
const viewTop = window.pageYOffset;
const viewBottom = window.innerHeight + viewTop;
~~~
  window.[pageYOffset](https://developer.mozilla.org/en-US/docs/Web/API/Window/pageYOffset)表示視口上邊的距離,如果沒有出現垂直方向的滾動條,那么對應屬性的值為 0。window.[innerHeight](https://developer.mozilla.org/en-US/docs/Web/API/Window/innerHeight)表示視口的高度。
  而圖像到視口頂部的距離可以通過[getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)的 DOMRect.top 屬性得到,如下所示。
~~~
const nodeTop = node.getBoundingClientRect().top + viewTop;
~~~
  完整的代碼如下所示, blank.gif 是一張 1\*1 的空白占位圖,data-src 是真實的圖像地址,[scroll](https://developer.mozilla.org/en-US/docs/Web/API/Window/scroll)是滾動條事件。
~~~html
<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](https://github.com/callmecavs/layzr.js)。
  接下來講解第 2 種通過[Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)實現懶加載。
  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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading)屬性。該屬性會指示瀏覽器當圖像不在可視區域時的加載方式。
  這種方式相比較前兩者,最為簡潔,不需要編寫額外的腳本,示例代碼如下所示。
~~~html
<img src="cover.jpg" loading="lazy" width="300" height="400"/>
~~~
  2022 年有 91.47% 的瀏覽器已支持[loading](https://caniuse.com/loading-lazy-attr)屬性,并且大概有 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 或更大的圖像。
  因此,壓縮或降低圖像的大小,可以顯著地提升頁面性能。
  2023-11-20 注意,預定義圖像的寬和高,就可以在頁面加載時為圖像預留空間。否則當圖像加載時,頁面的布局就會發生變化。
**1)壓縮**
  圖像壓縮分為有損和無損。
  前者會改變圖像本身,減少信息量,降低圖像質量,文件無法還原,但是壓縮效率會比較高。
  后者會優化數據存儲方式,利用算法描述重復信息,文件可以還原,但是壓縮效率比有損低。
  在線壓縮網站[TinyPNG](https://tinypng.com/)采用智能有損壓縮技術對圖像進行處理,在我實際使用時,發現最高可壓縮 70% 以上的大小。
  原理就是通過合并圖中相似的顏色,將 24 位的 PNG 圖像壓縮成小得多的 8 位色值的圖像,并且去掉了不必要的元數據。
  經過壓縮后的圖像,人的肉眼并不會看出與原圖明顯的差異。
  若是要用代碼對圖像進行壓縮,可以采用三種觸發時機。
* 第一種是在圖像上傳到服務器后,通過成熟的第三方 Node 庫(例如[imagemin](https://github.com/imagemin)、[node-ffmpeg](https://github.com/damianociarla/node-ffmpeg)、[sharp](https://github.com/lovell/sharp)等)進行壓縮處理。
* 第二種是在訪問圖像地址時,帶上各類參數,動態的對圖像進行壓縮或裁剪,例如 cover.png?w/100,按比例裁剪成 100 的寬度,高度自適應。
* 第三種是在構建過程中對圖像進行壓縮,壓縮后再上傳到服務器中,例如 webpack 的?[ImageMinimizerPlugin](https://webpack.js.org/plugins/image-minimizer-webpack-plugin/)插件等。
  2023-11-20 動態處理除了支持裁剪、壓縮之外,還可以進行格式轉換、漸進顯示(從模糊到清晰)等操作。
  因為處理需要花費些時間,所以一般在處理之后,就會做持久化的存儲,下次訪問相同參數時,就直接返回處理后的圖像。
  目前市場上成熟的 CDN 服務,都會提供相關的圖像處理功能,若要自己開發,可以將其作為參考。
**2)WebP**
  WebP 是由 Google 提供的一種圖像格式,支持無損和有損兩種壓縮。
  官方資料表明,[WebP](https://developers.google.com/speed/webp?hl=zh-cn)比 PNG 格式的圖像小 26%,比 JPEG 格式的圖像小 25~34% 。
  在可以接受有損壓縮的情況下,有損 WebP 也支持透明度,并且其文件大小通常比 PNG 小 3 倍。
  雖然表現如此優秀,但是 2022 年,WebP 格式的使用率只占 8.9%,如下圖所示,GIF、JPEG 和 PNG 仍然是主流。
:-: 
  阻礙其推廣的一大問題是[兼容性](https://caniuse.com/?search=webp),好在目前 iOS 14 以上已經支持 WebP,不考慮 IE 的話,主流的瀏覽器都已支持 WebP 格式。
**3)響應式**
  響應式是指根據屏幕尺寸、像素密度或其它設備特性,動態的請求最符合場景的圖像。
  像素密度(PPI)就是每英寸像素,計量設備屏幕的精細程度,值越高圖像越精細,常見的屏幕有 Retina、XHDPI 等。
  接下來用一個例子來演示不同尺寸的屏幕顯示不同的圖像,首先為 img 元素聲明[srcset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-srcset)屬性。
  用逗號分隔多個描述字符串,每一段包含圖像地址和寬度或像素密度描述符,注意,此處的寬度是圖像的原始寬度。
  然后再聲明 sizes 屬性,其值就是媒體查詢的條件和圖像的顯示寬度,最后一條描述可以省略條件,如下所示。
~~~html
<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](https://getbootstrap.com/docs/5.3/layout/containers/)。
  注意,若在 srcset 聲明的是像素密度,那么就不需要再額外聲明 sizes 屬性了。
  在 2022 年,srcset 屬性的使用占比在 34%,size 屬性的使用占比在 13%~19%。
  如果要同時適配特定的屏幕尺寸和像素密度,那么可以通過[picture](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)元素實現響應式。
  下面是一個示例,在[source](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source)元素中,media 是媒體查詢條件,srcset 的功能和 img 元素中的相同。
~~~html
<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 時,就加載這種格式的圖像,否則就加載后面的默認圖像。
~~~html
<picture>
<source type="image/webp" srcset="cover.webp">
<img src="cover.jpg" />
</picture>
~~~
**4)切片**
  2023-11-20 圖像切片是指將一張幾百 M 或幾個 G 的大圖分割成若干個小圖的過程,以便于存儲和處理,常用于網絡地圖、圖像拼接等應用中。
  在日常拖動地圖時,就會看到新拖出的部分,會從幾個純色方塊瞬間變成某塊地圖,這其實是一種提升顯示效率和縮短用戶等待的手段。
:-: 

  切片的幾個主要步驟包括:定義切片大小、計算切片數量、切割圖像、存儲切片和加載切片。
## 三、其他優化
  除了上述兩類比較大的優化之外,還有一些其他的細碎優化,在此節會列舉幾個。
  例如對圖像一個比較簡單而有益的優化是預設寬高,提前占位,就能避免影響 CLS 的計算。
**1)延遲解碼**
  圖像解碼是光柵化過程中一個比較耗時的步驟,當圖像越大時,解碼時間就越長。
  那么非合成動畫(即非 CSS3 動畫)就有可能因主線程被阻塞而卡頓。值得一提的是,CSS3 動畫運行在合成線程中,所以不會受其影響。
  HTMLImageElement.[decode()](https://developer.mozilla.org/en-US/docs/Web/API/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";
});
~~~
  不過,若要想知道究竟是什么原因的錯誤,目前還無法做到。
**3)漸進JPEG**
  2023-11-20 JPEG 格式的圖像在呈現的時候,有兩種方式,一種是自上而下掃描式,還有一種就是先呈現模糊圖像,然后逐漸清晰。
  日常看漫畫的時候,因為圖像都會比較大,所以被漸進式的加載會比較明顯。
:-: 
  PS 軟件支持將導出的圖像以漸進顯示。據國外某位大神研究得出,漸進式圖像在瀏覽器中的加載會更快。
**4)多域名請求**
  2023-11-20 瀏覽器的并發請求數目限制是針對同一域名的。
  同一時間針對同一域名下的請求有一定數量限制,超過限制數目的請求會被阻塞。
:-: 
  所以要同時請求大量圖像,就可以為圖像配置多個域名,繞開瀏覽器的并發限制。
  接下來做一個對比,分別是一個域名和兩個域名,分別加載圖片。
:-: 
  當一個域名的時候最多只能并發6個請求,而兩個域名的時候能并發10個請求。
:-: 
## 總結
  本文對圖像的優化進行了系統性的梳理,首先是對請求做優化。
  為了更科學的對圖像進行請求,列出了懶加載、預加載和 Data URI 三種優化方法。
  然后對尺寸做優化,講解了壓縮細節,WebP 格式的特點,以及響應式處理的妙用。
  最后再介紹了幾個同樣也能優化圖像的方法,包括占位、延遲解碼和失敗處理。
*****
> 原文出處:
[博客園-前端性能精進](https://www.cnblogs.com/strick/category/2267607.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1610941255021780992)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020