京東PLUS前端H5效能優化實踐

語言: CN / TW / HK

隨著移動網際網路的發展,使用者對產品的使用體驗要求越來越高。H5 作為業務的重要載體,應用非常廣泛,如何把控好 H5 的效能是一門重要的工作。因此 H5 頁面效能是一個非常核心的使用者體驗指標。

研究表明一個網頁載入的快慢會直接影響使用者在這個頁面逗留的時長,使用者在發現頁面有內容呈現的時候,如果在頁面進行了點選但是頁面沒有立即響應,使用者通常會認為頁面加載出現了延遲;如果頁面在載入過程中某些元素髮生了大幅度的偏移,這對於使用者來說體驗是相當糟糕的。

因此提高使用者體驗應從頁面的渲染速度、響應能力及頁面的抖動情況考慮,目前比較主流的衡量頁面指標的引數是谷歌 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(First Contentful Paint)是指頁面從開始載入到頁面內容的任何部分在螢幕上完成渲染的時間,也就是我們通常所說的首次內容繪製時間,它是測量感知載入速度的一個重要指標。那麼什麼“內容”才算FCP的呢,其中包含了文字、影象(包括背景影象)、<svg>元素或非白色的<canvas>元素。

LCP(Largest Contentful Paint)是根據頁面首次開始載入的時間點來報告可視區域內最大影象或文字塊完成渲染的相對時間。最大內容繪製表示著頁面的主要內容已經載入完成,它的檢測是通過Element Timming API來提供支援,使用的也是通過效能監聽事件的一個方式。那哪些元素在LCP的計算之內呢?除了常見的<img>元素,<video>元素、通過url元素載入的背景影象以及包含文字節點或其他行內級文字元素子元素的塊級元素都在計算範圍之內。需要注意的是LCP的計算是一個動態的過程。

CLS(Cumulative Layout Shift)是指整個頁面中所有意外佈局偏移中最大一連串的佈局偏移分數,即我們通常所說的累計佈局偏移,其中一連串的佈局偏移,也叫會話視窗,是指一個或多個連續發生的單次佈局偏移,每次偏移相隔的時間少於1秒,且整個視窗的最大持續時長為5秒。就像下圖中的灰色文字區域在頁面載入時如果發生了位移,那麼它的位移就會被計入CLS的分數。

TTI(Time To Interactive)是指頁面從開始載入到主要子資源完成渲染,並且能夠快速、可靠的響應使用者輸入的時間,通常我們也叫可互動時間。那麼TTI是如何計算的呢,如下圖首先沿時間軸正向搜尋時長至少為5秒的安靜視窗(安靜視窗是指沒有長任務且不超過兩個正在處理的網路get請求),然後沿時間軸反向搜尋安靜視窗之前的最後一個長任務,如果沒有找到長任務,則在FCP步驟停止執行,TTI就是安靜視窗之前最後一個長任務的結束時間,如果沒有找到長任務的話,則與FCP值相同。

TBT(Total Blocking Time)是測量FCP與TTI之間的總時間,這期間,主執行緒被阻塞的時間過長,無法作出輸入響應。我們通常把任務時間超過50毫秒的任務稱之為長任務,那麼一個頁面的總阻塞時間其實就是FCP和TTI之間發生每個長任務的阻塞時間總和。如下圖淡紅色區域的時間總和就是這個頁面的TBT分數。

如何檢測頁面效能?

在介紹了相關的指標之後,如何知道一個頁面效能的好壞呢?我們PLUS團隊聯合燭龍團隊在基於lighthouse的基礎上,結合我們京東實際的業務情況,共同開發了一套效能監控的SDK,在其中明確的提出了H5和PC兩套效能評分標準,並且給出了效能良好與較差的閾值。

如何做效能優化?

在深入瞭解各項指標之後,我們來看一下如何對頁面進行效能優化。一個頁面從輸入URL到頁面最後在瀏覽器的渲染流程大致如下:

URL解析 => DNS解析 => 建立TCP連線 => 客戶端傳送請求 => 伺服器處理和響應請求 => 瀏覽器解析並渲染響應內容 => 斷開TCP連結

可以看到其中從URL解析開始到伺服器響應請求的過程都是屬於網路層面,瀏覽器解析渲染相應內容屬於渲染層面,接下來我們就將從網路層面和渲染層面這兩個大方向來看一下如何去做效能優化。

我們都知道,在網路層面其實對於前端工程師來講可優化的點並不多,但是通過網路層面的優化,可以極大程度上降低TTFB的等待時間,從而提高FCP、LCP、TTI、TBT這些指標的分數,下面是一些常見優化方式:

  • 快取技術:我們可以合理的使用快取,如CDN快取、瀏覽器快取以及應用離線包,來減少資源的請求。除了這些,還可以在介面中設定Expires 和 Cache-Control 利用http快取來減少介面的請求時間。

  • 服務端還可以使用http2/http3、減少重定向等方式來保證我們的介面響應速度。

在瞭解了網路層面的一些優化方式之後,我們主要來看下在渲染層面如何進行優化,首先,我們來看一下瀏覽器的渲染機制,大概分為以下幾個步驟:

  • 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹。

  • 將DOM樹和CSSOM樹結合,生成渲染樹。

  • 計算圖層佈局。

  • 繪製圖層。

  • 合成圖層,顯示頁面。

在解析HTML的過程中會去請求頁面所需的js/css等靜態資源,對於資源請求這部分,我們可以進行一些特定的優化,讓我們的資源能夠更快速的進行載入,減少TTFB等待時間,從而提高FCP、LCP、TTI以及TBT的分數,比如:

  • 可以通過新增preconnect和dns-prefetch屬性預連結所需要的源,其中preconnect可以提前建立連線,但是如果在10秒內沒有使用連線,瀏覽器會關閉它,從而浪費一些早期的連線佔用CPU的時間,dns-prefetch會在請求資源時提前進行域名解析。

  • 如果我們想要提前載入資源,可以新增preload屬性來預先載入一些關鍵的資源,幫助頁面更快的進行渲染。

  • 還可以將不重要的script指令碼新增async和defer屬性,然後提取重要的css,多餘的css分割成不同的檔案,將不重要的檔案新增media屬性來消除阻塞渲染。

如果想更快的完成資源的請求,我們也可以對資源進行優化,使產出的檔案體積更小,檔案請求耗時更短,這也能使我們的FCP和LCP分數得以提高:

  • 移除未使用的css/js,如何能夠快速檢查出哪些程式碼我們有用到呢,可以開啟Chrome => 開發者工具 => shift + alt + p 搜尋Coverage Tab,點選重新整理可以看到資源的請求狀況以及可用程式碼覆蓋率,然後點選開啟每個資源,可以檢視具體內容有沒有被使用,其中綠色部分為關鍵程式碼,紅色部分為非關鍵程式碼,可以通過刪除紅色部分程式碼來減小靜態資源的體積。

  • 通過webpack/rollup/gulp等構建工具對程式碼進行打包壓縮

  • 對資源進行gzip壓縮

  • 將資源進行拆分(包括css、js),並按需引用,並且延遲載入優先順序較低的js

  • 對於圖片我們可以使用工具進行影象壓縮,也可以使用影象cdn,格式最好使用webp,將大大減少圖片的大小,對於大型的gif可以轉為視訊

在解析完html之後,瀏覽器會識別並載入所有的 CSS 樣式資訊與 DOM 樹合併,最終生成頁面渲染樹,我們在寫css的時候,需要注意以下幾點,這樣可以使我們的頁面能夠更快速的渲染出來。

  • 減少css選擇器的層級,避免多層巢狀

  • 避免使用較多的css表示式

  • 對於css動畫,儘量使用 transform(transform可開啟硬體加速)

  • 對於一些特定的動畫,可以選擇使用requestAnimationFrame 代替

當瀏覽器生成渲染樹以後,就會根據渲染樹來進行佈局,然後圖層會進行計算和繪製,最後合成圖層在頁面顯示出來。在這個渲染的過程中,我們可以進行以下的優化:

  • 避免DOM過大或者多層巢狀

  • 避免批量的對DOM進行修改

除了對網路和渲染兩個層面優化之外,我們也可以針對特定的指標進行優化。

1. 對於FCP和LCP,還可以在HTML中增加骨架屏,或者使用服務端渲染的方式進行優化,同時也可以提升使用者體驗。

2. 對於CLS該如何進行優化呢?我們首先來分析一下CLS偏移較大產生的原因,常見的原因主要有:無尺寸的頭像、廣告,動態注入的內容,在更新 DOM 之前等待網路響應的操作等。那麼如何解決呢?我們可以給影象設定長寬比,增加佔位符,給動態注入的元素留出預留空間,有些情況下雖然不能完全避免位移偏差,但會盡可能減少CLS。

3. 對於TTI和TBT,影響這兩個指標的其實最多的就是長任務的執行,長任務可以在瀏覽器performance面板檢視(如下圖),我們點選圖中紅色長任務可以看到這個任務中執行的內容,從而去分析做一些優化,這裡也提供幾點對與長任務優化的建議:

  • 頁面首屏的資料最好聚合到一個介面中,這樣既能保證在介面返回之後首屏可以直接渲染,還能避免多個介面請求引起的長任務。

  • 減少頁面的重複渲染,如使用react hooks中的 useMemo、useCallback,加入鉤子依賴來避免元件的重複渲染。

  • 降低不重要介面請求的優先順序,如埋點上報,可以利用requestIdleCallback api在瀏覽器空閒時再進行上報。

PLUS效能優化實踐

我們在分析瞭如何對頁面進行效能優化之後,來看一下PLUS團隊在效能優化方面做的一些實踐。我們選用了PLUS流量比較大的兩個頁面作為實驗,由於這兩個頁面在線上已經迭代了很多年並且頁面中之前老邏輯設計的比較複雜,所以導致這兩個頁面各項效能指標相對較低。未試用首頁分數、會員店分數都較低。我們在分析了相關的程式碼之後,對各項指標做了專門的優化。

拿未試用首頁來講:

1. 在FCP和LCP方面,添加了DNS預熱並且對骨架屏做了調整。

  • 首先給頁面中的靜態資源、圖片地址設定了DNS預解析。通過 dns-prefetch 提前建立域名解析。當瀏覽網頁時,瀏覽器會在載入過程中對網頁中的域名進行解析快取,這樣在點選當前網頁連線時無需進行DNS解析,從而減少等待時間。

  • 還對骨架屏也進行了調整,通過在HTML裡設定骨架屏,在頁面資料尚未載入前,先給使用者展示出大致結構,直到請求返回後再補充需要顯示的資料內容,既降低了使用者的焦灼情緒,又能使頁面載入過程變得自然通暢,不會造成長時間的白屏或閃爍,同時也提高了FCP的分數。

2. CLS方面,我們通過分析得知,影響CLS的主要原因是如果在APP內使用沉浸式頭的話,由於沉浸式頭部是非同步呼叫獲取的,就導致它會造成頭部高度偏移,頁面抖動。我們通過判斷不同機型給定不同的沉浸式初始高度,等待沉浸式非同步返回後再重新設定,從而減少了頁面抖動及佈局偏移。優化前後CLS對比如下圖:

3. 在TTI和TBT方面,我們做了如下優化:

  • 首先對介面優先順序做了調整,針對業務本身,評估介面呼叫優先順序。優先請求首屏核心介面,針對可能出現的造成網頁偏移的資料欄位優先處理,統一返回,從而減少位移。

  • 在打包優化上,通過webpack對業務包和公用包進行抽離,從而降低單個包體積過大的問題。

  • 利用瀏覽器空閒時間進行埋點上報和低優先順序介面請求,在頁面中有許多的權益Icon圖示的埋點,我們把這些埋點上報的請求利用瀏覽器requestIdleCallback Api使請求在瀏覽器空閒時再進行上報,從而減少主執行緒的繁忙程度,更快的完成首屏渲染,以下是封裝的requestIdleCallback的部分程式碼:

  • 樓層懶渲染:樓層內容不在首屏展示,因此初始化樓層填充 loading 進行佔位,通過 observe 監聽樓層出現在可視區再進行真實介面請求及渲染。這樣的話大大減少了首屏要請求的介面數量。

經過以上的優化,我們可以看一下未試用首頁的最終成果。從視訊的載入效果來看明顯是比之前好的。 PLUS會員店優化後效能也獲得了大幅度提升。

未試用首頁-優化前後對比視訊:

在前端的領域中,效能優化是個永久的話題,效能優化的經驗既需要對技術極致的追求,也需要持續的積累沉澱。未來我們將做更深入的探索,如使用webWorker,Hybrid等技術持續優化前端H5頁面,以給到使用者更好的體驗。