大廠技術堅持周更精選好文背景
在最近觀察業務表現過程中,注意到系統中圖片占較大比重,但是圖片的加載經常會出現空白閃爍等等的一些體驗問題,部分頁面如下
一些場景的加載卡頓截取
可以看到是典型的圖文為主的展示頁面,系統內有多處類似的場景。并且加載首屏的圖片資源消耗也是非常耗時,對課程列表的分析結果。圖片比重和大小都偏大。
因此這里做優化的收益是比較明顯的能給用戶和公司帶來收益的。但是缺少一個系統化的優化流程。
開始之前
在開始之前我們先對一些基本只是有些了解,如圖片格式,什么是無損和有損壓縮。
回顧下圖片格式
既然是說圖片加載,那么我們先對常見的圖片格式做一個梳理和回顧,因為格式也是影響圖片加載的一個重要因素,簡單列舉一下常見的圖片格式:
無損 OR 有損有損壓縮
維基百科定義:有損數據壓縮(英語:lossy )是一種數據壓縮[1]方法,經過此方法壓縮、解壓的數據會與原始數據不同但是非常接近。有損數據壓縮又稱破壞性資料壓縮、不可逆壓縮。有損數據壓縮借由將次要的數據舍棄,犧牲一些質量來減少數據量、提高壓縮比。根據各種格式設計的不同,有損數據壓縮都會有代間損失[2]——每次壓縮與解壓文件都會帶來漸進的質量下降。
由于有損壓縮減少了文件本身的數據量,且以犧牲圖像質量為代價,因此壓縮后的文件無論是在磁盤占用還是內存占用上都會比原始圖像要小。針對于目前探討的圖片加載方式,對應的都是有損壓縮,目標都是更小的內存占用和更快的解碼速度。
無損壓縮
維基百科定義:無損數據壓縮( ),是指資料經過壓縮[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的使用目前已經比較廣泛,如在以及抖音pc上都可以看到。
部分頁面的截取,在封面圖等大圖場景均使用的webp格式
抖音pc站
壓縮技術
webp的壓縮技術基于 VP8[12]關鍵幀編碼,無損 WebP 壓縮使用已知的圖像片段來精確地重建新的像素,在無法找到相應的匹配值的情況下,使用本地調色板進行優化。在webp的開發者平臺已經有詳細的壓縮技術的推演,可以直接戳這里[13]看下。
WebP 應用效果
隨著瀏覽器對 WebP 支持的普及,目前也有越來越多的互聯網開始使用 WebP,這里分享幾個數據:
結論:無論是技術上還是使用上都已經得到了可行的驗證,并且有明顯收益。
優化思路
圖片的優化分為加載階段和顯示階段。
加載階段圖片體積
圖片體積直接反應了網路需要加載的時間,等同于磁盤占用,因此減少圖片體積能直接減少圖片請求的時間。進而在首屏提升FCP等相關指標,讓瀏覽器能更快拿到數據進行繪制。
內存占用
內存占用和圖片體積不等同,兩張不同體積的圖片可能有著相同的內存占用,因此優化內存占用可以讓瀏覽器解碼圖片和光柵化的時間減少,因為不需要計算繪制那么多的圖片信息。光柵化時間的減少直接影響了頁面的渲染速度,以及頁面的卡頓。
顯示階段加載占位
占位圖是為了給用戶有感知的加載,提升用戶體驗。避免用戶等待過程中的流失。
懶加載也已經是當前各種站點的常規優化手段,懶加載盡量減少了不必要的資源請求以提高瀏覽器的渲染效率,減少內存占用。并顯著減少不必要的帶寬,是為用戶和公司都省錢的方式。
格式回退
對于瀏覽器對不同格式的圖片支持程度不同,我們的一些優化手段和格式可能不太適用所有瀏覽器,但是為了保證性能和體驗并最大兼容支持的瀏覽器,我們需要對圖片進行格式降級處理。如對于不支持webp的瀏覽器自動降級為png。
錯誤占位
錯誤占位也是必要的一步,當所有的嘗試都失敗后我們也需要一種良好的方式展示并給用戶感知到。比如目前業務內的錯誤展示。
實踐-實驗階段圖片壓縮
對應于我們優化思路的加載階段,使用公司已有的平臺能力。我們可以獲得不同格式和壓縮比例的圖片。比如我們選擇壓縮比75的webp以及原圖兩種格式。webp作為默認格式,原圖則作為的兜底資源。這里需要注意的是,圖片列表需要服務端的支持,因為目前系統的圖片是經由服務端返回的鑒權url,因此這部分需要配合改造。
基本格式如下
type?ImgUrlList={
?//?原圖
?origin:string,
?//?webp格式
?webp:string,
?//?avif格式
?avif:string,
}
模板配置如圖
對于為什么圖片地址需要多個,主要是為了方便我們做回退處理,遇到瀏覽器不兼容的格式就犧牲流量換取可正常展示的圖片,保證內容可見。這里獲得的圖片格式消費流程如下:
通過近一周的站點數據統計,目前業務方瀏覽器數據如下,其中占比78.66% ,瀏覽器版本最低55,最低99,均在webp的支持范圍內。數據均兼容不考慮移動端瀏覽器。由于IE也存在極小的比重,所以IE應該會是觸發降級占比最高的。
圖片加載
圖片加載這里是優化思路的顯示階段的實現,主要包含從加載占位到失敗占位的整個流程,當然也包含懶加載。加載我們在觀測階段和穩定階段使用了不同的方式。這里針對觀測階段的方案展開介紹。最穩定方案是 方式,可以在下文穩定階段看到。
觀測主要是為了有數據對比,這里我們使用到了xx圖片處理包來做圖片加載,主要原因有三:一經過抖音pc和西瓜視頻的場景驗證、二集成上報的能力,能夠拿到圖片的相關數據、三提供了圖片加載和回退的支持,滿足當前場景。使用示例如下
import?type?ImageObserver?from?'xxxxxxxxx';
let?imgObserver:?ImageObserver;
export?async?function?getImgObserver():?Promise?{
??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以及-src的第一個均不可用,預期是可以自動降級到最后一個可用地址
為了保證圖片加載流程的可控性,比如在圖片即將出現再去做響應的加載處理。因此一些通用的默認攔截圖片并自動做加載處理的方式就不在適用了,因為我們沒辦法嚴格控制每個圖片的顯示時間也不好做攔截處理。因此懶加載我們手動通過來實現,基本代碼如下,其中ver是的一個實現封裝。
??const?observerCb:?IntersectionObserverCallback?=?useCallback((entrys,?observer)?=>?{
????const?entry?=?entrys[0];
????if?(entry.isIntersecting)?{
??????setImgVisible(true);
??????observer.disconnect();
????}
??},?[]);
??const?{?updateObserverEl?}?=?useIntersectionObserver({
????cb:?observerCb,
??});
這樣我們明確控制了每個圖片的加載時機,并對加載結果精細化控制和處理。在一次觀測完成后立即清除觀測圖像壓縮技術好不好,完成一次加載。
加載數據上報
我們通過第一步獲取了可用的幾種格式,因為我們不知道用戶的瀏覽器會是什么樣子,所以不能一股腦的都換成webp格式圖像壓縮技術好不好,所以我們需要知道webp的格式加載成功了多少,我們的圖片加載耗時情況是什么樣子。有多少是回退到了原圖,加載耗時又是什么樣子。那當我們有新的方案能不能讓用戶無縫切換過去,怎么做用戶放量等等問題。因此我們需要對圖片加載做監控。
細心的你可能已經注意到我們圖片加載部分有一個-,沒錯這個就是用來做上報的,上報流程為嘗試加載->失敗重試->加載結果->上報。插件會收集加載過程中的圖片信息,加載時長,失敗情況進行上報。這樣我們就能夠根據數據情況查看我們改造的用戶覆蓋度和使用情況,以便我們做后續分析。
優化反推
這一步是對我們優化結果的進一步結論導出,什么意思呢。以我們加載的圖片類型數據為例,如果我們的webp支持程度很好,那是不是可以實驗性的將avif格式作為下一次的實驗對象來驗證更高的性能。如果我們的圖片每種格式都很慢,那么我們自然可以反推cdn來優化解決方案。同時如果webp的不支持,也可以看下我們的降級策略是不是很好的生效了,保證的系統的高可用。等等。因為我們有了數據支撐,反推變得更加容易。
實踐-穩定階段
我們通過上一步的實踐已經完成了我們需要的數據觀測和預期效果。這時我們已經有了圖片在線上的加載耗時,解碼耗時,加載穩定性相關的數據,并且反推了在系統整體設計的上下游對圖片的限制的合理性,比如課程封面場景限制圖片上傳尺寸10M,但是這個限制無論如何都嚴重影響加載性能,那降低到200K是既滿足需要又不影響性能的適合值,那么這就是通過實驗階段推導到的優化結果。也是進入穩定階段的重要一步。因此上一步的實驗階段需要盡可能有效的分析全面數據。
上報移除+瀏覽器支持
那么說了一堆之后,我們穩定階段可以做點什么。當然是期望再優化一點,于是我們做的事情有兩個,一是下掉上一步的監控,二是變更為瀏覽器處理圖片,同時滿足我們的場景。第一步就比較明顯因為監控本身是有流量損耗和代碼體積影響的。那么第二步就是加個js處理圖片降級的方式平滑過渡到瀏覽器一支持。于是就有了如下形式的代碼
??const?pictureRender?=?()?=>?{
????const?{?webp,?avif,?image?}?=?remain.urlList;
????return?(
??????
????????<source?srcSet={avif}?type="image/avif"?/>
????????<source?srcSet={webp}?type="image/webp"?/>
?????????onError?.()}?{...remain}?/>
??????
????);
??};
這里我們使用了標簽來做圖片的自動降級,關于標簽的用法和場景可以這篇文章[14]。總的來說就是做響應式圖片和自動降級的一個比較好的方式。這里就不展開了。我們通過上面的代碼把我們兼容的格式進行分類指定,以滿足的使用場景。示例的集中格式會在加載不滿足條件時依次降級。因為的加載事件最終還是會落到img標簽上,所以我們上面的監聽方式依然適用。
兼容實驗場景和穩定階段
到這里我們已經總結了穩定階段和實驗階段各自采用的加載策略。但是有一點好處是,這兩者是不沖突的。我們希望繼續保持對新業務場景開啟實驗觀測的能力,穩定業務可以繼續用穩定場景方案。因此我們只需要輕微改造就可以完成這個支持,完整代碼貼在下方。這里需要注意的是,雖然保留了兩者的能力,但是并不會影響首頁體積,因為本身js監控圖片的方式也是動態加載的,因此除了打包階段會有總包體積的占用,對系統性能是沒有損耗的。
import?{?getImgObserver?}?from?'../../utils/observer';
import?React,?{?useRef,?useEffect?}?from?'react';
export?const?ImageMonitor:?React.FC?=?(props:?any)?=>?{
??const?{?currentref,?onError,?usePicture,?...remain?}?=?props;
??const?imgNode?=?useRef(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?(
??????
????????<source?srcSet={avif}?type="image/avif"?/>
????????<source?srcSet={webp}?type="image/webp"?/>
?????????onError?.()}?{...remain}?/>
??????
????);
??};
//?兼容js處理圖片和瀏覽器原生處理圖片
??if?(usePicture)?{
????return?pictureRender();
??}
??return?(
??????????{...remain}
??????ref={el?=>?{
????????if?(!imgNode.current)?{
??????????imgNode.current?=?el;
??????????currentref?.(el);
????????}
??????}}
??????flag="monitor"
????/>
??);
};
?? 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助^_^