前端:WebP自適應實踐

七脈神劍 發表於 2021-11-25
前端

WebP介紹

WebP 是 Google 推出的一種同時提供了有損和無損兩種壓縮方式的圖片格式,優勢體現在其優秀的影像壓縮演算法,能夠帶來更小的圖片體積,同時擁有更高的的影像質量。根據官方說明,WebP 在無失真壓縮的情況下能比 PNG 減少26%的體積,有失真壓縮的情況能比 JPEG 減少25%-34%的體積。

下圖可以看出,相對於傳統的圖片格式,WebP 格式存在瀏覽器相容性方面的問題。本文通過工程化的手段來實現 WebP 格式的自適應載入。

前端:WebP自適應實踐

傳統做法

為了在前端專案裡用上 WebP 格式,並且相容不支援該格式的瀏覽器,通常的做法是判斷瀏覽器支援性,引入 WebP 圖片或其他通用格式的圖片。針對HTML、JS、CSS 三種引入圖片的場景,有以下幾種處理方式:

HTML

藉助 標籤自適應載入的特性,如下,WebP 格式放在 標籤,用JPEG、PNG等通用格式做兜底。

<picture>

  <source srcSet="https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.webp" type="image/webp" />

  <img decoding="async" loading="lazy" src="https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.jpeg" />

</picture>

JS

通過 JS 判斷瀏覽器是否支援 WebP,若支援則引入 WebP 格式的圖片,若不支援則引入JPEG、PNG等通用格式。有以下兩種判斷方式:

  1. canvas 判斷
isSupportWebp = document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp") === 0;
  1. 載入 WebP 圖片判斷
function isSupportWebp(callback) {

    var img = new Image();

    img.onload = function () {

         var result = (img.width > 0) && (img.height > 0);

         callback(result);

    };

    img.onerror = function () {

        callback(false);

    };

    img.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';

}

CSS

首先需要判斷瀏覽器是否支援 WebP 格式,若支援則在 HTML 根節點新增類名標識。

document.documentElement.classList.add('webp')

然後利用選擇器的優先順序做到 WebP 的自適應載入,CSS 中引入圖片的方式做如下改動:

.img { background-image: url('https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.jpeg') }

.webp .img { background-image: url('https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.webp') }

自動化處理

當專案中有大量本地圖片引入時,手動處理的方式就顯得比較繁瑣,除了要根據圖片引入的方式分別處理,還需要事先將所有圖片轉為 WebP 格式。

考慮到前端專案大多由 Webpack 構建,因此,嘗試開發一款 Webpack 外掛支援將專案裡的圖片轉為 WebP 格式並且支援圖片格式自適應。

方案設計

首先需要將專案中的圖片轉為 WebP 格式,考慮到本地轉換的耗時較大,這一步更適合放到雲端來做,在雲端處理圖片的服務也相對成熟,大多數雲服務廠商均有提供,且圖片上傳之後也可以顯著減少打包產物的體積。

上傳圖片

第一步是收集專案中的圖片檔案上傳至雲端,file-loader 支援將 import/require() 引入的檔案寫入到目標資料夾並將檔案解析為 url,所以可在 file-loader 的基礎上進行改造,方案是:

  1. 獲取 loader 匹配到的圖片檔案,將圖片上傳至雲端,在雲端生成 WebP 格式的圖片;
  2. 將原始圖片檔案替換為圖片服務生成的URL;
  3. 若圖片上傳失敗則降級為 file-loader 的處理流程,將圖片傳入輸出資料夾。

插入標記

處理過程中有多個地方依賴瀏覽器對 WebP 的相容性,所以需要有一個全域性的標記。通過在 標籤裡注入判斷程式碼,若瀏覽器支援 WebP 格式則在根節點新增"webp"類名標識,可以在頁面渲染前獲取瀏覽器對 WebP 格式的相容性。

前端專案通常會用到 html-webpack-plugin,該外掛可以協助建立 HTML 檔案並自動引入 Webpack 生成的 bundle,外掛中提供有多個 hook,如下圖所示。為確保已經生成了 head 和 body 標籤,可以選擇 在 alterAssetTagGroup 階段注入相關的判斷程式碼。

前端:WebP自適應實踐

替換圖片

接下來則根據全域性標識將圖片替換為相應的 url。根據圖片被引入的位置,分為以下兩種情況:

  1. 圖片在 JS 中被引入。將圖片模組替換為一段 JS 程式碼,根據類名標識返回 WebP 或者通用格式的圖片 url ;
  2. 圖片在 CSS 中被引入。若在 CSS 模組中引入 JS 程式碼,一方面會執行出錯,另一方面 css-loader 會在編譯階段執行這段程式碼,達不到在瀏覽器端判斷 WebP 相容性的目的,因此 CSS 部分需要單獨處理。

處理CSS

處理CSS中引入的圖片有兩種方案:

  1. 利用 CSS 選擇器優先順序。在 CSS 中有圖片引入的類後邊插入帶 webp 類選擇器的樣式,原理同手動替換時處理 CSS 的方式。
  2. 全量生成 WebP 版 CSS。即檔案中引入的圖片皆為 WebP 格式,在連結樣式檔案時判斷根節點類名標識,引入 WebP 版 CSS 或原始 CSS。

第一種方案的缺點是改變了部分樣式的優先順序,可能會影響整體樣式,因此採用第二種方案。

方案實現

本文的方案選擇了火山引擎提供的 veImageX 圖片服務來處理圖片。veImageX 是火山引擎提供的圖片整體解決方案,能夠將圖片轉換為 WebP、HEIF、AVIF等多種格式 ,支援從圖片上傳、儲存、處理到分發的完整流程。

圖片上傳到 veImageX 之後,可以快速接入 veImageX 的各項雲端處理能力,如:裁剪、旋轉、濾鏡以及橡皮擦、內容擦除等多項 AI 處理能力,並且可以方便地通過更換 URL 字尾的方式獲取不同格式的圖片。因此,以下方案實現基於 veImageX 展開。

  1. 接入 veImageX 圖片服務,獲取 accessKey、secretKey 以及服務ID,具體接入方式請參考說明文件;
  2. Webpack 外掛分為兩部分,在 loader 裡上傳並替換圖片,在 plugin 裡生成 webp 類名標記並處理 CSS 中引入的圖片。通過 Webpack loader 獲取專案裡的圖片檔案,藉助火山引擎提供的 SDK 將圖片上傳至 veImageX,並將圖片模組替換為服務生成的 url。基於 veImageX 改變 URL 字尾獲取相應圖片格式的特性,根據圖片被引入的位置做不同的處理。

如下,JS 中引入的圖片根據瀏覽器相容性來判斷,CSS 中引入的圖片則返回通用格式。

result = `var ret = '';

if (typeof document === 'object') {

var format = '';

document.documentElement.classList.forEach(item => { if (item.match(/__(\w+)__/)) format = (item.match(/__(\w+)__/))[1]})

if (format) {

  ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}." + format;

} else {

  ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}.image";

}

} else {

ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}.image";

}

${esModule ? 'export default' : 'module.exports ='} ret`;

該部分單獨封裝成了 veimagex-webpack-loader,支援將專案中的圖片上傳至 veImageX,不需要 WebP 自適應能力的可直接使用該loader。

  1. 在 html-webpack-plugin 的 alterAssetTagGroup hook裡插入瀏覽器 WebP 相容性判斷的程式碼,這部分程式碼在瀏覽器端執行,若瀏覽器支援 WebP 則在根節點新增"webp"類名標識,如下:
compiler.hooks.compilation.tap('ImagexWebpackPlugin', function (compilation) {

    const hooks = self.htmlWebpackPlugin.getHooks(compilation);

    hooks.alterAssetTagGroups.tapAsync(

      'ImagexWebpackPlugin',

      self.checkSupportWebp.bind(self)

    );

  });



ImagexWebpackPlugin.prototype.checkSupportFormat = function (

  htmlPluginData,

  callback

) {

  htmlPluginData.headTags.unshift({

    tagName: 'script',

    closeTag: true,

    attributes: {

      type: 'text/javascript'

    },

    innerHTML: `

      var isSupportFormat = !![].map && document.createElement('canvas').toDataURL('image/${this.options.format}').indexOf('data:image/${this.options.format}') == 0;

      if (isSupportFormat) document.documentElement.classList.add('__${this.options.format}__');

    `

  });

  callback(null, htmlPluginData);

};
  1. 對於 CSS 檔案的處理是全量生成 WebP 版 CSS 的方案,所以在 alterAssetTagGroup hook裡還需要對 引入的 CSS 檔案做處理,將 轉為