悟空活動中臺 - 基於 WebP 的圖片高性能加載方案

本文首發於 vivo互聯網技術 微信公衆號
連接: https://mp.weixin.qq.com/s/rSpWorfNTajtqq_pd7H-nw
做者:悟空中臺研發團隊

1、背景

移動端網頁的加載速度對用戶體驗極爲重要,是影響頁面轉化率的關鍵因素,H5 活動頁每每使用大量的圖片素材來豐富活動效果,素材加載的快慢會對用戶感知形成重要的影響。javascript

在《悟空活動中臺 - H5 活動加載優化》一文中咱們提到過圖片壓縮也是提高悟空中臺產出 H5 頁面加載性能的重要手段之一,對本篇將從技術選型、架構設計到方案落地,全方位的呈現悟空活動中臺基於 WebP 的圖片高性能加載方案。前端

爲何要作圖片加載性能優化?

包含了大量圖片素材的 H5 頁面,呈現給用戶以前,至少要等待首屏加載完成;要提高加載速度,一方面請求的響應速度要足夠快,另外一方面要儘可能減少傳輸的數據量。java

2、方案選型

一、演進

原始作法是:拿到圖片文件後,使用圖片壓縮工具進行壓縮,頁面再引入壓縮後的小體積文件;node

該方案存在嚴重的問題:效率低下 ——須要開發者或者設計師針對每張素材圖進行手動壓縮、肉眼審覈質量、壓縮獲得的文件手動上傳。linux

咱們從高清晰度高壓縮比小體積的訴求出發,最終選擇了使用 WebP 做爲首選圖片文件格式。git

二、爲何是WebP

WebP 是 Google 推出的一種同時提供了有損壓縮與無損壓縮(可逆壓縮)的圖片文件格式。派生自影像編碼格式 VP8,被認爲是 WebM 多媒體格式的姊妹項目,是由 Google 在購買 On2 Technologies 後發展出來,以 BSD 受權條款發佈。github

WebP 的優點體如今它具備更優的圖像數據壓縮算法,能帶來更小的圖片體積,並且擁有肉眼識別無差別的圖像質量;同時具有了無損和有損的壓縮模式、Alpha 透明以及動畫的特性,在 JPEG 和 PNG 上的轉化效果都至關優秀、穩定和統一。web

相比於其餘相同大小、不一樣格式的壓縮圖像,WebP 格式的圖片擁有更小的體積以及更高的質量,優點十分明顯。算法

下圖是一些實測案例:數據庫

使用 WebP 對圖片進行有損壓縮,在默認配置 75% 的壓縮比下,能夠將 PNG 圖片大小壓縮至原圖體積的 13% 左右,JPG 圖片甚至能夠壓縮至原圖體積的 10% 左右(可參考官方測試頁面),實際效果顯著。

3、圖片服務

一、素材服務

悟空中臺的素材服務架構以下圖所示,在 node server 節點中,咱們集成了圖片轉 WebP 以及轉碼後文件存儲的服務。

二、圖片壓縮

圖片壓縮服務實現了將用戶上傳的圖片數據,進行格式校驗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 格式。所以致使了要保存的數據變大。

面對這個問題,咱們與設計和產品同事共同制定了相應策略:若是壓縮後的文件體積大於原圖,則使用原圖。

三、服務流程

在肯定合適的壓縮比例和壓縮方案後,就能夠對圖片壓縮服務進行總體設計,流程以下:

  1. node 執行 cwebp 指令對圖片文件進行轉碼;
  2. 當轉碼後的圖片體積大於源文件時,在 WebP 圖片的文件名後追加「nwebp」字符串標記,以便前端識別;
  3. 將編碼後的 WebP 文件和源文件一同上傳至文件服務器,並拿到返回的 URL;
  4. 將圖片名稱、存儲資源路徑等存儲至素材中心服務數據庫中;
  5. 存儲完成後將圖片名稱、存儲資源路徑等經過接口返回前端展現。

4、頁面邏輯

一、優先使用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 圖片的。其流程以下圖所示:

(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的後續處理
  }
}

二、處理圖片 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 + '")'
}

5、提高兼容性

WebP 格式雖然優勢衆多,可是有一個嚴重的問題—— 兼容性 並不理想。下面咱們將從 「擴展WebP兼容範圍」 的訴求出發,探索 前端解碼WebP文件 的可行性。

一、WebP的兼容性問題

WebP 格式雖然存在壓縮率高、體積小等優點,可是其自身並非通用瀏覽器圖片格式規範,像 Safari 和 FireFox 等宿主環境均沒有很好的支持該格式(參考自can i use):

爲了保證悟空中臺產出的專題頁在更多的瀏覽器中可以以更快的速度加載、渲染,咱們又向前走了一步,對 WebP 格式的純前端解碼作出了下面的探索。

二、在頁面解碼

核心理念是將 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):

其中 膠水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)
}

三、效果對比

咱們構造了一個圖片素材較多的H5專題在 Safari 中測試,效果以下(爲了更好的體現加載過程,下放動圖相對實際速度均 放慢了3倍 ):

一、頁面元素不添加 v-webp 指令(加載圖片原文件):

二、頁面元添加 v-webp 指令(前端解碼WebP):

能夠看出在不支持WebP的宿主中,使用了 v-webp 指令後,頁面的 響應速度 (白屏時間短)和圖片 渲染速度 均有較爲明顯的提高;至此,咱們已經設計並實現了一套相對完善的圖片素材加載性能優化方案。

6、小結

悟空活動中臺從提高 H5 頁面圖片加載性能的訴求出發,歷經:

  1. 壓縮格式選擇
  2. 壓縮形式和壓縮率選取
  3. 前端指令集成
  4. 提高兼容性

等一系列手段,探索出一套基於 WebP 的圖片高性能加載方案,更好的賦能了 H5 活動的開發和運營。悟空中臺開發團隊將永不止步,持續研究和思考,爲你們帶來更多的實戰技巧,感謝您的閱讀。

【悟空活動中臺】系列往期精彩文章:
揭祕 vivo 如何打造千萬級 DAU 活動中臺 - 啓航篇》 主要爲你們講述 vivo 活動中臺的能力與創新。
悟空活動中臺 - 微組件狀態管理(上)》介紹了活動頁內 RSC 組件之間的狀態管理和背後的設計思路。
悟空活動中臺 - 微組件狀態管理(下)》探索平臺和跨沙箱環境下的微組件狀態管理。
vivo 悟空活動中臺-基於行爲預設的動態佈局方案》本文以「滿屏」場景下的頁面佈局思考爲切入點,以微組件爲元素單元,提供了一種新的佈局方案設計思路——基於行爲預設的動態佈局方案,並詳細的分享了設計目的及具體實現方案。
vivo悟空活動中臺 - 微組件多端探索》是基於自助多端擴展,也就意味着多端 微 組件選擇越豐富,內容越通用,玩法越多樣,產品價值也會越高。
悟空活動中臺 - H5 活動加載優化》從提升資源請求速度,資源壓縮、緩存、渲染等多種角度出發,尋找悟空活動專題加載優化方案。

更多內容敬請關注 vivo 互聯網技術 微信公衆號

注:轉載文章請先與微信號:Labs2020 聯繫

相關文章
相關標籤/搜索