京東PLUS前端H5性能優(yōu)化實踐
?前言
隨著移動互聯(lián)網(wǎng)的發(fā)展,用戶對產(chǎn)品的使用體驗要求越來越高。H5 作為業(yè)務的重要載體,應用非常廣泛,如何把控好 H5 的性能是一門重要的工作。因此 H5 頁面性能是一個非常核心的用戶體驗指標。
研究表明一個網(wǎng)頁加載的快慢會直接影響用戶在這個頁面逗留的時長,用戶在發(fā)現(xiàn)頁面有內(nèi)容呈現(xiàn)的時候,如果在頁面進行了點擊但是頁面沒有立即響應,用戶通常會認為頁面加載出現(xiàn)了延遲;如果頁面在加載過程中某些元素發(fā)生了大幅度的偏移,這對于用戶來說體驗是相當糟糕的。
因此提高用戶體驗應從頁面的渲染速度、響應能力及頁面的抖動情況考慮,目前比較主流的衡量頁面指標的參數(shù)是谷歌 lighthouse 提出的幾個指標, 主要包括 FCP(First Contentful Paint), LCP(Largest Contentful Paint), CLS(Cumulative Layout Shift), TTI(Time To Interactive), TBT(Total Blocking Time)等。下面我們將從FCP、LCP、CLS、TTI、TBT五個指標來進行一一介紹。
FCP
FCP(First Contentful Paint)是指頁面從開始加載到頁面內(nèi)容的任何部分在屏幕上完成渲染的時間,也就是我們通常所說的首次內(nèi)容繪制時間,它是測量感知加載速度的一個重要指標。那么什么“內(nèi)容”才算FCP的呢,其中包含了文本、圖像(包括背景圖像)、<svg>元素或非白色的<canvas>元素。
LCP
LCP(Largest Contentful Paint)是根據(jù)頁面首次開始加載的時間點來報告可視區(qū)域內(nèi)最大圖像或文本塊完成渲染的相對時間。最大內(nèi)容繪制表示著頁面的主要內(nèi)容已經(jīng)加載完成,它的檢測是通過Element Timming API來提供支持,使用的也是通過性能監(jiān)聽事件的一個方式。那哪些元素在LCP的計算之內(nèi)呢?除了常見的<img>元素,<video>元素、通過url元素加載的背景圖像以及包含文本節(jié)點或其他行內(nèi)級文本元素子元素的塊級元素都在計算范圍之內(nèi)。需要注意的是LCP的計算是一個動態(tài)的過程。
CLS
CLS(Cumulative Layout Shift)是指整個頁面中所有意外布局偏移中最大一連串的布局偏移分數(shù),即我們通常所說的累計布局偏移,其中一連串的布局偏移,也叫會話窗口,是指一個或多個連續(xù)發(fā)生的單次布局偏移,每次偏移相隔的時間少于1秒,且整個窗口的最大持續(xù)時長為5秒。就像下圖中的灰色文字區(qū)域在頁面加載時如果發(fā)生了位移,那么它的位移就會被計入CLS的分數(shù)。
TTI
TTI(Time To Interactive)是指頁面從開始加載到主要子資源完成渲染,并且能夠快速、可靠的響應用戶輸入的時間,通常我們也叫可交互時間。那么TTI是如何計算的呢,如下圖首先沿時間軸正向搜索時長至少為5秒的安靜窗口(安靜窗口是指沒有長任務且不超過兩個正在處理的網(wǎng)絡get請求),然后沿時間軸反向搜索安靜窗口之前的最后一個長任務,如果沒有找到長任務,則在FCP步驟停止執(zhí)行,TTI就是安靜窗口之前最后一個長任務的結束時間,如果沒有找到長任務的話,則與FCP值相同。
TBT
TBT(Total Blocking Time)是測量FCP與TTI之間的總時間,這期間,主線程被阻塞的時間過長,無法作出輸入響應。我們通常把任務時間超過50毫秒的任務稱之為長任務,那么一個頁面的總阻塞時間其實就是FCP和TTI之間發(fā)生每個長任務的阻塞時間總和。如下圖淡紅色區(qū)域的時間總和就是這個頁面的TBT分數(shù)。
如何檢測頁面性能?
在介紹了相關的指標之后,如何知道一個頁面性能的好壞呢?我們PLUS團隊聯(lián)合燭龍團隊在基于lighthouse的基礎上,結合我們京東實際的業(yè)務情況,共同開發(fā)了一套性能監(jiān)控的SDK,在其中明確的提出了H5和PC兩套性能評分標準,并且給出了性能良好與較差的閾值。
如何做性能優(yōu)化?
在深入了解各項指標之后,我們來看一下如何對頁面進行性能優(yōu)化。一個頁面從輸入URL到頁面最后在瀏覽器的渲染流程大致如下:
- URL解析 => DNS解析 => 建立TCP連接 => 客戶端發(fā)送請求 => 服務器處理和響應請求 => 瀏覽器解析并渲染響應內(nèi)容 => 斷開TCP鏈接
可以看到其中從URL解析開始到服務器響應請求的過程都是屬于網(wǎng)絡層面,瀏覽器解析渲染相應內(nèi)容屬于渲染層面,接下來我們就將從網(wǎng)絡層面和渲染層面這兩個大方向來看一下如何去做性能優(yōu)化。
我們都知道,在網(wǎng)絡層面其實對于前端工程師來講可優(yōu)化的點并不多,但是通過網(wǎng)絡層面的優(yōu)化,可以極大程度上降低TTFB的等待時間,從而提高FCP、LCP、TTI、TBT這些指標的分數(shù),下面是一些常見優(yōu)化方式:
- 緩存技術:我們可以合理的使用緩存,如CDN緩存、瀏覽器緩存以及應用離線包,來減少資源的請求。除了這些,還可以在接口中設置Expires 和 Cache-Control 利用http緩存來減少接口的請求時間。
- 服務端還可以使用http2/http3、減少重定向等方式來保證我們的接口響應速度。
在了解了網(wǎng)絡層面的一些優(yōu)化方式之后,我們主要來看下在渲染層面如何進行優(yōu)化,首先,我們來看一下瀏覽器的渲染機制,大概分為以下幾個步驟:
- 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹。
- 將DOM樹和CSSOM樹結合,生成渲染樹。
- 計算圖層布局。
- 繪制圖層。
- 合成圖層,顯示頁面。
在解析HTML的過程中會去請求頁面所需的js/css等靜態(tài)資源,對于資源請求這部分,我們可以進行一些特定的優(yōu)化,讓我們的資源能夠更快速的進行加載,減少TTFB等待時間,從而提高FCP、LCP、TTI以及TBT的分數(shù),比如:
- 可以通過添加preconnect和dns-prefetch屬性預鏈接所需要的源,其中preconnect可以提前建立連接,但是如果在10秒內(nèi)沒有使用連接,瀏覽器會關閉它,從而浪費一些早期的連接占用CPU的時間,dns-prefetch會在請求資源時提前進行域名解析。
- 如果我們想要提前加載資源,可以添加preload屬性來預先加載一些關鍵的資源,幫助頁面更快的進行渲染。
- 還可以將不重要的script腳本添加async和defer屬性,然后提取重要的css,多余的css分割成不同的文件,將不重要的文件添加media屬性來消除阻塞渲染。
如果想更快的完成資源的請求,我們也可以對資源進行優(yōu)化,使產(chǎn)出的文件體積更小,文件請求耗時更短,這也能使我們的FCP和LCP分數(shù)得以提高:
- 移除未使用的css/js,如何能夠快速檢查出哪些代碼我們有用到呢,可以打開Chrome => 開發(fā)者工具 => shift + alt + p 搜索Coverage Tab,點擊刷新可以看到資源的請求狀況以及可用代碼覆蓋率,然后點擊打開每個資源,可以查看具體內(nèi)容有沒有被使用,其中綠色部分為關鍵代碼,紅色部分為非關鍵代碼,可以通過刪除紅色部分代碼來減小靜態(tài)資源的體積。
- 通過webpack/rollup/gulp等構建工具對代碼進行打包壓縮
- 對資源進行gzip壓縮
- 將資源進行拆分(包括css、js),并按需引用,并且延遲加載優(yōu)先級較低的js
- 對于圖片我們可以使用工具進行圖像壓縮,也可以使用圖像cdn,格式最好使用webp,將大大減少圖片的大小,對于大型的gif可以轉為視頻
在解析完html之后,瀏覽器會識別并加載所有的 CSS 樣式信息與 DOM 樹合并,最終生成頁面渲染樹,我們在寫css的時候,需要注意以下幾點,這樣可以使我們的頁面能夠更快速的渲染出來。
- 減少css選擇器的層級,避免多層嵌套
- 避免使用較多的css表達式
- 對于css動畫,盡量使用 transform(transform可開啟硬件加速)
- 對于一些特定的動畫,可以選擇使用requestAnimationFrame 代替
當瀏覽器生成渲染樹以后,就會根據(jù)渲染樹來進行布局,然后圖層會進行計算和繪制,最后合成圖層在頁面顯示出來。在這個渲染的過程中,我們可以進行以下的優(yōu)化:
- 避免DOM過大或者多層嵌套
- 避免批量的對DOM進行修改
除了對網(wǎng)絡和渲染兩個層面優(yōu)化之外,我們也可以針對特定的指標進行優(yōu)化。
1. 對于FCP和LCP,還可以在HTML中增加骨架屏,或者使用服務端渲染的方式進行優(yōu)化,同時也可以提升用戶體驗。
2. 對于CLS該如何進行優(yōu)化呢?我們首先來分析一下CLS偏移較大產(chǎn)生的原因,常見的原因主要有:無尺寸的頭像、廣告,動態(tài)注入的內(nèi)容,在更新 DOM 之前等待網(wǎng)絡響應的操作等。那么如何解決呢?我們可以給圖像設置長寬比,增加占位符,給動態(tài)注入的元素留出預留空間,有些情況下雖然不能完全避免位移偏差,但會盡可能減少CLS。
3. 對于TTI和TBT,影響這兩個指標的其實最多的就是長任務的執(zhí)行,長任務可以在瀏覽器performance面板查看(如下圖),我們點擊圖中紅色長任務可以看到這個任務中執(zhí)行的內(nèi)容,從而去分析做一些優(yōu)化,這里也提供幾點對與長任務優(yōu)化的建議:
- 頁面首屏的數(shù)據(jù)最好聚合到一個接口中,這樣既能保證在接口返回之后首屏可以直接渲染,還能避免多個接口請求引起的長任務。
- 減少頁面的重復渲染,如使用react hooks中的 useMemo、useCallback,加入鉤子依賴來避免組件的重復渲染。
- 降低不重要接口請求的優(yōu)先級,如埋點上報,可以利用requestIdleCallback api在瀏覽器空閑時再進行上報。
PLUS性能優(yōu)化實踐
我們在分析了如何對頁面進行性能優(yōu)化之后,來看一下PLUS團隊在性能優(yōu)化方面做的一些實踐。我們選用了PLUS流量比較大的兩個頁面作為實驗,由于這兩個頁面在線上已經(jīng)迭代了很多年并且頁面中之前老邏輯設計的比較復雜,所以導致這兩個頁面各項性能指標相對較低。未試用首頁分數(shù)、會員店分數(shù)都較低。我們在分析了相關的代碼之后,對各項指標做了專門的優(yōu)化。
拿未試用首頁來講:
1. 在FCP和LCP方面,添加了DNS預熱并且對骨架屏做了調整。
- 首先給頁面中的靜態(tài)資源、圖片地址設置了DNS預解析。通過 dns-prefetch 提前建立域名解析。當瀏覽網(wǎng)頁時,瀏覽器會在加載過程中對網(wǎng)頁中的域名進行解析緩存,這樣在點擊當前網(wǎng)頁連接時無需進行DNS解析,從而減少等待時間。
- 還對骨架屏也進行了調整,通過在HTML里設置骨架屏,在頁面數(shù)據(jù)尚未加載前,先給用戶展示出大致結構,直到請求返回后再補充需要顯示的數(shù)據(jù)內(nèi)容,既降低了用戶的焦灼情緒,又能使頁面加載過程變得自然通暢,不會造成長時間的白屏或閃爍,同時也提高了FCP的分數(shù)。
2. CLS方面,我們通過分析得知,影響CLS的主要原因是如果在APP內(nèi)使用沉浸式頭的話,由于沉浸式頭部是異步調用獲取的,就導致它會造成頭部高度偏移,頁面抖動。我們通過判斷不同機型給定不同的沉浸式初始高度,等待沉浸式異步返回后再重新設置,從而減少了頁面抖動及布局偏移。優(yōu)化前后CLS對比如下圖:
3. 在TTI和TBT方面,我們做了如下優(yōu)化:
- 首先對接口優(yōu)先級做了調整,針對業(yè)務本身,評估接口調用優(yōu)先級。優(yōu)先請求首屏核心接口,針對可能出現(xiàn)的造成網(wǎng)頁偏移的數(shù)據(jù)字段優(yōu)先處理,統(tǒng)一返回,從而減少位移。
- 在打包優(yōu)化上,通過webpack對業(yè)務包和公用包進行抽離,從而降低單個包體積過大的問題。
- 利用瀏覽器空閑時間進行埋點上報和低優(yōu)先級接口請求,在頁面中有許多的權益Icon圖標的埋點,我們把這些埋點上報的請求利用瀏覽器requestIdleCallback Api使請求在瀏覽器空閑時再進行上報,從而減少主線程的繁忙程度,更快的完成首屏渲染,以下是封裝的requestIdleCallback的部分代碼:
?
- 樓層懶渲染:樓層內(nèi)容不在首屏展示,因此初始化樓層填充 loading 進行占位,通過 observe 監(jiān)聽樓層出現(xiàn)在可視區(qū)再進行真實接口請求及渲染。這樣的話大大減少了首屏要請求的接口數(shù)量。
經(jīng)過以上的優(yōu)化,我們可以看一下未試用首頁的最終成果。從視頻的加載效果來看明顯是比之前好的。PLUS會員店優(yōu)化后性能也獲得了大幅度提升。
未試用首頁-優(yōu)化前后對比視頻:
結語
在前端的領域中,性能優(yōu)化是個永久的話題,性能優(yōu)化的經(jīng)驗既需要對技術極致的追求,也需要持續(xù)的積累沉淀。未來我們將做更深入的探索,如使用webWorker,Hybrid等技術持續(xù)優(yōu)化前端H5頁面,以給到用戶更好的體驗。