性能優(yōu)化之全面圖片改造方案
大廠技術(shù) 堅(jiān)持周更 精選好文
背景
在最近觀察業(yè)務(wù)表現(xiàn)過(guò)程中,注意到系統(tǒng)中圖片占較大比重,但是圖片的加載經(jīng)常會(huì)出現(xiàn)空白閃爍等等的一些體驗(yàn)問(wèn)題,部分頁(yè)面如下

一些場(chǎng)景的加載卡頓截取

可以看到是典型的圖文為主的展示頁(yè)面,系統(tǒng)內(nèi)有多處類似的場(chǎng)景。并且加載首屏的圖片資源消耗也是非常耗時(shí),lighthouse對(duì)課程列表的分析結(jié)果。圖片比重和大小都偏大。

因此這里做優(yōu)化的收益是比較明顯的能給用戶和公司帶來(lái)收益的。但是缺少一個(gè)系統(tǒng)化的優(yōu)化流程。
開(kāi)始之前
在開(kāi)始之前我們先對(duì)一些基本只是有些了解,如圖片格式,什么是無(wú)損和有損壓縮。
回顧下圖片格式
既然是說(shuō)圖片加載,那么我們先對(duì)常見(jiàn)的圖片格式做一個(gè)梳理和回顧,因?yàn)楦袷揭彩怯绊憟D片加載的一個(gè)重要因素,簡(jiǎn)單列舉一下常見(jiàn)的圖片格式:
jpg/jpeg
png
gif
WebBP
Avif
Jpeg xl
無(wú)損 OR 有損
有損壓縮
維基百科定義:有損數(shù)據(jù)壓縮(英語(yǔ):lossy compression)是一種數(shù)據(jù)壓縮[1]方法,經(jīng)過(guò)此方法壓縮、解壓的數(shù)據(jù)會(huì)與原始數(shù)據(jù)不同但是非常接近。有損數(shù)據(jù)壓縮又稱破壞性資料壓縮、不可逆壓縮。有損數(shù)據(jù)壓縮借由將次要的數(shù)據(jù)舍棄,犧牲一些質(zhì)量來(lái)減少數(shù)據(jù)量、提高壓縮比。根據(jù)各種格式設(shè)計(jì)的不同,有損數(shù)據(jù)壓縮都會(huì)有代間損失[2]——每次壓縮與解壓文件都會(huì)帶來(lái)漸進(jìn)的質(zhì)量下降。
由于有損壓縮減少了文件本身的數(shù)據(jù)量,且以犧牲圖像質(zhì)量為代價(jià),因此壓縮后的文件無(wú)論是在磁盤占用還是內(nèi)存占用上都會(huì)比原始圖像要小。針對(duì)于目前探討的圖片加載方式,對(duì)應(yīng)的都是有損壓縮,目標(biāo)都是更小的內(nèi)存占用和更快的解碼速度。
無(wú)損壓縮
維基百科定義:無(wú)損數(shù)據(jù)壓縮(Lossless Compression),是指資料經(jīng)過(guò)壓縮[3]后,信息不被破壞,還能完全恢復(fù)到壓縮前的原樣。相比之下,有損數(shù)據(jù)壓縮[4]只允許一個(gè)近似原始資料進(jìn)行重建,以換取更好的壓縮率。無(wú)損數(shù)據(jù)壓縮在許多應(yīng)用程序中使用。例如,ZIP[5]和gzip[6]。無(wú)損數(shù)據(jù)壓縮通常用于嚴(yán)格要求“經(jīng)過(guò)壓縮、解壓縮的資料必須與原始資料一致”的場(chǎng)合。
無(wú)損壓縮的方法可以通過(guò)一些編碼手段,用結(jié)構(gòu)化的數(shù)據(jù)來(lái)減少對(duì)重復(fù)信息的磁盤占用,針對(duì)圖片來(lái)說(shuō)減少了圖片在磁盤上的空間占用。但是并不能減少圖像的內(nèi)存占用量,這是因?yàn)椋?dāng)從磁盤或網(wǎng)絡(luò)請(qǐng)求上獲取圖像時(shí),瀏覽器又會(huì)對(duì)圖片進(jìn)行解碼,把丟失的像素用適當(dāng)?shù)念伾畔⑻畛溥M(jìn)來(lái)。
因此如果要減少圖像占用內(nèi)存的容量,就必須使用有損壓縮方法。
聊一聊webp
概念一覽
WebP 是一種現(xiàn)代圖像格式,可為 Web 上的圖像提供卓越的無(wú)損和有損壓縮。使用 WebP可以創(chuàng)建更小、更豐富的圖像,從而使 Web 更快。與 PNG 相比,WebP 無(wú)損圖像的大小要小 26% 。[7]在同等 SSIM[8]質(zhì)量指數(shù)下, WebP 有損圖像比可比較的 JPEG 圖像小 25-34% 。[9]無(wú)損 WebP支持透明度(也稱為 alpha 通道),成本僅為22% 額外字節(jié)[10]。對(duì)于可以接受有損 RGB 壓縮的情況,有損 WebP 還支持透明度,通常提供比 PNG 小 3 倍的文件大小。
來(lái)個(gè)直觀體驗(yàn)

也可以戳這里看下社區(qū)其他同學(xué)做的對(duì)比效果[11],可以看到webp在圖片體積和效果上都做的不錯(cuò),很適合我們的場(chǎng)景。并且webp的使用目前已經(jīng)比較廣泛,如在youtube以及抖音pc上都可以看到。
Youtube 部分頁(yè)面的截取,在封面圖等大圖場(chǎng)景均使用的webp格式

抖音pc站

壓縮技術(shù)
webp的壓縮技術(shù)基于 VP8[12]關(guān)鍵幀編碼,無(wú)損 WebP 壓縮使用已知的圖像片段來(lái)精確地重建新的像素,在無(wú)法找到相應(yīng)的匹配值的情況下,使用本地調(diào)色板進(jìn)行優(yōu)化。在webp的開(kāi)發(fā)者平臺(tái)已經(jīng)有詳細(xì)的壓縮技術(shù)的推演,可以直接戳這里[13]看下。
WebP 應(yīng)用效果
隨著瀏覽器對(duì) WebP 支持的普及,目前也有越來(lái)越多的互聯(lián)網(wǎng)開(kāi)始使用 WebP,這里分享幾個(gè)數(shù)據(jù):
YouTube 的視頻略縮圖采用 WebP 后,網(wǎng)頁(yè)加載速度提升了 10%;
Google Chrome 應(yīng)用商店采用 WebP 后,每天可以節(jié)省幾 TB 的帶寬,頁(yè)面加載時(shí)間減少了30% 左右;
花瓣網(wǎng)在 2017 年 5 月開(kāi)啟 WebP 后,在網(wǎng)站總體請(qǐng)求量沒(méi)有減少的情況下,整體帶寬下降了近 50%。
結(jié)論:無(wú)論是技術(shù)上還是使用上都已經(jīng)得到了可行的驗(yàn)證,并且有明顯收益。
優(yōu)化思路
圖片的優(yōu)化分為加載階段和顯示階段。
加載階段
圖片體積
圖片體積直接反應(yīng)了網(wǎng)路需要加載的時(shí)間,等同于磁盤占用,因此減少圖片體積能直接減少圖片請(qǐng)求的時(shí)間。進(jìn)而在首屏提升FCP等相關(guān)指標(biāo),讓瀏覽器能更快拿到數(shù)據(jù)進(jìn)行繪制。
內(nèi)存占用
內(nèi)存占用和圖片體積不等同,兩張不同體積的圖片可能有著相同的內(nèi)存占用,因此優(yōu)化內(nèi)存占用可以讓瀏覽器解碼圖片和光柵化的時(shí)間減少,因?yàn)椴恍枰?jì)算繪制那么多的圖片信息。光柵化時(shí)間的減少直接影響了頁(yè)面的渲染速度,以及頁(yè)面的卡頓。
顯示階段
加載占位
占位圖是為了給用戶有感知的加載,提升用戶體驗(yàn)。避免用戶等待過(guò)程中的流失。
懶加載
懶加載也已經(jīng)是當(dāng)前各種站點(diǎn)的常規(guī)優(yōu)化手段,懶加載盡量減少了不必要的資源請(qǐng)求以提高瀏覽器的渲染效率,減少內(nèi)存占用。并顯著減少不必要的帶寬,是為用戶和公司都省錢的方式。
格式回退
對(duì)于瀏覽器對(duì)不同格式的圖片支持程度不同,我們的一些優(yōu)化手段和格式可能不太適用所有瀏覽器,但是為了保證性能和體驗(yàn)并最大兼容支持的瀏覽器,我們需要對(duì)圖片進(jìn)行格式降級(jí)處理。如對(duì)于不支持webp的瀏覽器自動(dòng)降級(jí)為png。
錯(cuò)誤占位
錯(cuò)誤占位也是必要的一步,當(dāng)所有的嘗試都失敗后我們也需要一種良好的方式展示并給用戶感知到。比如目前業(yè)務(wù)內(nèi)的錯(cuò)誤展示。

實(shí)踐-實(shí)驗(yàn)階段
圖片壓縮
對(duì)應(yīng)于我們優(yōu)化思路的加載階段,使用公司已有的平臺(tái)能力。我們可以獲得不同格式和壓縮比例的圖片。比如我們選擇壓縮比75的webp以及原圖兩種格式。webp作為默認(rèn)格式,原圖則作為backup的兜底資源。這里需要注意的是,圖片列表需要服務(wù)端的支持,因?yàn)槟壳跋到y(tǒng)的圖片是經(jīng)由服務(wù)端返回的鑒權(quán)url,因此這部分需要配合改造。
基本格式如下
type ImgUrlList={
// 原圖
origin:string,
// webp格式
webp:string,
// avif格式
avif:string,
}
模板配置如圖

對(duì)于為什么圖片地址需要多個(gè),主要是為了方便我們做回退處理,遇到瀏覽器不兼容的格式就犧牲流量換取可正常展示的圖片,保證內(nèi)容可見(jiàn)。這里獲得的圖片格式消費(fèi)流程如下

通過(guò)近一周的站點(diǎn)數(shù)據(jù)統(tǒng)計(jì),目前業(yè)務(wù)方瀏覽器數(shù)據(jù)如下,其中chrome占比78.66% ,瀏覽器版本chrome最低55,fireforx最低99,均在webp的支持范圍內(nèi)。數(shù)據(jù)均兼容不考慮移動(dòng)端瀏覽器。由于IE也存在極小的比重,所以IE應(yīng)該會(huì)是觸發(fā)降級(jí)占比最高的。

圖片加載
圖片加載這里是優(yōu)化思路的顯示階段的實(shí)現(xiàn),主要包含從加載占位到失敗占位的整個(gè)流程,當(dāng)然也包含懶加載。加載我們?cè)谟^測(cè)階段和穩(wěn)定階段使用了不同的方式。這里針對(duì)觀測(cè)階段的方案展開(kāi)介紹。最穩(wěn)定方案是Picturede 方式,可以在下文穩(wěn)定階段看到。
觀測(cè)主要是為了有數(shù)據(jù)對(duì)比,這里我們使用到了xx圖片處理包來(lái)做圖片加載,主要原因有三:一經(jīng)過(guò)抖音pc和西瓜視頻的場(chǎng)景驗(yàn)證、二集成上報(bào)的能力,能夠拿到圖片的相關(guān)數(shù)據(jù)、三提供了圖片加載和回退的支持,滿足當(dāng)前場(chǎng)景。使用示例如下
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;
}本圖片處理包包含了圖片加載錯(cuò)誤重試的邏輯,跟我們上面圖片壓縮章節(jié)設(shè)計(jì)的圖片列表相結(jié)合,可以完成自動(dòng)回退。
錯(cuò)誤示例如下,我們給定一個(gè)可用地址,其中src以及backup-src的第一個(gè)均不可用,預(yù)期是可以自動(dòng)降級(jí)到最后一個(gè)可用地址
為了保證圖片加載流程的可控性,比如在圖片即將出現(xiàn)再去做響應(yīng)的加載處理。因此一些通用的默認(rèn)攔截圖片并自動(dòng)做加載處理的方式就不在適用了,因?yàn)槲覀儧](méi)辦法嚴(yán)格控制每個(gè)圖片的顯示時(shí)間也不好做攔截處理。因此懶加載我們手動(dòng)通過(guò)IntersectionObserver來(lái)實(shí)現(xiàn),基本代碼如下,其中useIntersectionObserver是IntersectionObserver的一個(gè)實(shí)現(xiàn)封裝。
const observerCb: IntersectionObserverCallback = useCallback((entrys, observer) => {
const entry = entrys[0];
if (entry.isIntersecting) {
setImgVisible(true);
observer.disconnect();
}
}, []);
const { updateObserverEl } = useIntersectionObserver({
cb: observerCb,
});
這樣我們明確控制了每個(gè)圖片的加載時(shí)機(jī),并對(duì)加載結(jié)果精細(xì)化控制和處理。在一次觀測(cè)完成后立即清除觀測(cè),完成一次加載。
加載數(shù)據(jù)上報(bào)
我們通過(guò)第一步獲取了可用的幾種格式,因?yàn)槲覀儾恢烙脩舻臑g覽器會(huì)是什么樣子,所以不能一股腦的都換成webp格式,所以我們需要知道webp的格式加載成功了多少,我們的圖片加載耗時(shí)情況是什么樣子。有多少是回退到了原圖,加載耗時(shí)又是什么樣子。那當(dāng)我們有新的方案能不能讓用戶無(wú)縫切換過(guò)去,怎么做用戶放量等等問(wèn)題。因此我們需要對(duì)圖片加載做監(jiān)控。
細(xì)心的你可能已經(jīng)注意到我們圖片加載部分有一個(gè)xxxxxxxx-logger,沒(méi)錯(cuò)這個(gè)就是用來(lái)做上報(bào)的,上報(bào)流程為嘗試加載->失敗重試->加載結(jié)果->上報(bào)。logger插件會(huì)收集加載過(guò)程中的圖片信息,加載時(shí)長(zhǎng),失敗情況進(jìn)行上報(bào)。這樣我們就能夠根據(jù)數(shù)據(jù)情況查看我們改造的用戶覆蓋度和使用情況,以便我們做后續(xù)分析。
優(yōu)化反推
這一步是對(duì)我們優(yōu)化結(jié)果的進(jìn)一步結(jié)論導(dǎo)出,什么意思呢。以我們加載的圖片類型數(shù)據(jù)為例,如果我們的webp支持程度很好,那是不是可以實(shí)驗(yàn)性的將avif格式作為下一次的實(shí)驗(yàn)對(duì)象來(lái)驗(yàn)證更高的性能。如果我們的圖片每種格式都很慢,那么我們自然可以反推cdn來(lái)優(yōu)化解決方案。同時(shí)如果webp的不支持,也可以看下我們的降級(jí)策略是不是很好的生效了,保證的系統(tǒng)的高可用。等等。因?yàn)槲覀冇辛藬?shù)據(jù)支撐,反推變得更加容易。
實(shí)踐-穩(wěn)定階段
我們通過(guò)上一步的實(shí)踐已經(jīng)完成了我們需要的數(shù)據(jù)觀測(cè)和預(yù)期效果。這時(shí)我們已經(jīng)有了圖片在線上的加載耗時(shí),解碼耗時(shí),加載穩(wěn)定性相關(guān)的數(shù)據(jù),并且反推了在系統(tǒng)整體設(shè)計(jì)的上下游對(duì)圖片的限制的合理性,比如課程封面場(chǎng)景限制圖片上傳尺寸10M,但是這個(gè)限制無(wú)論如何都嚴(yán)重影響加載性能,那降低到200K是既滿足需要又不影響性能的適合值,那么這就是通過(guò)實(shí)驗(yàn)階段推導(dǎo)到的優(yōu)化結(jié)果。也是進(jìn)入穩(wěn)定階段的重要一步。因此上一步的實(shí)驗(yàn)階段需要盡可能有效的分析全面數(shù)據(jù)。
上報(bào)移除+瀏覽器支持
那么說(shuō)了一堆之后,我們穩(wěn)定階段可以做點(diǎn)什么。當(dāng)然是期望再優(yōu)化一點(diǎn),于是我們做的事情有兩個(gè),一是下掉上一步的監(jiān)控,二是變更為瀏覽器處理圖片,同時(shí)滿足我們的場(chǎng)景。第一步就比較明顯因?yàn)楸O(jiān)控本身是有流量損耗和代碼體積影響的。那么第二步就是加個(gè)js處理圖片降級(jí)的方式平滑過(guò)渡到瀏覽器一支持。于是就有了如下形式的代碼
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標(biāo)簽來(lái)做圖片的自動(dòng)降級(jí),關(guān)于picture標(biāo)簽的用法和場(chǎng)景可以這篇文章[14]。總的來(lái)說(shuō)就是做響應(yīng)式圖片和自動(dòng)降級(jí)的一個(gè)比較好的方式。這里就不展開(kāi)了。我們通過(guò)上面的代碼把我們兼容的格式進(jìn)行分類指定,以滿足picture的使用場(chǎng)景。示例的集中格式會(huì)在加載不滿足條件時(shí)依次降級(jí)。因?yàn)閜icture的加載事件最終還是會(huì)落到img標(biāo)簽上,所以我們上面的監(jiān)聽(tīng)方式依然適用。
兼容實(shí)驗(yàn)場(chǎng)景和穩(wěn)定階段
到這里我們已經(jīng)總結(jié)了穩(wěn)定階段和實(shí)驗(yàn)階段各自采用的加載策略。但是有一點(diǎn)好處是,這兩者是不沖突的。我們希望繼續(xù)保持對(duì)新業(yè)務(wù)場(chǎng)景開(kāi)啟實(shí)驗(yàn)觀測(cè)的能力,穩(wěn)定業(yè)務(wù)可以繼續(xù)用穩(wěn)定場(chǎng)景方案。因此我們只需要輕微改造就可以完成這個(gè)支持,完整代碼貼在下方。這里需要注意的是,雖然保留了兩者的能力,但是并不會(huì)影響首頁(yè)體積,因?yàn)楸旧韏s監(jiān)控圖片的方式也是動(dòng)態(tài)加載的,因此除了打包階段會(huì)有總包體積的占用,對(duì)系統(tǒng)性能是沒(méi)有損耗的。
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"
/>
);
};
?? 謝謝支持
以上便是本次分享的全部?jī)?nèi)容,希望對(duì)你有所幫助^_^
喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。
歡迎關(guān)注公眾號(hào) 趣談前端 收貨大廠一手好文章~

從零搭建全棧可視化大屏制作平臺(tái)V6.Dooring
點(diǎn)個(gè)在看你最好看
