之前,整個《現代圖片性能優化及體驗優化指南》分了 5 篇來發,本文是系列合集,方便大家收藏及連貫閱讀。 圖片資源,在我們的業務中可謂是占據了非常大頭的一環,尤其是其對帶寬的消耗是十分巨大的。 對圖片的性能優化及體驗優化在今天就顯得尤為重要。本文,就將從各個方面闡述,在各種新特性滿頭飛的今天,我們可以 ...
之前,整個《現代圖片性能優化及體驗優化指南》分了 5 篇來發,本文是系列合集,方便大家收藏及連貫閱讀。
圖片資源,在我們的業務中可謂是占據了非常大頭的一環,尤其是其對帶寬的消耗是十分巨大的。
對圖片的性能優化及體驗優化在今天就顯得尤為重要。本文,就將從各個方面闡述,在各種新特性滿頭飛的今天,我們可以如何儘可能的對我們的圖片資源,進行性能優化及體驗優化。
圖片類型的選取及 Picture 標簽的使用
首先,從圖片的類型上而言,除了常見的 PNG-8/PNG-24,JPEG,GIF 之外,我們更多的關註另外幾個較新的圖片格式:
- WebP
- JPEG XL
- AVIF
首先,通過一張表格,快速過一下這幾個圖片,我們將從圖片類型、透明通道、動畫、編解碼性能、壓縮演算法、顏色支持、記憶體占用、相容性方面,對比它們:
圖片類型 | Alpha 通道 | 動畫 | 編解碼性能 | 壓縮演算法 | 顏色支持 | 記憶體占用 | 相容性 |
---|---|---|---|---|---|---|---|
GIF | 支持 | 支持 | 較高 | 無損壓縮 | 索引色(256) | 基本一致 | ALL |
PNG-8/PNG-24 | 支持 | 不支持 | 較高 | 無損壓縮 | 索引色(256)\直接色 | 基本一致 | ALL |
JPEG | 不支持 | 不支持 | 較高 | 有損壓縮 | 直接色 | 基本一致 | ALL |
WebP | 支持 | 支持 | 編解碼性能差(低配設備更為顯著) | 有損壓縮\無損壓縮 | 直接色 | 基本一致 | 高版本 Chrome\Opera\Android |
JPEG XL | 支持 | 支持 | 漸進式解碼 | 有損壓縮\無損壓縮 | 直接色 | 基本一致 | 部分高版本 Chrome\Opera\Firefox\Edge |
AVIF | 支持 | 支持 | 編解碼性能一般 | 有損壓縮\無損壓縮 | 直接色 | 基本一致 | 高版本 Chrome\Opera\Android\Edge |
首先,瞭解瞭解上述的一些參數含義:
- Alpha 通道:圖片是否支持透明的特性
當然,需要指出的是,Alpha 沒有透明度的意思,不代表透明度。opacity 和 transparency 才和透明度有關,前者是不透明度,後者是透明度。比如 css 中的「opacity: 0.5」就是設定元素有 50% 的不透明度。後來 Alvy Ray Smith 提出每個像素再增加一個 Alpha 通道,取值為0到1,用來儲存這個像素是否對圖片有「貢獻」,0代表透明、1代表不透明。也就是說,「Alpha 通道」儲存一個值,其外在表現是「透明度」,Alpha 和透明度沒啥關係
- 動畫:很好理解,圖片是否支持多幀率動態圖片,類似於 GIF
- 編解碼性能:圖像的解碼與編碼。這個很關鍵,很多人對待圖片容易忽視圖片的編解碼性能,解碼圖像主要從圖像文件中讀出圖像數據,而編碼則是將圖像數據寫入圖像文件。解碼與編碼的過程正好相反。而這兩者的性能耗時會影響我們頁面的的展示性能。
- 壓縮演算法:該圖片格式是否支持壓縮,支持的話,圖片的壓縮又會分為無損壓縮與有損壓縮
有損壓縮演算法是一種數據壓縮方法,經過此方法壓縮、解壓的數據會與原始數據不同但是非常接近。原理是藉由將次要的信息數據捨棄,犧牲一些質量來減少數據量、提高壓縮比
無損壓縮指數據經過壓縮後,信息不受損失,還能完全恢復到壓縮前的原樣。無損壓縮通常用於嚴格要求“經過壓縮、解壓縮的數據必須與原始數據一致”的場合。
- 顏色支持:會分為索引色與直接色,在過去,為了節省存儲空間,並非所有圖片都能支持所有顏色值,因此存在索引色這種優化方式。
索引顏色是一種以有限的方式管理數字圖像顏色的技術,以節省電腦記憶體和文件存儲,同時加速顯示刷新和文件傳輸。即用一個數字來代表(索引)一種顏色,在存儲圖片的時候,存儲一個數字的組合,同時存儲數字到圖片顏色的映射。這種方式只能存儲有限種顏色。索引色常見有1位(即黑白),8位(即灰階/256色),16位(即高彩),24位(即真彩),30/36/48位(即全彩)。
直接色使用四個數字來代表一種顏色,這四個數字分別代表這個顏色中紅色、綠色、藍色以及透明度(即 RGBA)。現在流行的顯示設備可以在這四個維度分別支持256種變化,所以直接色可以表示2的32次方種顏色。
- 記憶體占用:圖片對記憶體資源的占用
- 相容性:影響圖片格式能否大規模推廣的核心要素之一
WebP vs JPEG XL vs AVIF: JPEG 替代之戰
因為傳統的 PNG-8/PNG-24,JPEG,GIF 各自或多或少都存在一些問題,近些年來它們的替代方案之爭也愈演愈烈,核心領跑者可能是 WebP、JPEG XL、AVIF。
再簡單瞭解瞭解它們:
WebP
WebP 最初由 Google 在 2010 年 9 月發佈,其特性總結如下:
- 可以同時提供無損/有損壓縮(像 JPEG 一樣)和支持透明度(像 PNG 一樣)的圖片文件格式
- 支持動畫效果(像 GIF 一樣)
- WebP 主要優勢在於有損編碼,其無損編碼的性能和壓縮比表現一般
- WebP 的缺點在於其編解碼性能不是特別理想
- 在相容性方面,除了 IE,基本已經得到了全系列瀏覽器支持
對於複雜的圖像(比如照片)來說,WebP 無損編碼表現並不好,但有損編碼表現卻非常棒。相近質量的圖片解碼速度 WebP 相距 JPEG 也已經相差不大了,而文件壓縮比卻能提升不少。
下圖是我之前還在 TX 的時候做的一個測試對比:
載入同樣張數的 JPEG 與 WebP 的耗時對比:
對於 WebP 圖片格式,簡單做個總結:
- 目前 WebP 與 JPEG 相比較,據資料考證,編碼速度慢 10 倍,解碼速度慢 1.5 倍
- WebP 雖然會增加額外的解碼時間,但由於大幅減少了文件體積,縮短了載入的時間,大頁面圖片量較多的場景下,頁面的渲染速度是有較大加快的
- 目前而言,是 WebP、JPEG XL、AVIF 三者中相容性最好的
截止至(2023-02-05)的相容性圖:
JPEG XL
JPEG XL 由聯合圖像專家組(開發原始 JPEG 標準的同一組織)於 2021 年發佈,旨在成為傳統 JPEG 的長期替代品。作為一種免版稅的開源標準,JPEG XL 的創建者希望其格式的開放性能夠吸引網路開發人員採用該標準,該格式的擴展名為 .jxl
,JXL 核心比特流於 2021 年 1 月凍結,文件格式於 2021 年 4 月定稿。:
JPEG XL 中的 X 指 2000 年以來的多個 JPEG 標準的名稱:JPEG XT、JPEG XR、JPEG XS,而 L 表示 'long-term',表示“長期”。創建這種格式是為替換舊的JPEG文件格式,並使用足夠長的時間。
其主要特點有:
- 與傳統圖像格式(例如JPEG、GIF和PNG)相比,有著更佳的效率與更豐富的功能
- 全面支持廣色域和 HDR,支持 Alpha 通道,支持多幀(也就是動畫支持)
- 有損壓縮時:相同的視覺質量,比 JPEG 小約 60%。
- 無損壓縮時:比 PNG 減小約 35%(對於 HDR,減小 50%)。
- 支持無損 JPEG 轉碼,減小約 20% 文件大小。
- 漸進式解碼,專為支持不同顯示解析度的響應式載入
- 開源免費:具有使用三條款版BSD許可證的開源參考實現的免版稅格式
看看同一張圖片,相同質量下的大小表現:
JPEG XL 是目前而言,最有可能全面替代傳統圖片格式(Gif、PNG、JPEG)的下一代標準,當然,在今天,需要看看其相容性:
好吧,目前的相容有點離譜。Chrome 從 91 版本開始已經實驗室性質支持了 .jxl
格式的圖片,需要通過 --enable-features=JXL
配置開啟,遺憾的是,從 Chrome 110 開始,Chrome 又不再支持 JPEG XL 。
有趣的是,Chrome 從 110 版本開始中棄用了對 JPEG-XL 的支持,谷歌的回答是,人們對 JPEG-XL 沒有足夠的興趣,並且與現有格式相比也沒有足夠的優勢。谷歌之前一直對 JPEG 的支持都是實驗性的性質的,他們認為 JPEG XL 缺乏生態系統支持,並且該格式沒有足夠多的好處(相對 WebP 和 AVIF)。也就是說,目前而言,Chrome 對 WebP 和 AVIF 等替代格式更感興趣。
AVIF
最後,我們再來看看 AVIF 格式圖片。
AVIF 是由開放媒體聯盟 (AOM) 開發並於 2019 年發佈的另一種最新圖像格式。該格式基於 AV1 視頻編解碼器,源自視頻幀。其特點如下:
- 同樣的,與傳統圖像格式(例如JPEG、GIF和PNG)相比,有著更佳的效率與更豐富的功能
- 支持 Alpha 通道,支持動態圖像和動畫
- 支持有損、無損壓縮。AVIF 文件在低保真有損圖像壓縮方面表現出色(比 JPEG XL 更優)。壓縮的 AVIF 圖像保留了很高的圖片質量,避免了惱人的壓縮偽影等問題
- 相對而言,AVIF 的解碼和編碼速度不如 JPEG XL,它不支持漸進式渲染
- 最後,再看看相容性,目前(2023-02-05)它的相容性介於 WebP 與 JPEG XL 之間
看看 CaniUse 的數據:
下圖是 WebP vs JPEG XL vs AVIF 三者在圖片解碼上的性能表現:
從圖中可以看到,對於解碼性能的對比,結果居然是 WebP > AVIF > JPEG XL 。JPEG XL 的編解碼性能並沒有其描述的那麼強大。
圖片格式總結
總結一下,WebP、AVIF 和 JPEG XL 都是瀏覽器不廣泛支持的新型圖像格式。雖然 WebP、AVIF 已經存在很長時間,但到今天,影響它們大規模使用的依舊是相容問題。它們各自有各自的特點與優勢,誰能勝出仍未知曉。
雖然 AVIF、JPEG XL 等新型圖片格式未得到任何瀏覽器的完全支持,但是在新版本的 Chrome、Firefox 和 Edge Chromium,可以使用配置標誌啟用對應圖像格式,配合 HTML 的 Picture
標簽,我們還是可以一定程度上對我們的圖片進行格式選擇上的優化的。
這,就可以引出我們要說的第二部分 -- HTML Picture 標簽的使用。
Picture 元素的使用
HTML5 規範新增了 Picture Element。那麼 <picture>
元素的作用是什麼呢?
<picture>
元素通過包含零或多個 <source>
元素和一個 <img>
元素來為不同的顯示/設備場景提供圖像版本。瀏覽器會選擇最匹配的子 <source>
元素,如果沒有匹配的,就選擇 <img>
元素的 src 屬性中的 URL。然後,所選圖像呈現在 <img>
元素占據的空間中。
什麼意思呢?怎麼使用 <picture>
元素呢?
假設,沒有 <picture>
,只有 <img>
元素,我們想儘可能在支持一些現代圖片格式的瀏覽器上使用類似於上述我們提到的 WebP、AVIF 和 JPEG XL 等圖片格式,而不支持的瀏覽器回退使用常規的 JPEG、PNG 等。沒錯,就是一種漸進增強的思想,該怎麼辦呢?
只能是 JavaScript 去寫對應的邏輯,通過 JS 腳本進行特性查詢,動態賦值給 <img>
的 src。
而有了 <picture>
後,瀏覽器將原生支持上述的一些列操作,我們來看看對應的語法:
<picture>
<!-- 可能是一些對相容性有要求的,但是性能表現更好的現代圖片格式-->
<source src="image.avif" type="image/avif">
<source src="image.jxl" type="image/jxl">
<source src="image.webp" type="image/webp">
<!-- 最終的兜底方案-->
<img src="image.jpg" type="image/jpeg">
</picture>
上述代碼的含義是:
- 第 1 個
source
元素指向新 AVIF 格式的圖像。如果瀏覽器支持 AVIF 圖像,那麼它會選擇該圖像文件。否則,它將移動到下一個source
元素。 - 第 2個
source
元素指向新 JPEG XL 格式的圖像。如果瀏覽器支持 JPEG XL 圖像,那麼它會選擇該圖像文件。否則,它將移動到下一個source
元素。 - 第 3 個
source
元素指向一張WebP 格式的圖像。如果瀏覽器能夠渲染 WebP 圖像,它將使用該圖像文件。 - 否則瀏覽器將回退到使用
img
元素 src 屬性中的圖像文件。img 元素指向的是 JPEG 格式的圖片,它是最終的兜底方案。
這意味著現在我們可以在不犧牲向後相容性的情況下開始使用新的圖像格式。
簡而言之,<picture>
元素的作用:
- 通過
<source>
給出一系列對相容性有所要求的現代圖片格式選項 - 通過
<img>
給出兜底的高相容性圖片格式選項 - 瀏覽器通過對給出的圖片格式做特性檢測,要決定載入哪個 URL,user agent 檢查每個
<source>
的 srcset、media 和 type 屬性,來選擇最匹配頁面當前佈局、顯示設備特征等的相容圖像。 - 最終,所選圖像呈現在
<img>
元素占據的空間中
模塊總結
總結一下,本文對常見的圖片格式以及最新的幾種未被大規模相容的圖片格式進行的對比,它們分別是:
- PNG-8/PNG-24
- JPEG
- GIF
- WebP
- JPEG XL
- AVIF
其後,著重介紹了 3 種現代圖片格式:WebP、JPEG XL、AVIF。相對於 JPEG 等傳統格式,它們在色彩表現、動畫支持、是否支持無損有損壓縮、壓損比率、編解碼性能上有著更進一步的提升,正在成為下一階段 Web 圖像的標準。
最後,介紹了 <picture>
元素,藉助它,我們能更好的實現圖片的漸進增強。
適配不同的屏幕尺寸及 DPR
下一個模塊,我們來看看圖片資源如何更好的適配不同的屏幕尺寸。
這裡首先會涉及一個預備知識,屏幕的 DPR 值,那麼,什麼是 DPR 呢?要瞭解 DPR,又需要知道什麼是設備獨立像素 以及 物理像素。
設備獨立像素
以 iPhone6/7/8為例,這裡我們打開 Chrome 開發者工具:
這裡的 375 * 667
表示的是什麼呢,表示的是設備獨立像素(DIP),也可以理解為 CSS 像素,也稱為邏輯像素:
設備獨立像素 = CSS 像素 = 邏輯像素
如何記憶呢?這裡使用 CSS 像素來記憶,也就是說。我們設定一個寬度為 375px 的 div,剛好可以充滿這個設備的一行,配合高度 667px ,則 div 的大小剛好可以充滿整個屏幕。
物理像素
OK,那麼,什麼又是物理像素呢。我們到電商網站購買手機,都會看一看手機的參數,以 JD 上的 iPhone7 為例:
可以看到,iPhone7 的解析度是 1334 x 750
,這裡描述的就是屏幕實際的物理像素。
物理像素,又稱為設備像素。顯示屏是由一個個物理像素點組成的,1334 x 750
表示手機分別在垂直和水平上所具有的像素點數。通過控制每個像素點的顏色,就可以使屏幕顯示出不同的圖像,屏幕從工廠出來那天起,它上面的物理像素點就固定不變了,單位為pt。
設備像素 = 物理像素
DPR(Device Pixel Ratio) 設備像素比
OK,有了上面兩個概念,就可以順理成章引出下一個概念。DPR(Device Pixel Ratio) 設備像素比,這個與我們通常說的視網膜屏(多倍屏,Retina屏)有關。
設備像素比描述的是未縮放狀態下,物理像素和設備獨立像素的初始比例關係。
簡單的計算公式:
DPR = 物理像素 / 設備獨立像素
我們套用一下上面 iPhone7 的數據(取設備的物理像素寬度與設備獨立像素寬度進行計算):
iPhone7’s DPR = iPhone7’s 物理像素寬度 / iPhone7's 設備獨立像素寬度 = 2
750 / 375 = 2
或者是 1334 / 667 = 2
可以得到 iPhone7 的 dpr 為 2。也就是我們常說的視網膜屏幕。
視網膜(Retina)屏幕是蘋果公司"發明"的一個營銷術語。 蘋果公司將 dpr > 1
的屏幕稱為視網膜屏幕。
在視網膜屏幕中,以 dpr = 2 為例,把 4(2x2) 個像素當 1 個像素使用,這樣讓屏幕看起來更精緻,但是元素的大小本身卻不會改變:
OK,我們再來看看 iPhone XS Max:
它的物理像素如上圖是 2688 x 1242
,
它的 CSS 像素是 896 x 414
,很容易得出 iPhone XS Max 的 dpr 為 3。
為不同 DPR 屏幕,提供恰當的圖片
那麼,DPR 和圖片適配有什麼關係呢?
舉個例子,同樣的 CSS 像素大小下,屏幕如果有不同 DPR,同樣大小的圖片渲染出來的效果不盡相同。
我們以 dpr = 3
的手機為例子,在 300 x 389
CSS 像素大小的範圍內,渲染 1倍/2倍/3倍 圖的效果如下:
實際圖片所占的物理像素為 900 x 1167。
可以看到,在高 DPR 設備下提供只有 CSS 像素大小的圖片,是非常模糊的。
因此,為了在不同的 DPR 屏幕下,讓圖片看起來都不失真,我們需要為不同 DPR 的圖片,提供不同大小的圖片。
那麼,有哪些可行的解決方案呢?
方案一:無腦多倍圖
假設,在移動端假設我們需要一張 CSS 像素為 300 x 200
的圖像,考慮到現在已經有了 dpr = 3 的設備,那麼要保證圖片在 dpr = 3 的設備下也正常高清展示,我們最大可能需要一張 900 x 600
的原圖。
這樣,不管設備的 dpr 是否為 3,我們統一都使用 3 倍圖。這樣即使在 dpr = 1,dpr = 2 的設備上,也能非常好的展示圖片。
當然這樣並不可取,會造成大量帶寬的浪費。
現代瀏覽器,提供了更好的方式,讓我們能夠根據設備 dpr 的不同,提供不同尺寸的圖片。
方案二:媒體查詢
方案二,我們可以考慮使用媒體查詢。到今天,我們可以通過相應的媒體查詢,得知當前的設備的 DPR 值,這樣,我們就可以在對應的媒體查詢中,使用對應的圖片。
像是這樣:
#id {
background: url([email protected])
}
@media (device-pixel-ratio: 2) {
#id {
background: url([email protected])
}
}
@media (device-pixel-ratio: 3) {
#id {
background: url([email protected])
}
}
這個方案的缺點在於:
- 要寫的代碼可能太多了,而且,可能存在一些介於 12,23 之間的 DPR 值,不好窮舉出所有場景
- 需要註意語法需要的相容性,需要添加首碼,譬如
-webkit-min-device-pixel-ratio
,當然這個可以由autoprefixer
輔助解決
方案三:CSS 配合 image-set 語法
image-set
屬於 CSS background 中的一種語法,image-set()
函數為設備提供最合適的圖像解析度,它提供一組圖像選項,每個選項都有一個相關的 DPR 聲明,瀏覽器將從中選擇最適合設備的圖像進行設置。
什麼意思呢,來看看代碼:
.img {
/* 不支持 image-set 的瀏覽器*/
background-image: url('../[email protected]');
/* 支持 image-set 的瀏覽器*/
background-image: image-set(
url('./[email protected]') 2x,
url('./[email protected]') 3x
);
}
這樣一看,作用應該很清晰了。對於支持 image-set
語法的瀏覽器:
- 如果其設備對應的 DPR 為 2,會選取這條
url('./[email protected]') 2x
記錄,也就是最終生效的 URL 是'./[email protected]'
; - 如果其設備對應的 DPR 為 3,會選取這條
url('./[email protected]') 3x
記錄,也就是最終生效的 URL 是'./[email protected]'
;
其中的 2x
,3x
就是用於匹配 DRP的。
使用 image-set
的一些痛點與媒體查詢方案類似。代碼量與相容性語法,而且難以匹配所有情況。
方案四:srcset 配合 1x 2x 像素密度描述符
簡單來說,srcset 可以根據不同的 dpr 拉取對應尺寸的圖片:
<div class='illustration'>
<img src='illustration-small.png'
srcset='images/illustration-small.png 1x,
images/illustration-big.png 2x'
>
</div>
上面 srcset
里的 1x,2x 表示 像素密度描述符,表示
- 當屏幕的 dpr = 1 時,使用
images/illustration-small.png
這張圖 - 當屏幕的 dpr = 2 時,使用
images/illustration-big.png
這張圖 - 如果不支持
srcset
語法,src='illustration-small.png'
將會是最終的兜底方案
方案五:srcset 屬性配合 sizes 屬性 w 寬度描述符
上面 1x,2x 的寫法比較容易接受易於理解。
但是,上述 3 種方案都存在統一的問題,只考慮了 DPR,但是忽略了響應性佈局的複雜性與屏幕的多樣性。
因此,規範還推出了一種方案 -- srcset 屬性配合 sizes 屬性 w 寬度描述符。
srcset
屬性還有一個 w 寬度描述符,配合 sizes
屬性一起使用,可以覆蓋更多的面。
sizes
屬性怎麼理解呢?它定義圖像元素在不同的視口寬度時,可能的大小值。
以下麵這段代碼為例子:
<img
sizes = “(min-width: 600px) 600px, 300px"
src = "photo.png"
srcset = “[email protected] 300w,
[email protected] 600w,
[email protected] 1200w,
>
解析一下:
sizes = “(min-width: 600px) 600px, 300px"
的意思是:
- 如果屏幕當前的 CSS 像素寬度大於或者等於 600px,則圖片的 CSS 寬度為 600px
- 反之,則圖片的 CSS 寬度為 300px
也就是 sizes 屬性聲明瞭在不同寬度下圖片的 CSS 寬度表現。這裡可以理解為,大屏幕下圖片寬度為 600px,小屏幕下圖片寬度為 300px。
需要註意的是,這裡大屏、小屏下圖片具體的寬度表現,還是需要藉助媒體查詢代碼,經由 CSS 實現的
srcset = “[email protected] 300w, [email protected] 600w, [email protected] 1200w
裡面的 300w,600w,900w 叫寬度描述符。
那麼,怎麼確定當前場景會選取哪張圖片呢?
當前屏幕 dpr = 2 ,CSS 寬度為 375px。
當前屏幕 CSS 寬度為 375px,則圖片 CSS 寬度為 300px。分別用上述 3 個寬度描述符的數值除以 300。
- 300 / 300 = 1
- 600 / 300 = 2
- 1200 / 300 = 4
上面計算得到的 1、 2、 4 即是算出的有效的像素密度,換算成和 x 描述符等價的值 。這裡 600w 算出的 2 即滿足 dpr = 2 的情況,選擇此張圖。
當前屏幕 dpr = 3 ,CSS 寬度為 414px。
當前屏幕 CSS 寬度為 414px,則圖片 CSS 寬度仍為 300px。再計算一次:
- 300 / 300 = 1
- 600 / 300 = 2
- 1200 / 300 = 4
因為 dpr = 3,2 已經不滿足了,則此時會選擇 1200w 這張圖。
當前屏幕 dpr = 1 ,CSS 寬度為 1920px。
當前屏幕 CSS 寬度為 1920px,則圖片 CSS 寬度變為了 600px。再計算一次:
- 300 / 600 = .5
- 600 / 600 = 1
- 1200 / 600 = 2
因為 dpr = 1,所以此時會選擇 600w 對應的圖片。
具體的可以試下這個 Demo:CodePen Demo -- srcset屬性配合w寬度描述符配合sizes屬性
此方案的意義在於考慮到了響應性佈局的複雜性與屏幕的多樣性,利用上述規則,可以一次適配 PC 端大屏幕和移動端高清屏,一箭多雕。
嗯,總結一下,在實現響應式圖像時,我們同時使用 srcset
和 sizes
屬性。它們的作用是:
srcset
:定義多個不同寬度的圖像源,讓瀏覽器在 HTML 解析期間選擇最合適的圖像源sizes
:定義圖像元素在不同的視口寬度時,可能的大小值
有了這些屬性後,瀏覽器就會根據 srcset/size 來創建一個解析度切換器的響應式圖片,可以在不同的解析度的情況下,提供相同尺寸的圖像,或者在不同的視圖大小的情況下,提供不同尺寸大小的圖像。
模塊總結
本章節一共列舉了 5 種實現響應式圖片,適配不同屏幕大小,不同 DPR 的方式,它們分別是:
- 無腦多倍圖的方式
- DRP 媒體查詢
- CSS Background 中的使用
image-set
- srcset 配合 1x 2x 像素密度描述符
- srcset 屬性配合 sizes 屬性 w 寬度描述符
合理使用它們,可以有效的為不同屏幕,提供最為恰當的圖片資源,在保證用戶體驗的同時,儘可能節省帶寬。
它們各有優缺點,可以根據自己實際的業務場景,選取合適相對成本最低的方案,並且適當的配合 Autoprefixer 以及一些 PostCSS 等工具,簡化代碼量。
圖片的寬高比、裁剪與縮放
OK,下麵進入到我們的第三個模塊,圖片的寬高比、裁剪與縮放。我們會介紹 4 個新的特性:
aspect-ratio
object-fit
object-position
image-rendering
使用 aspect-ratio
避免佈局偏移
很多時候,只能使用固定尺寸大小的圖片,我們的佈局可能是這樣:
對應的佈局:
<ul class="g-container">
<li>
<img src="http://placehold.it/150x100">
<p>圖片描述</p>
</li>
</ul>
ul li img {
width: 150px;
}
當然,萬一假設後端介面出現一張非正常大小的圖片,上述不加保護的佈局就會出問題:
所以對於圖片,我們總是建議同時寫上高和寬,避免因為圖片尺寸錯誤帶來的佈局問題:
ul li img {
width: 150px;
height: 100px;
}
同時,給 <img>
標簽同時寫上高寬,可以在圖片未載入之前提前占住位置,避免圖片從未載入狀態到渲染完成狀態高寬變化引起的重排問題。
當然,到今天,我們還可以使用 aspect-ratio
設定圖片的高寬比。
aspect-ratio
CSS 屬性為容器規定了一個期待的寬高比,這個寬高比可以用來計算自動尺寸以及為其他佈局函數服務。
像是上面的代碼,我們就可以替換成:
ul li img {
width: 150px;
aspect-ratio: 3 / 2;
}
當然,有的時候,我們的佈局是響應式動態在變化的,容器的寬度也是不確定的,因此,有了 aspect-ratio
之後,我們的寫法就可以更佳的舒服。
ul li img {
width: 100%;
aspect-ratio: 3 / 2;
}
這裡,容器基於 Flex 彈性佈局或者響應式佈局,其寬度是不固定的,但是圖片的寬高比是固定的,使用 aspect-ratio: 3 / 2
就能非常好的適配這種情況。
我們藉助了 aspect-ratio 這個 CSS 中較新的屬性來始終自動獲得正確的寬高比,無論其父元素的寬度如何變化。
當然,
aspect-ratio
不僅僅只是能運用在這裡,在aspect-ratio
出現之前,我們只能通過一些其它的 Hack 方式,譬如設置padding-top
等方式模擬固定的寬高比。在aspect-ratio
之後,我們終於有了設定容器固定寬高比的能力。
object-fit
避免圖片拉伸
當然,限制高寬也會出現問題,譬如圖片被拉伸了,非常的難看:
這個時候,我們可以藉助 object-fit
,它能夠指定可替換元素的內容(也就是圖片)該如何適應它的父容器的高寬。
ul li img {
width: 150px;
aspect-ratio: 3 / 2;
object-fit: cover;
}
利用 object-fit: cover
,使圖片內容在保持其寬高比的同時填充元素的整個內容框。
object-fit
的取值有 fill
、none
、contain
、cover
,與 background-size
類似,可以類比記憶。
也可以看看這張圖,很易於理解:
object-fit
還有一個配套屬性 object-position
,它可以控製圖片在其內容框中的位置。(類似於 background-position
),預設是 object-position: 50% 50%
,如果你不希望圖片居中展示,可以使用它去改變圖片實際展示的 position。
ul li img {
width: 150px;
aspect-ratio: 3 / 2;
object-fit: cover;
object-position: 50% 100%;
}
像是這樣,object-position: 100% 50%
指明從底部開始展示圖片。這裡有一個很好的 Demo 可以幫助你理解 object-position
。
CodePen Demo -- Object position
使用 image-rendering
設置圖片縮放演算法
相對於上面幾個新特性,image-rendering
會更為冷門。
很多時候,我們設置一個圖片在頁面上的展示大小為 200px x 200px
,但是圖片的原始尺寸可能是 800px x 800px
,也可能是 50px x 50px
。
這個時候,我們就可以利用 image-rendering
,設置圖片在縮放狀態下的展示演算法。
image-rendering
在特定的場景下,能夠起到奇效。
來看這樣一個有意思的 DEMO,假設我們有這樣一個原圖效果,它是一個二維碼,大小為 100px x 100px
:
如果我們將它放大,放到很大,明顯,這個二維碼會失真,像是這樣:
OK,在這種放大失真的情況想,可以使用 image-rendering
改變圖片縮放演算法,這裡我們試一下 image-rendering: pixelated
:
.img {
image-rendering: pixelated;
}
效果變化,如下圖所示:
可以看到,image-rendering: pixelated
處理過的圖像,竟然變得如此清晰!
CodePen Demo -- QrCode Image-rendering demo
來看看 image-rendering
的幾個取值:
image-rendering: auto
:自 Gecko 1.9(Firefox 3.0)起,Gecko 使用雙線性(bilinear)演算法進行重新採樣(高質量)。image-rendering: smooth
:使用能最大化圖像客觀觀感的演算法來縮放圖像image-rendering: high-quality
:與 smooth 相同,但更傾向於高質量的縮放。image-rendering: crisp-edges
:必須使用可有效保留對比度和圖像中的邊緣的演算法來對圖像進行縮放,並且,該演算法既不會平滑顏色,又不會在處理過程中為圖像引入模糊。合適的演算法包括最近鄰居(nearest-neighbor)演算法和其他非平滑縮放演算法,比如 2×SaI 和 hqx-* 系列演算法。此屬性值適用於像素藝術作品,例如一些網頁游戲中的圖像。image-rendering: pixelated
:放大圖像時,使用最近鄰居演算法,因此,圖像看著像是由大塊像素組成的。縮小圖像時,演算法與 auto 相同。
雖然規範定義了挺多值,但是實際上,現代瀏覽器基本暫時只支持:auto
、pixelated
、以及 -webkit-optimize-contrast
(Chrome 下的 smooth)。
看描述都會挺懵逼的,實際使用的時候,最好每個都試一下驗證一下效果。總結而言,image-rendering
的作用是在圖像縮放時,提供不一樣的渲染方式,讓圖片的展示形態更為多樣化,或者說是儘可能的去減少圖片的失真帶來的信息損耗。
我們再看一個 DEMO,原圖如下(例子來源於 W3C 規範文檔):
實際效果:
當然,看上去 pixelated
的效果挺好,這是由於這是一張偏向於矢量的圖片,細節不多,對於高精度的人物圖,就不太適用於 pixelated
,容易把圖片馬賽克化。
真正規範希望的在放大後讓圖片儘可能不失真的 crisp-edges
效果,目前暫時沒有得到瀏覽器的實現。後面可以期待一下。
CodePen Demo -- Image-rendering demo
模塊總結
這一章,我們介紹了 4 個較新的 CSS 特性:
aspect-ratio
:控制容器的寬高比,避免產生佈局偏移及抖動object-fit
:設定內容應該如何適應到其使用高度和寬度確定的框,避免圖片拉伸object-position
:基於object-fit
,設置圖片實際展示的 position 範圍image-rendering
:控製圖片在縮放狀態下的展示演算法
合理利用它們,可以給用戶在圖片上以更好的體驗。
懶載入/非同步圖像解碼方案
繼續下一個章節。本章節,我們來討論下圖片的懶載入與非同步圖像解碼方案。
圖片的懶載入
懶載入是一種網頁性能優化的常見方式,它能極大的提升用戶體驗。到今天,現在一張圖片超過幾 M 已經是常見事了。如果每次進入頁面都需要請求頁面上的所有的圖片資源,會較大的影響用戶體驗,對用戶的帶寬也是一種極大的損耗。
所以,圖片懶載入的意義即是,當頁面未滾動到相應區域,該區域內的圖片資源(網路請求)不會被載入。反之,當頁面滾動到相應區域,相關圖片資源的請求才會被髮起。
在過去,我們通常都是使用 JavaScript 方案進行圖片的懶載入。而今天,我們在圖片的懶載入實現上,有了更多不一樣的選擇。
JavaScript 方案實現圖片的懶載入
首先,回顧一下過往最常見的,使用 JavaScript 方案實現圖片的懶載入。
通過 JavaScript 實現的懶載入,主要是兩種方式:
- 監聽 onscroll 事件,通過
getBoundingClientRect
API 獲取元素圖片距離視口頂部的距離,配合當前可視區域的位置實現圖片的懶載入 - 通過 HTML5 的
IntersectionObserver
API,Intersection Observer(交叉觀察器) 配合監聽元素的isIntersecting
屬性,判斷元素是否在可視區內,能夠實現比監聽 onscroll 性能更佳的圖片懶載入方案
但是,JavaScript 方案的一個劣勢在於,不管如何,需要引入一定量的 JavaScript 代碼,進行一定量的運算。
到今天,其實我們有更多的其他便捷的方式去實現圖片的懶載入。
使用 content-visibility: auto
實現圖片內容的延遲渲染
首先,介紹一個非常有用,但是相對較為冷門的屬性 -- content-visibility
。
content-visibility
:屬性控制一個元素是否渲染其內容,它允許用戶代理(瀏覽器)潛在地省略大量佈局和渲染工作,直到需要它為止。
利用 content-visibility
的特性,我們可以實現如果該元素當前不在屏幕上,則不會渲染其後代元素。
假設我們有這樣一個 DEMO:
<div class="g-wrap">
// 模塊 1
<div class="paragraph">
<p>Lorem Start!</p>
<img src="https://s1.ax1x.com/2023/02/20/pSX1xMV.png" alt="" />
<p>Lorem End!</p>
</div>
// 模塊 2
<div class="paragraph">
<p>Lorem Start!</p>
<img src="https://s1.ax1x.com/2023/02/20/pSX1xMV.png" alt="" />
<p>Lorem End!</p>
</div>
// ... 連續幾十個上述類似的結構
</div>
只需要給需要延遲(實時)渲染的元素,設置簡單的 CSS 樣式:
.paragraph {
content-visibility: auto;
}
我們來看一下,設置了 content-visibility: auto
與沒設置的區別。
如果,不添加上述的 content-visibility: auto
代碼,頁面的滾動條及滾動效果如下:
那麼,在添加了 content-visibility: auto
之後,註意觀察頁面的滾動條及滾動效果:
可以看到滾動條在向下滾動在不斷的抽搐,這是由於下麵不在可視區域內的內容,一開始是沒有被渲染的,在每次滾動的過程中,才逐漸渲染,以此來提升性能。
Codepen Deom -- content-visibility: auto Image Load Demo
content-visibility: auto
VS 圖片懶載入
當然,其實使用 content-visibility: auto
並不能真正意義上實現圖片的懶載入。
這是因為,即便當前頁面可視區域外的內容未被渲染,但是圖片資源的 HTTP/HTTPS 請求,依然會在頁面一開始被觸發!
因此,這也得到了一個非常重要的結論:
content-visibility: auto
無法直接替代圖片懶載入,設置了 content-visibility: auto
的元素在可視區外只是未被渲染,但是其中的靜態資源仍舊會在頁面初始化的時候被全部載入。因此,它更像是一個虛擬列表的替代方案。
關於
content-visibility
本文限於篇幅,沒有完全展開,但是它是一個非常有意思且對渲染性能有幫助的屬性,完整的教程,你可以看我的這篇文章 -- 使用 content-visibility 優化渲染性能
使用 loading=lazy
HTML 屬性實現圖片懶載入
OK,content-visibility
很不錯,但是略有瑕疵。但是,我們還有其他方式。
HTML5 新增了一個 loading
屬性。
到今天,除了 IE 系列瀏覽器,目前都支持通過 loading
屬性實現延遲載入。此屬性可以添加到 <img>
元素中,也可以添加到 <iframe>
元素中。
屬性的值為 loading=lazy
會告訴瀏覽器,如果圖像位於可視區時,則立即載入圖像,併在用戶滾動到它們附近時獲取其他圖像。
我們可以像是這樣使用它:
<img src="xxx.png" loading="lazy">
這樣,便可以非常便捷的實現圖片的懶載入,省去了添加繁瑣的 JavaScript 代碼的過程。
看看 loading=lazy
到今天(2023-02-26)的相容性,還是非常不錯的:
使用 decoding=async
實現圖片的非同步解碼
除了 loading=lazy
,HTML5 還新增了一個非常有意思的屬性增強圖片的用戶體驗。那就是 decoding
屬性。
HTMLImageElement 介面的 decoding
屬性用於告訴瀏覽器使用何種方式解析圖像數據。
它的可選取值如下:
sync
: 同步解碼圖像,保證與其他內容一起顯示。async
: 非同步解碼圖像,加快顯示其他內容。auto
: 預設模式,表示不偏好解碼模式。由瀏覽器決定哪種方式更適合用戶。
上文其實也提及了,瀏覽器在進行圖片渲染展示的過程中,是需要對圖片文件進行解碼的,這一個過程快慢與圖片格式有關。
而如果我們不希望圖片的渲染解碼影響頁面的其他內容的展示,可以使用 decoding=async
選項,像是這樣:
<img src="xxx.png" decoding="async">
這樣,瀏覽器便會非同步解碼圖像,加快顯示其他內容。這是圖片優化方案中可選的一環。
同樣的,我們來看看到今天(2023-02-26),decoding="async"
的相容性,整體還是非常不錯的,作為漸進增強方案使用,是非常好的選擇。
實際檢驗 loading=lazy
與 decoding=async
效果
OK,下麵我們製作一個簡單的 DEMO,試一下 loading=lazy
與 decoding=async
的效果。
我們準備一個擁有 339 個圖片的 HTML 頁面,每個圖片文件的 src 大小不一。
<div class="g-container">
<img src="image1.jpeg">
<img src="image2.jpeg">
// ... 339 個
</div>
CSS 的設置也很重要,由於是純圖片頁面,如果不給圖片設置預設高寬,最頁面刷新的一瞬間,<img>
元素的高寬都是 0,會導致所有 <img>
元素都在可視區內,所以,我們需要給 <img>
設置一個預設的高寬:
img {
margin: 8px;
width: 300px;
height: 200px;
object-fit: cover;
}
這樣,再不添加 loading=lazy
與 decoding=async
的狀態下,看看 Network
的表現:
我這裡沒有模擬弱網環境,網速非常快,可以看到,發送了 339 個圖片資源請求,也就是全部的圖片資源在頁面載入的過程中都請求了,頁面 Load
事件完成的時間為 1.28s。
好,我們給所有的圖片元素,添加上 loading=lazy
與 decoding=async
:
<div class="g-container">
<img src="image1.jpeg" loading="lazy" decoding="async">
<img src="image2.jpeg" loading="lazy" decoding="async">
// ... 339 個
</div>
看看效果:
可以看到,這一次只發送了 17 個圖片資源請求,頁面 Load
事件完成的時間為 26ms。
優化前 | 優化後 |
---|---|
1.28s | 26 ms |
1.28s 到 26ms,效果是非常明顯的,如果是弱網環境,對首屏載入性能的提升,會更為明顯!
當然,實際我測試的過程也,也單獨試過 decoding="async"
的作用,只是由於是純圖片頁面,效果不那麼明顯。感興趣的同學,可以自行嘗試。
模塊總結
在本章節中,我們介紹了不同的方式實現圖片的懶載入、延遲渲染、非同步解碼,它們分別是:
- 通過 onscroll 事件與
getBoundingClientRect
API 實現圖片的懶載入方案 - 通過 Intersection Observer(交叉觀察器)實現比監聽 onscroll 性能更佳的圖片懶載入方案
- 通過
content-visibility: auto
實現圖片資源的延遲渲染 - 通過
loading=lazy
HTML 屬性實現圖片懶載入 - 通過
decoding=async
HTML 屬性實現圖片的非同步解碼
圖片資源的容錯及可訪問性處理
OK,最後一個章節,我們簡單聊一聊圖片資源的容錯及可訪問性處理。
圖片的可訪問性處理
可訪問性(A11Y),在我們的網站中,屬於非常重要的一環,但是大部分同學都容易忽視它。
在一些重交互、重邏輯的網站中,我們需要考慮用戶的使用習慣、使用場景,從高可訪問性的角度考慮,譬如假設用戶沒有滑鼠,僅僅使用鍵盤,能否順暢的使用我們的網站?
非常重要的一點是,提高可訪問性也能讓普通用戶更容易理解 Web 內容。
基於 Usability & Web Accessibility - image
對於圖像信息,我們需要大致遵循如下可訪問性原則:
- 所有有意義的 img 元素必須有 alt 屬性
- 提供替代 alt 屬性的其他方式
- 使用輔助技術隱藏裝飾圖像
第一點非常好理解,所有的有意義的圖片元素都必須要提供 alt
屬性。
第二點比較有意思,在 A11Y 中,其實有一套 WAI-ARIA 標準。WAI-ARIA 是一個為殘疾人士等提供無障礙訪問動態、可交互Web內容的技術規範。
簡單來說,它提供了一些屬性,增強標簽的語義及行為:
- 可以使用 tabindex 屬性控制元素是否可以聚焦,以及它是否/在何處參與順序鍵盤導航
- 可以使用 role 屬性,來標識元素的語義及作用,譬如使用
<div id="saveChanges" tabindex="0" role="button">Save</div>
來模擬一個按鈕 - 還有大量的
aria-*
屬性,表示元素的屬性或狀態,幫助我們進一步地識別以及實現元素的語義化,優化無障礙體驗
上述第二點,提供替代 alt 屬性的其他方式 的含義就是使用 WAR-ARIA 規範提供的諸如 aria-label
和 aria-labelledby
屬性為圖像提供可訪問的名稱。
當存在這些屬性時,輔助技術(屏幕閱讀器)將忽略圖像的 alt
屬性並讀取 ARIA 標簽。
而第三點,使用輔助技術隱藏裝飾圖像,又是什麼意思呢?
上面第一點 所有有意義的 img 元素必須有 alt 屬性,反過來說,頁面上也會存在無意義的裝飾性的圖片,這些圖片內容對輔助技術(屏幕閱讀器)而言,其實是可以忽略的。
對於沒有任何功能或信息內容的裝飾圖像,可以通過多種方式對屏幕閱讀器隱藏:
- 使用空的
alt
屬性 - 使用 ARIA 屬性
role="presentation"
標明圖片元素是裝飾可忽略圖片 - 使用 CSS background 的方式呈現這些圖片
alt 不要與 title 混淆
OK,下麵來講一些有意思的細節內容。
有一個非常基礎的知識,簡單過一下,也就是圖片元素中,alt
與 title
的差異:
- 圖片中的
alt
屬性是在圖片不能正常顯示時出現的文本提示。 - 圖片中的
title
屬性是在滑鼠在移動到元素上的文本提示。
正確使用 alt 屬性
對於使用屏幕閱讀器的用戶而言,圖片是無法正常展示或者被的瀏覽的,基於此,我們需要利用好 alt
屬性,或者是上述的aria-label
和 aria-labelledby
屬性。
那麼,這些屬性內的內容應該填充什麼呢?我們需要基於圖片的功能加以區分:
-
信息性圖像:以圖形方式表示概念和信息的圖像,通常是圖片、照片和插圖。
alt
替代文本應該至少是一個簡短的描述,傳達圖像所呈現的基本信息。 -
裝飾性圖像:當圖像的唯一目的是為頁面添加視覺裝飾,而不是傳達對理解頁面很重要的信息時,如上述所言,使用空的 alt,譬如
alt=""
-
功能圖像:用作鏈接或按鈕的圖像的替代文本應該描述鏈接或按鈕的功能,而不是視覺圖像。此類圖像的示例是表示列印功能的印表機圖標或提交表單的按鈕。
-
文本圖像:可讀文本有時會出現在圖像中。如果圖片不是徽標,請避免圖片中出現文字。但是,如果使用文本圖像,替代文本應包含與圖像中相同的詞。
-
圖像組:如果多張圖像傳達一條信息,則一張圖像的替代文本應傳達整組信息。
-
圖像映射:包含多個可點擊區域的圖像的替代文本應該為鏈接集提供整體上下文。此外,每個可單獨點擊的區域都應該有替代文本來描述鏈接的目的或目的地。
其實 alt
的學問是非常之多的,如果我們的頁面能做到這一點,那真的算是從根上開始思考,開始優化用戶體驗。
img 元素與 background 元素的取捨
OK,那麼,講到這裡,還有一個有意思的點就很自然的應該被提及。
那就是我們應該什麼時候使用 <img>
元素,什麼時候使用 background
內嵌圖片?
我們可以從性能及功能兩個方面進行考慮:
類型 | img | backgroud-image |
---|---|---|
圖層位置 | 前景 | 背景 |
預設初始尺寸 | 不定 | 固定 |
是否會產生迴流重繪 | 會 | 不會 |
圖片載入失敗 | 可以觸發元素的 onerror 事件,展示 alt 屬性 | 無法有效設置異常處理場景 |
使用場景 | Logo、產品圖片、廣告圖片 | 裝飾性無語義內容等 |
其實性能上並不是核心考慮的點,因為上文我們也講到了在今天可以大規模使用是 loading="lazy"
屬性,圖片可以進行原生支持的懶載入。
我們在考慮選取 <img>
還是 backgroud-image
的時候,更多的還是從圖片功能上進行考慮。一般來說,作為修飾的且無語義的裝飾性圖片選擇使用 background-image
,而比較重要的與網頁內容相關的就使用 <img>
標簽。
由於有語義的圖片使用 <img>
展示,它的一個好處在於,當圖片載入失敗的時候,可以觸發元素的 onerror 事件,我們可以有效的利用這一點,對圖片進行異常處理。
圖片的異常處理
當圖片鏈接掛了,載入失敗了,我們比較好的處理方式應該是怎麼樣呢?
處理的方式有很多種。在張鑫旭老師的這篇文章中 -- 圖片載入失敗後CSS樣式處理最佳實踐 有一個不錯的實踐。
核心思路為:
- 利用圖片載入失敗,觸發
<img>
元素的onerror
事件,給載入失敗的<img>
元素新增一個樣式類 - 利用新增的樣式類,配合
<img>
元素的偽元素,在展示預設兜底圖的同時,還能一起展示<img>
元素的alt
信息
<img src="test.png" alt="Alt Info" onerror="this.classList.add('error');">
img.error {
position: relative;
display: inline-block;
}
img.error::before {
content: "";
/** 定位代碼 **/
background: url(error-default.png);
}
img.error::after {
content: attr(alt);
/** 定位代碼 **/
}
我們利用偽元素 before
,載入預設錯誤兜底圖,利用偽元素 after
,展示圖片的 alt
信息:
OK,到此,完整的對圖片的處理就算完成了,這也比較好的闡述了為什麼,對有語義,有 alt
信息的圖片,我們應該使用 <img>
元素來實現。這是因為,我們可以在錯誤發生的時候,比較好的對圖片進行兜底展示,讓用戶直觀的能夠看到 alt 內容。
完整的 Demo 你可以戳這裡看看:
當然,上述方案存在兩個