本文首發于 vivo互聯網技術 微信公眾號
鏈接:https://mp.weixin.qq.com/s/rSpWorfNTajtqq_pd7H-nw
作者:悟空中臺研發團隊
一、背景
移動端網頁的加載速度對用戶體驗極為重要,是影響頁面轉化率的關鍵因素,H5 活動頁往往使用大量的圖片素材來豐富活動效果,素材加載的快慢會對用戶感知造成重要的影響。
在《悟空活動中臺 - H5 活動加載優化》一文中我們提到過圖片壓縮也是提升悟空中臺產出 H5 頁面加載性能的重要手段之一,對本篇將從技術選型、架構設計到方案落地,全方位的呈現悟空活動中臺基于 WebP 的圖片高性能加載方案。
為什么要做圖片加載性能優化?
包含了大量圖片素材的 H5 頁面,呈現給用戶之前,至少要等待首屏加載完成;要提升加載速度,一方面請求的響應速度要足夠快,另一方面要盡量減小傳輸的數據量。
二、方案選型
1、演進
原始做法是:拿到圖片文件后,使用圖片壓縮工具進行壓縮,頁面再引入壓縮后的小體積文件;
該方案存在嚴重的問題:效率低下——需要開發者或者設計師針對每張素材圖進行手動壓縮、肉眼審核質量、壓縮得到的文件手動上傳。
我們從高清晰度、高壓縮比、小體積的訴求出發,最終選擇了使用WebP作為首選圖片文件格式。
2、為什么是WebP
WebP 是 Google 推出的一種同時提供了有損壓縮與無損壓縮(可逆壓縮)的圖片文件格式。派生自影像編碼格式 VP8,被認為是 WebM 多媒體格式的姊妹項目,是由 Google 在購買 On2 Technologies 后發展出來,以 BSD 授權條款發布。
WebP 的優勢體現在它具有更優的圖像數據壓縮算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的圖像質量;同時具備了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都相當優秀、穩定和統一。
相比于其他相同大小、不同格式的壓縮圖像,WebP 格式的圖片擁有更小的體積以及更高的質量,優勢十分明顯。
下圖是一些實測案例:
使用 WebP 對圖片進行有損壓縮,在默認配置75%的壓縮比下,可以將 PNG 圖片大小壓縮至原圖體積的13%左右,JPG 圖片甚至可以壓縮至原圖體積的10%左右(可參考官方測試頁面),實際效果顯著。
三、圖片服務
1、素材服務
悟空中臺的素材服務架構如下圖所示,在 node server 節點中,我們集成了圖片轉 WebP 以及轉碼后文件存儲的服務。
[圖片上傳失敗...(image-b1cedd-1597632523493)]
2、圖片壓縮
圖片壓縮服務實現了將用戶上傳的圖片數據,進行格式校驗、WebP 格式轉碼、上傳文件服務器以及存儲的過程。
使用 cwebp 進行壓縮
cwebp 是 Google 官方提供的用于將 PNG、JPEG、TIFF 或原始 Y'CbCr 格式的文件壓縮轉換為 WebP 格式的命令行編碼工具(安裝方法請參考官網安裝說明)。
使用方法如下:
cwebp [options] input_file -o output_file.webp
其中 options 是壓縮參數配置,包含是否啟用無損壓縮-lossless,壓縮系數-q(0~100) 等,如使用 80 的壓縮系數對目標文件進行有損壓縮:
cwebp -q 80 image.png -o image.webp
Node 服務使用 cwebp-bin
cwebp-bin模塊提供了 Node.js 使用 cwebp 能力進行圖片壓縮轉碼的接口,我們的圖片壓縮服務引入該模塊模塊實現常見格式圖片到WebP的轉碼。
(1)工具安裝
首先需要在服務器執行下述指令以安裝模塊內部集成的 WebP 工具程序(libwebp-x.x.tar.gz):
npm install --global cwebp-bin
(2)網絡優化
在實際使用時,打包上線時會偶發該安裝包資源請求失敗的問題;為了安裝過程的順利進行,悟空中臺的開發者將該安裝包的url由原github下載地址改為了更加穩定的google官方下載地址:
// node_modules/@vivo/cwebp-bin/lib/install.js - line 14
binBuild.file(path.resolve(__dirname, '../vendor/source/libwebp-1.1.0.tar.gz'), [
`./configure --disable-shared --prefix="${bin.dest()}" --bindir="${bin.dest()}"`,
'make && make install'
]).then(() => {
...
}).catch(error => {
...
});
改為
// node_modules/@vivo/cwebp-bin/lib/install.js - line 14
var cfg = [
'./configure --disable-shared --prefix="' + bin.dest() + '"',
'--bindir="' + bin.dest() + '"'
].join(' ');
var builder = new BinBuild()
.src('http://downloads.webmproject.org/releases/webp/libwebp-0.5.1.tar.gz')
.cmd(cfg)
.cmd('make && make install');
return builder.run(function (err) {
...
});
(3)圖片壓縮
圖片壓縮服務中使用以下代碼調用 cwebp 工具進行原圖到 WebP 的轉碼:
const {execFileSync} = require('child_process');
const cwebp = require('cwebp-bin');
execFileSync(cwebp, ['input.png', '-o', 'output.webp'])
壓縮形式選取
通過上文我們了解到,WebP支持有損壓縮和無損壓縮兩種形式,下面我們將針對性的測試兩種壓縮形式的差異并選出適合的方案。
之所以要對比有損與無損的區別,主要是考慮到時間上的效率和空間上的節約。如果在損失 20~30% 的精度后,用戶的肉眼上難以區分,那么這個精度的損失就是有意義的,因為相對于無損壓縮,有損壓縮帶來的體積的縮小以及壓縮時的效率,都比無損壓縮更適合用于企業的生產模式下。
我們選取了特點分別為色彩單一、色彩較為豐富和色彩極為豐富的三張圖片進行測試:
下面列出了上述圖片分別使用 WebP 無損和有損壓縮進行測試的樣本數據。
(1)WebP無損壓縮:
execFileSync(cwebp, ['-lossless', filePath, '-o', webpPath]);
結果統計
(2)WebP有損壓縮(90%壓縮率):
execFileSync(cwebp, ['-q', '90', filePath, '-o', webpPath]);
結果統計
根據上面兩份測試數據可以得出,對于同一張圖片:
從壓縮比角度來看,90%壓縮率的有損壓縮得到的圖片體積小于無損壓縮產出圖片體積的20%。
從壓縮時間角度來看,90%壓縮率的有損壓縮耗費的時間是無損壓縮20%以內。
對于不同圖片,色彩越豐富,壓縮花費的時間越長,壓縮比越小;甚至會出現壓縮的到的圖片體積超過原圖的情況(具體原因見下文)。
通過以上測試數據反映的結果來看,有損壓縮的優勢更大。
壓縮率選取
使用 WebP 有損壓縮來進行圖片的壓縮,就不得不考慮接下來的問題:WebP 壓縮比設置為多少才是最佳實踐?
同樣的,我們對上述圖片進行了以下抽樣數據對比:
(1)Webp 有損壓縮(90%壓縮率):
execFileSync(cwebp, ['-q', '90', filePath, '-o', webpPath]);
結果統計
(2)Webp 有損壓縮(默認值 75%壓縮率):
execFileSync(cwebp, ['-q', '75', filePath, '-o', webpPath]);
結果統計
為什么要拿 75% 壓縮率來做對比?原因是 cwebp 有損壓縮的默認壓縮率是 75%,這個比例也是通常情況下官方推薦的。
但是在實際業務場景下,75% 的壓縮比例并不能滿足產品需求。比如說一張圖片經過壓縮后同時在移動端和 PC 端使用;或圖片的色彩空間尤其復雜等等這些情況,再經過 75% 的有損壓縮,我們觀察到色彩對比度明顯的圖片局部有模糊的情況。
經過與設計師同學一起反復的測試實驗,我們使用了 90% 的壓縮率來代替默認的 75% 。此時轉換后的圖片與原圖片結構化差異值SSIM不會小于 0.88 ,視覺效果上用戶基本發現不了圖片已經進行了壓縮。
結構相似性指標(英文:structural similarity index,SSIM index)是一種用以衡量兩張數位影像相似程度的指標。當兩張影像其中一張為無失真影像,另一張為失真后的影像,二者的結構相似性可以看成是失真影像的影像品質衡量指標。相較于傳統所使用的影像品質衡量指標,像是峰值信噪比(英語:PSNR),結構相似性在影像品質的衡量上更能符合人眼對影像品質的判斷。
關于 WebP 壓縮質量與 SSIM 的比例關系,請參考 Google 官方說明WebP Compression Study。
我們可以通過在 cwebp 的執行命令中加入-print_ssim選項,令壓縮結果中呈現 SSIM 信息:
await execFileSync(cwebp, ['-print_ssim', '-q', '90', filePath, '-o', webpPath])
執行輸出信息:
WebP 壓縮后反而比原圖更大?
我們在測試的過程中還觀察到有一些圖片轉換為 WebP 格式后得到文件體積比原圖更大。經過查閱Google 官方文檔,得出是由于格式差異以及轉碼算法導致的:
WebP 的壓縮率設置超過 75%時,在遇到在遇到一些特殊編碼的圖片時,會調整壓縮時的算法,如:
- 當圖片的編碼類型處理后發生變化時,壓縮后的圖片體積就會變大。比如說編碼類型從索引類型變化到了真彩類型,這種場景下壓縮時需要處理的像素點數就會大三倍,所以壓縮圖片的體積就大了。
- 當原圖片中重復的顏色數目比較多時,Webp 有損壓縮時會根據原像素值計算出新的像素值,而壓縮時重點會處理的就是重復的顏色數目,所以壓縮后的圖片體積自然就大了。
- 當原圖中包含透明管道時,由于 Webp 并不支持灰度圖帶上透明通道這種類型,帶上透明通道就將格式固定成了 RGBA 格式。因此導致了要保存的數據變大。
面對這個問題,我們與設計和產品同事共同制定了相應策略:如果壓縮后的文件體積大于原圖,則使用原圖。
3、服務流程
在確定合適的壓縮比例和壓縮方案后,就可以對圖片壓縮服務進行整體設計,流程如下:
[圖片上傳失敗...(image-6d4a1b-1597632523493)]
node 執行 cwebp 指令對圖片文件進行轉碼;
當轉碼后的圖片體積大于源文件時,在 WebP 圖片的文件名后追加“nwebp”字符串標記,以便前端識別;
將編碼后的 WebP 文件和源文件一同上傳至文件服務器,并拿到返回的 URL;
將圖片名稱、存儲資源路徑等存儲至素材中心服務數據庫中;
存儲完成后將圖片名稱、存儲資源路徑等通過接口返回前端展示。
四、頁面邏輯
1、優先使用WebP
前端頁面策略是當網頁運行在支持 WebP 格式的宿主環境(如 Chrome、Android Webview 等)中時,優先使用 WebP 圖片資源,在不支持的宿主環境中,使用原始圖片資源。
(1)判斷宿主環境是否支持WebP
頁面首先需要判斷當前宿主環境是否支持 WebP :
const supportWebP = (function () {
var canvas = typeof document === 'object' ? document.createElement('canvas') : {}
canvas.width = canvas.height = 1
return canvas.toDataURL ? canvas.toDataURL('image/webp').indexOf('image/webp') === 5 : false
})()
(2)素材加載
前面講解了后臺圖片壓縮和存儲服務的設計,接下來我們來一起了解一下前端邏輯上是如何加載 WebP 圖片的。其流程如下圖所示:
[圖片上傳失敗...(image-69c15a-1597632523493)]
(3)使用指令獲取圖片url
獲取圖片 url 的方式有多種,我們的需求是在圖片資源加載前獲取真實的圖片 url,并對其進行處理,而 Vue 提供的自定義指令可以幫助我們以侵入性極小的形式的拿到目標元素的相關信息。
這里我們使用bind指令進行一次性的初始化設置,在當指令第一次綁定到元素時調用,通過獲取到元素關聯的素材的 url,以 img 元素為例:
bind: function (el, binding) {
if (el.tagName.toLowerCase() === 'img' && el.src && el.src.indexOf('data:image') === -1 && supportWebP) {
// 通過 src 屬性獲取 img 元素關聯的圖片地址
var _src = el.src
// ... 對img的后續處理
}
}
2、****處理圖片 url
首先判斷當前 url 中是否有素材上傳時標記的“nwebp”字樣,如果有則說明該圖片轉為 WebP 格式后體積反而大于原圖,此時無需使用 WebP 素材替換原有素材;否則,則加載體積更小的 WebP 文件代替原素材文件。
然后判斷當前運行環境是否支持 WebP 格式圖片的渲染,如果支持,則加載 WebP 素材資源,否則使用原文件鏈接。
(1)img 元素處理
我們在 img 標簽上添加上文定義的v-webp指令如下:
<img src="https://someurl" v-webp />
在 img 元素的 create 階段, v-webp 指令被 bind 并執行定義好的 hook。
在 hook 中,我們對于 img 元素我們可以根據 el.src 獲取到元素關聯素材的 url,當判斷需要采用 WebP 格式文件時,在原素材 url 后拼接.webp,從而使得對應圖片元素加載的是 WebP 編碼后的素材:
// ... 對img的后續處理
// 帶有 nwebp 標記的圖片不做轉換
if (_src.indexOf('nwebp') > -1) {
return
}
let webpSrc = ''
if (_src.indexOf('.webp') > -1) {
webpSrc = _src
} else {
webpSrc = _src + '.webp'
}
el.src = webpSrc
el.onerror = function() {
// WebP加載失敗則回退至源文件
el.src = _src
}
對于運行環境不支持 WebP 加載的情況,則無需做任何處理,直接加載原圖即可:
if (!supportWebP) {
return
}
(2)background-image 處理
對于 img 之外的元素,我們在 v-webp 指令中傳入要作為 backgroundImage 屬性值的 url:
<div v-webp="https://someurl"></div>
在 hook 中,根據 binding.value 獲取指令的綁定值,即圖片 url,當判斷需要采用 WebP 格式文件時,在原素材 url 后拼接“.webp” 構造頁面用 url,否則直接使用原圖 url,然后為該 DOM 元素設置內聯的 backgroundImage style 即可:
if (supportWebP) {
el.style.backgroundImage = 'url("' + webpSrc + '")'
} else {
el.style.backgroundImage = 'url("' + binding.value + '")'
}
五、提升兼容性
WebP 格式雖然優點眾多,但是有一個嚴重的問題——兼容性并不理想。下面我們將從 “擴展WebP兼容范圍” 的訴求出發,探索前端解碼WebP文件的可行性。
1、WebP的兼容性問題
WebP 格式雖然存在壓縮率高、體積小等優勢,但是其自身并不是通用瀏覽器圖片格式規范,像 Safari 和 FireFox 等宿主環境均沒有很好的支持該格式(參考自can i use):
為了保證悟空中臺產出的專題頁在更多的瀏覽器中能夠以更快的速度加載、渲染,我們又向前走了一步,對 WebP 格式的純前端解碼做出了下面的探索。
2、在頁面解碼
核心理念是將 WebP 圖片作為傳輸介質,保證了頁面圖片數據的下載速度;在拿到 WebP 圖片后,對于不支持的宿主環境,將 WebP 圖片進行解碼成通用的Base64格式進行渲染。
(1)使用JS解碼
純前端是否可以實現 WebP 格式到 Base64 格式的解碼呢?Google 官方團隊提供了 js 解碼 WebP 的庫——libwebp.js;但是我們隨機挑選一些 WebP 圖片實際測試下來發現性能欠佳:
該方案下 WebP 圖片實際的加載時間為網絡數據傳輸用時 + 解碼用時,面對性能要求較高的場景,WebP 的加載速度真要受限于 JS 不擅長的編解碼運算能力了么?當我們再次研究libwebp的資料時,瀏覽到下述說明:
webp_js 還有一個WebAssembly版本。
(2)使用WebAssembly提升解碼性能
WebAssembly 作為 Web 標準,在各個瀏覽器均有較好的支持,兼容性遠強于WebP:
WebAssembly 可以作為 C/C++/Rust 等語言的編譯目標在瀏覽器環境中以接近原生的速度運行,計算性能要遠遠優于 JavaScript。
WebAssembly的工作流程如下(圖片來自MDN):
[圖片上傳失敗...(image-c38669-1597632523494)]
其中膠水JS(JS“glue”code)的作用是提供 JS 調用 wasm 能力的接口。
編譯并測試 libwebp
我們將 libwebp 編譯成 wasm 文件供 JavaScript 調用,提供高速解碼 WebP 的能力。具體的編譯過程可以參照 libwebp/webp_js 的編譯說明,編譯環境建議使用linux/unix,其余步驟此處不再贅述。
編譯后我們得到了 wasm 文件(gzip壓縮后體積51kb)和膠水 js(gzip壓縮后體積44kb) ,然后使用上述同樣的素材進行性能測試結果如下:
由以上測試基本可以得出:
當 WebP 素材較小時,wasm 解碼相相對于純 js 解碼,可以節省接近一半時間;
當 WebP 素材較大時,wasm 方案可以使解碼速度提升超過 100%,且隨著素材增大,提升越明顯。
有了 WebAssembly 的加持,我們將原有圖片加載流程進行了如下圖所示升級:
以 img 元素為例,代碼處理邏輯如下:
// 如果當前瀏覽器環境不支持WebP格式,則使用wasm將WebP文件解碼為Base64
if (supportsWebP) {
el.src = webpSrc
} else {
// 使用fetch請求拿到WebP文件
const res = await fetch(webpSrc)
// 設置拿到的文件的編碼,以符合wasm解碼的入參條件
const webp_data_buffer = await res.arrayBuffer()
const webp_data = new Uint8Array(webp_data_buffer)
// 調用碎wasm編譯生成的膠水js的解碼方法,將解碼后的Base64值作為圖片素材的url使用
el.src = wasmDecode(webp_data)
}
3、效果對比
我們構造了一個圖片素材較多的H5專題在 Safari 中測試,效果如下(為了更好的體現加載過程,下放動圖相對實際速度均 放慢了3倍 ):
1、頁面元素不添加 v-webp 指令(加載圖片原文件):
2、頁面元添加 v-webp 指令(前端解碼WebP):
可以看出在不支持WebP的宿主中,使用了 v-webp 指令后,頁面的響應速度(白屏時間短)和圖片渲染速度均有較為明顯的提升;至此,我們已經設計并實現了一套相對完善的圖片素材加載性能優化方案。
六、小結
悟空活動中臺從提升 H5 頁面圖片加載性能的訴求出發,歷經:
- 壓縮格式選擇
- 壓縮形式和壓縮率選取
- 前端指令集成
- 提升兼容性
等一系列手段,探索出一套基于 WebP 的圖片高性能加載方案,更好的賦能了 H5 活動的開發和運營。悟空中臺開發團隊將永不止步,持續研究和思考,為大家帶來更多的實戰技巧,感謝您的閱讀。
【悟空活動中臺】系列往期精彩文章:
《揭秘 vivo 如何打造千萬級 DAU 活動中臺 - 啟航篇》 主要為大家講述 vivo 活動中臺的能力與創新。
《悟空活動中臺 - 微組件狀態管理(上)》介紹了活動頁內 RSC 組件之間的狀態管理和背后的設計思路。
《悟空活動中臺 - 微組件狀態管理(下)》探索平臺和跨沙箱環境下的微組件狀態管理。
《vivo 悟空活動中臺-基于行為預設的動態布局方案》本文以“滿屏”場景下的頁面布局思考為切入點,以微組件為元素單元,提供了一種新的布局方案設計思路——基于行為預設的動態布局方案,并詳細的分享了設計目的及具體實現方案。
《vivo悟空活動中臺 - 微組件多端探索》是基于自助多端擴展,也就意味著多端 微 組件選擇越豐富,內容越通用,玩法越多樣,產品價值也會越高。
《悟空活動中臺 - H5 活動加載優化》從提高資源請求速度,資源壓縮、緩存、渲染等多種角度出發,尋找悟空活動專題加載優化方案。