性能優化之全面圖片改造方案

背景

在最近觀察業務表現過程中,注意到系統中圖片佔較大比重,但是圖片的加載經常會出現空白閃爍等等的一些體驗問題,部分頁面如下

一些場景的加載卡頓截取

可以看到是典型的圖文爲主的展示頁面,系統內有多處類似的場景。並且加載首屏的圖片資源消耗也是非常耗時,lighthouse 對課程列表的分析結果。圖片比重和大小都偏大。

因此這裏做優化的收益是比較明顯的能給用戶和公司帶來收益的。但是缺少一個系統化的優化流程。

開始之前

在開始之前我們先對一些基本只是有些瞭解,如圖片格式,什麼是無損和有損壓縮。

回顧下圖片格式

既然是說圖片加載,那麼我們先對常見的圖片格式做一個梳理和回顧,因爲格式也是影響圖片加載的一個重要因素,簡單列舉一下常見的圖片格式:

無損 OR 有損

有損壓縮

維基百科定義:有損數據壓縮(英語:lossy compression)是一種數據壓縮 [1] 方法,經過此方法壓縮、解壓的數據會與原始數據不同但是非常接近。有損數據壓縮又稱破壞性資料壓縮、不可逆壓縮。有損數據壓縮藉由將次要的數據捨棄,犧牲一些質量來減少數據量、提高壓縮比。根據各種格式設計的不同,有損數據壓縮都會有代間損失 [2]——每次壓縮與解壓文件都會帶來漸進的質量下降。

由於有損壓縮減少了文件本身的數據量,且以犧牲圖像質量爲代價,因此壓縮後的文件無論是在磁盤佔用還是內存佔用上都會比原始圖像要小。針對於目前探討的圖片加載方式,對應的都是有損壓縮,目標都是更小的內存佔用和更快的解碼速度。

無損壓縮

維基百科定義:無損數據壓縮(Lossless Compression),是指資料經過壓縮 [3] 後,信息不被破壞,還能完全恢復到壓縮前的原樣。相比之下,有損數據壓縮 [4] 只允許一個近似原始資料進行重建,以換取更好的壓縮率。無損數據壓縮在許多應用程序中使用。例如,ZIP[5] 和 gzip[6]。無損數據壓縮通常用於嚴格要求 “經過壓縮、解壓縮的資料必須與原始資料一致” 的場合。

無損壓縮的方法可以通過一些編碼手段,用結構化的數據來減少對重複信息的磁盤佔用,針對圖片來說減少了圖片在磁盤上的空間佔用。但是並不能減少圖像的內存佔用量,這是因爲,當從磁盤或網絡請求上獲取圖像時,瀏覽器又會對圖片進行解碼,把丟失的像素用適當的顏色信息填充進來。

因此如果要減少圖像佔用內存的容量,就必須使用有損壓縮方法。

聊一聊 webp

概念一覽

WebP 是一種現代圖像格式,可爲 Web 上的圖像提供卓越的無損和有損壓縮。使用 WebP 可以創建更小、更豐富的圖像,從而使 Web 更快。與 PNG 相比,WebP 無損圖像的大小要小 26% 。[7] 在同等 SSIM[8] 質量指數下, WebP 有損圖像比可比較的 JPEG 圖像小 25-34% 。[9] 無損 WebP 支持透明度(也稱爲 alpha 通道),成本僅爲 22% 額外字節 [10]。對於可以接受有損 RGB 壓縮的情況,有損 WebP 還支持透明度,通常提供比 PNG 小 3 倍的文件大小。

來個直觀體驗

也可以戳這裏看下社區其他同學做的對比效果 [11],可以看到 webp 在圖片體積和效果上都做的不錯,很適合我們的場景。並且 webp 的使用目前已經比較廣泛,如在 youtube 以及抖音 pc 上都可以看到。

Youtube 部分頁面的截取,在封面圖等大圖場景均使用的 webp 格式

抖音 pc 站

壓縮技術

webp 的壓縮技術基於 VP8[12] 關鍵幀編碼,無損 WebP 壓縮使用已知的圖像片段來精確地重建新的像素,在無法找到相應的匹配值的情況下,使用本地調色板進行優化。在 webp 的開發者平臺已經有詳細的壓縮技術的推演,可以直接戳這裏 [13] 看下。

WebP 應用效果

隨着瀏覽器對 WebP 支持的普及,目前也有越來越多的互聯網開始使用 WebP,這裏分享幾個數據:

結論:無論是技術上還是使用上都已經得到了可行的驗證,並且有明顯收益。

優化思路

圖片的優化分爲加載階段和顯示階段。

加載階段

圖片體積

圖片體積直接反應了網路需要加載的時間,等同於磁盤佔用,因此減少圖片體積能直接減少圖片請求的時間。進而在首屏提升 FCP 等相關指標,讓瀏覽器能更快拿到數據進行繪製。

內存佔用

內存佔用和圖片體積不等同,兩張不同體積的圖片可能有着相同的內存佔用,因此優化內存佔用可以讓瀏覽器解碼圖片和光柵化的時間減少,因爲不需要計算繪製那麼多的圖片信息。光柵化時間的減少直接影響了頁面的渲染速度,以及頁面的卡頓。

顯示階段

加載佔位

佔位圖是爲了給用戶有感知的加載,提升用戶體驗。避免用戶等待過程中的流失。

懶加載

懶加載也已經是當前各種站點的常規優化手段,懶加載儘量減少了不必要的資源請求以提高瀏覽器的渲染效率,減少內存佔用。並顯著減少不必要的帶寬,是爲用戶和公司都省錢的方式。

格式回退

對於瀏覽器對不同格式的圖片支持程度不同,我們的一些優化手段和格式可能不太適用所有瀏覽器,但是爲了保證性能和體驗並最大兼容支持的瀏覽器,我們需要對圖片進行格式降級處理。如對於不支持 webp 的瀏覽器自動降級爲 png。

錯誤佔位

錯誤佔位也是必要的一步,當所有的嘗試都失敗後我們也需要一種良好的方式展示並給用戶感知到。比如目前業務內的錯誤展示。

實踐 - 實驗階段

圖片壓縮

對應於我們優化思路的加載階段,使用公司已有的平臺能力。我們可以獲得不同格式和壓縮比例的圖片。比如我們選擇壓縮比 75 的 webp 以及原圖兩種格式。webp 作爲默認格式,原圖則作爲 backup 的兜底資源。這裏需要注意的是,圖片列表需要服務端的支持,因爲目前系統的圖片是經由服務端返回的鑑權 url,因此這部分需要配合改造。

基本格式如下

type ImgUrlList={
 // 原圖
 origin:string,
 // webp格式
 webp:string,
 // avif格式
 avif:string,
}

模板配置如圖

對於爲什麼圖片地址需要多個,主要是爲了方便我們做回退處理,遇到瀏覽器不兼容的格式就犧牲流量換取可正常展示的圖片,保證內容可見。這裏獲得的圖片格式消費流程如下

通過近一週的站點數據統計,目前業務方瀏覽器數據如下,其中 chrome 佔比 78.66% ,瀏覽器版本 chrome 最低 55,fireforx 最低 99,均在 webp 的支持範圍內。數據均兼容不考慮移動端瀏覽器。由於 IE 也存在極小的比重,所以 IE 應該會是觸發降級佔比最高的。

圖片加載

圖片加載這裏是優化思路的顯示階段的實現,主要包含從加載佔位到失敗佔位的整個流程,當然也包含懶加載。加載我們在觀測階段和穩定階段使用了不同的方式。這裏針對觀測階段的方案展開介紹。最穩定方案是 Picturede 方式,可以在下文穩定階段看到。

觀測主要是爲了有數據對比,這裏我們使用到了 xx 圖片處理包來做圖片加載,主要原因有三:一經過抖音 pc 和西瓜視頻的場景驗證、二集成上報的能力,能夠拿到圖片的相關數據、三提供了圖片加載和回退的支持,滿足當前場景。使用示例如下

import type ImageObserver from 'xxxxxxxxx';
let imgObserver: ImageObserver;

export async function getImgObserver(): Promise<ImageObserver> {
  if (imgObserver) {
    return imgObserver;
  }
  const ImageObserverSDK = import('xxxxxxxxx');
  const LoggerSDK = import('xxxxxxxxx-logger');
  const [imgObserverSdk, logggerSdk] = await Promise.all([ImageObserverSDK, LoggerSDK]);

  const ImageObserver = imgObserverSdk?.default;
  const Logger = logggerSdk?.default;

  if (ImageObserver && Logger) {
    imgObserver = new ImageObserver({
      plugins: [Logger],
      divider: {
        dataSrc: 'src',
        backUpSrc: 'backup-src',
      },
      logger: {
        user_unique_id: 'cccccc', // TODO, 
        app_id: 111111, // TODO,       },
    });
  }
  return imgObserver;
}

本圖片處理包包含了圖片加載錯誤重試的邏輯,跟我們上面圖片壓縮章節設計的圖片列表相結合,可以完成自動回退。

錯誤示例如下,我們給定一個可用地址,其中 src 以及 backup-src 的第一個均不可用,預期是可以自動降級到最後一個可用地址

爲了保證圖片加載流程的可控性,比如在圖片即將出現再去做響應的加載處理。因此一些通用的默認攔截圖片並自動做加載處理的方式就不在適用了,因爲我們沒辦法嚴格控制每個圖片的顯示時間也不好做攔截處理。因此懶加載我們手動通過IntersectionObserver來實現,基本代碼如下, 其中useIntersectionObserverIntersectionObserver的一個實現封裝。

  const observerCb: IntersectionObserverCallback = useCallback((entrys, observer) ={
    const entry = entrys[0];
    if (entry.isIntersecting) {
      setImgVisible(true);
      observer.disconnect();
    }
  }[]);

  const { updateObserverEl } = useIntersectionObserver({
    cb: observerCb,
  });

這樣我們明確控制了每個圖片的加載時機,並對加載結果精細化控制和處理。在一次觀測完成後立即清除觀測,完成一次加載。

加載數據上報

我們通過第一步獲取了可用的幾種格式,因爲我們不知道用戶的瀏覽器會是什麼樣子,所以不能一股腦的都換成 webp 格式,所以我們需要知道 webp 的格式加載成功了多少,我們的圖片加載耗時情況是什麼樣子。有多少是回退到了原圖,加載耗時又是什麼樣子。那當我們有新的方案能不能讓用戶無縫切換過去,怎麼做用戶放量等等問題。因此我們需要對圖片加載做監控。

細心的你可能已經注意到我們圖片加載部分有一個xxxxxxxx-logger,沒錯這個就是用來做上報的,上報流程爲嘗試加載->失敗重試->加載結果->上報。logger 插件會收集加載過程中的圖片信息,加載時長,失敗情況進行上報。這樣我們就能夠根據數據情況查看我們改造的用戶覆蓋度和使用情況,以便我們做後續分析。

優化反推

這一步是對我們優化結果的進一步結論導出,什麼意思呢。以我們加載的圖片類型數據爲例,如果我們的 webp 支持程度很好,那是不是可以實驗性的將 avif 格式作爲下一次的實驗對象來驗證更高的性能。如果我們的圖片每種格式都很慢,那麼我們自然可以反推 cdn 來優化解決方案。同時如果 webp 的不支持,也可以看下我們的降級策略是不是很好的生效了,保證的系統的高可用。等等。因爲我們有了數據支撐,反推變得更加容易。

實踐 - 穩定階段

我們通過上一步的實踐已經完成了我們需要的數據觀測和預期效果。這時我們已經有了圖片在線上的加載耗時,解碼耗時,加載穩定性相關的數據,並且反推了在系統整體設計的上下游對圖片的限制的合理性,比如課程封面場景限制圖片上傳尺寸 10M, 但是這個限制無論如何都嚴重影響加載性能,那降低到 200K 是既滿足需要又不影響性能的適合值,那麼這就是通過實驗階段推導到的優化結果。也是進入穩定階段的重要一步。因此上一步的實驗階段需要儘可能有效的分析全面數據。

上報移除 + 瀏覽器支持

那麼說了一堆之後,我們穩定階段可以做點什麼。當然是期望再優化一點,於是我們做的事情有兩個,一是下掉上一步的監控,二是變更爲瀏覽器處理圖片,同時滿足我們的場景。第一步就比較明顯因爲監控本身是有流量損耗和代碼體積影響的。那麼第二步就是加個 js 處理圖片降級的方式平滑過渡到瀏覽器一支持。於是就有了如下形式的代碼

  const pictureRender = () ={
    const { webp, avif, image } = remain.urlList;
    return (
      <picture>
        <source srcSet={avif} type="image/avif" />
        <source srcSet={webp} type="image/webp" />
        <img src={image} onError={() => onError?.()} {...remain} />
      </picture>
    );
  };

這裏我們使用了 picture 標籤來做圖片的自動降級,關於 picture 標籤的用法和場景可以這篇文章 [14]。總的來說就是做響應式圖片和自動降級的一個比較好的方式。這裏就不展開了。我們通過上面的代碼把我們兼容的格式進行分類指定,以滿足 picture 的使用場景。示例的集中格式會在加載不滿足條件時依次降級。因爲 picture 的加載事件最終還是會落到 img 標籤上,所以我們上面的監聽方式依然適用。

兼容實驗場景和穩定階段

到這裏我們已經總結了穩定階段和實驗階段各自採用的加載策略。但是有一點好處是,這兩者是不衝突的。我們希望繼續保持對新業務場景開啓實驗觀測的能力,穩定業務可以繼續用穩定場景方案。因此我們只需要輕微改造就可以完成這個支持,完整代碼貼在下方。這裏需要注意的是,雖然保留了兩者的能力,但是並不會影響首頁體積,因爲本身 js 監控圖片的方式也是動態加載的,因此除了打包階段會有總包體積的佔用,對系統性能是沒有損耗的。

import { getImgObserver } from '../../utils/observer';
import React, { useRef, useEffect } from 'react';
export const ImageMonitor: React.FC<any> = (props: any) ={
  const { currentref, onError, usePicture, ...remain } = props;
  const imgNode = useRef<HTMLImageElement | null>(null);

  useEffect(() ={
    if (!usePicture) {
      const monitor = async () ={
        const observer = await getImgObserver();
        observer?.observer?.(imgNode.current).then((res: any) ={
          if (res.code !== 0) {                                                    // 加載最終失敗
            onError?.();
          }
        });
      };
      monitor();
    }
  }[]);

  const pictureRender = () ={
    const { webp, avif, image } = remain.urlList;
    return (
      <picture>
        <source srcSet={avif} type="image/avif" />
        <source srcSet={webp} type="image/webp" />
        <img src={image} onError={() => onError?.()} {...remain} />
      </picture>
    );
  };

// 兼容js處理圖片和瀏覽器原生處理圖片
  if (usePicture) {
    return pictureRender();
  }

  return (
    <img
      {...remain}
      ref={el ={
        if (!imgNode.current) {
          imgNode.current = el;
          currentref?.(el);
        }
      }}
      flag="monitor"
    />
  );
};

參考文章

https://segmentfault.com/a/1190000016735421

https://zhuanlan.zhihu.com/p/30534023

https://developers.google.com/speed/webp

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/picture

https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images

歡迎關注公衆號 ELab 團隊 收穫大廠一手好文章~

參考資料

[1]

數據壓縮: https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9

[2]

代間損失: https://zh.wikipedia.org/w/index.php?title=%E4%BB%A3%E9%97%B4%E6%8D%9F%E5%A4%B1&action=edit&redlink=1

[3]

壓縮: https://zh.wikipedia.org/wiki/%E8%B3%87%E6%96%99%E5%A3%93%E7%B8%AE

[4]

有損數據壓縮: https://zh.wikipedia.org/wiki/%E7%A0%B4%E5%A3%9E%E6%80%A7%E8%B3%87%E6%96%99%E5%A3%93%E7%B8%AE

[5]

ZIP: https://zh.wikipedia.org/wiki/ZIP

[6]

gzip: https://zh.wikipedia.org/wiki/Gzip

[7]

小 26% 。: https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results

[8]

SSIM: https://en.wikipedia.org/wiki/Structural_similarity

[9]

小 25-34% 。: https://developers.google.com/speed/webp/docs/webp_study

[10]

22% 額外字節: https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results

[11]

效果: https://isparta.github.io/compare-webp/index.html#12345

[12]

VP8: https://en.wikipedia.org/wiki/VP8

[13]

這裏: https://developers.google.com/speed/webp/docs/compression

[14]

這篇文章: https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AJFFZOlioRCqyohAgagD9g