浏览器性能优化实战
作者:rosefang,騰訊 PCG 前端開發工程師
當我們在做性能優化的時候,我們究竟在優化什么?瀏覽器底層是一個什么架構?瀏覽器渲染的本質究竟是什么?哪些方面對用戶的體驗影響才是最大的?有沒有業內一些通用的標準或標桿參考?都 1202 年了,雅虎軍規還有沒有用?性能分析工具都有哪些?我們怎么進行打點分析才是合適的?
本文為你一一講解這些。了解了這些問題,可能你在做性能優化的時候才能更加得心應手。
1. 性能優化的本質
1.1 展示更快,響應更快
性能優化的目的,就是為了提供給用戶更好的體驗,這些體驗包含這幾個方面:展示更快、交互響應快、頁面無卡頓情況。
更詳細的說,就是指,在用戶輸入 url 到站點完整把整個頁面展示出來的過程中,通過各種優化策略和方法,讓頁面加載更快;在用戶使用過程中,讓用戶的操作響應更及時,有更好的用戶體驗。
對于前端工程師來說,要做好性能優化,需要理解瀏覽器加載和渲染的本質。理解了本質原理,才能更好的去做優化。所以我們先來看看瀏覽器架構是怎樣的。
1.2 理解瀏覽器多進程架構
從大的方面來說,瀏覽器是一個多進程架構。
它可以是一個進程包含多個線程,也可以是多個進程中,每個進程有多個線程,線程之間通過 IPC 通訊。每個瀏覽器有不同的實現細節,并沒有標準規定瀏覽器必須如何去實現。
這里我們只談論 chrome 架構。
下面這張圖是目前 chrome 的多進程架構圖。
圖片引自 Mariko Kosaka 的《Inside look at modern web browser》我們來看看這些進程分別對應瀏覽器窗口中的哪一部分:
圖片引自 Mariko Kosaka 的《Inside look at modern web browser》那么,怎么看瀏覽器對應啟動了什么進程呢?
chrome 中,我們可以通過更多->More Tools->Task Manager 看到啟動的進程。
從 chrome 官網和源碼,我們也可以得知,多進程架構中包含這些進程:
Browser 進程:打開瀏覽器后,始終只有一個。該進程有 UI 線程、Network 線程、Storage 線程等。用戶輸入 url 后,首先是 Browser 進程進行響應和請求服務器獲取數據。然后傳遞給 Renderer 進程。
Renderer 進程:每一個 tab 一個,負責 html、css、js 執行的整個過程。前端性能優化也與這個進程有關。
Plugin 進程:與瀏覽器插件相關,例如 flash 等。
GPU 進程:瀏覽器共用一個。主要負責把 Renderer 進程中繪制好的 tile 位圖作為紋理上傳到 GPU,并調用 GPU 相關方法把紋理 draw 到屏幕上。
這里的話只是簡單介紹一下瀏覽器的多進程架構,讓大家對瀏覽器整體架構有個初步認識,其實背后的細節還有很多,這里就不一一展開。有興趣可以細看這一系列文章和chrome 官網介紹。
1.3 理解頁面渲染相關進程
1.3.1 Renderer Process & GPU Process
從以上的多架構,我們了解到,與前端渲染、性能優化相關的,其實主要是 Renderer 進程和 GPU 進程。那么,它們又是什么架構呢?
來看一下這張我們再熟悉不過的圖。
圖片引自 Paul 的《The Anatomy of a Frame》Renderer 進程:包括 3 個線程。合成線程(Compositor Thread)、主線程(Main Thread)、Compositor Tile Worker。
GPU 進程:只有 GPU 線程,負責接收從 Renderer 進程中的 Compositor Thread 傳過來的紋理,顯示到屏幕上。
1.3.2 Renderer Process 詳解
Renderer 進程中 3 個線程的作用為:
Compositor Thread:首先接收 vsync 信號(vsync 信號是指操作系統指示瀏覽器去繪制新的幀),任何事件都會先到達 Compositor 線程。如果主線程沒有綁定事件,那么 Compositor 線程將避免進入主線程,并嘗試將輸入轉換為屏幕上的移動。它將更新的圖層位置信息作為幀通過 GPU 線程傳遞給 GPU 進行繪制。
當用戶在快速滑動過程中,如果主線程沒有綁定事件,Compositor 線程是可以快速響應并繪制的,這是瀏覽器做的一個優化。
Main Thread:主線程就是我們前端工程師熟知的線程,這里會執行解析 Html、樣式計算、布局、繪制、合成等動作。所以關于性能的問題,都發生在了這里。所以應該重點關注這里。
Compositor Tile Worker:由合成線程產生一個或多個 worker 來處理光柵化的工作。
Service Workers 和 Web Workers 可以暫時理解也在 Renderer 進程中,這里不展開討論。
1.3.2.1 Main Thread
main-thread主線程需要重點講下。因為這是我們的代碼真實存在的環境。
從上一小節 Render 進程和 GPU 進程的圖中,我們可以看到有個紅色的箭頭,從 Recal Styles 和 Layout 指向了 requestAnimationFrame,這意味著有 Forced Synchronous Layout (or Styles)(強制回流和重繪)發生,這一點在性能方面特別要注意。
在 Main Thread 中,有這幾個需要注意一下:
requestAnimationFrame:因為布局和樣式計算是在 rAF 之后,所以在 rAF 是進行元素變更的理想時機。如果在這里對一個元素變更 100 個類,不會進行 100 次計算,它們會分批以后處理。需要注意的是,不能在 rAF 中查詢任何計算樣式和布局的屬性(例如:el.style.backgroundImage 或 el.style.offsetWidth),因為這樣會導致重繪和回流。
Layout:布局的計算通常是針對整個文檔的,并且與 DOM 元素的大小成正比!(這點特別要注意,如果一個頁面 DOM 元素太多,也會導致性能問題)
主線程的順序始終都是:
Input?Event?Handler->requestAnimationFrame->ParseHtml->ReculateStyles->Layout-?>Update?Layer?Tree->Paint->Composite->commit->requestIdleCallback只能從前往后,例如,必須先是 ReculateStyles,然后 Layout、然后 Paint。但是,如果它只需要做最后一步 Paint,那么這就是它全部要做的事情,不會再發生前面的 ReculateStyles 和 Layout。
這里其實給了我們一個啟示:如果要讓 fps 保持 60,即每幀的 js 執行時間少于 16.66ms,那么讓這個主線程執行的過程盡可能地少,是我們的性能優化目標。
根據主線程的這些步驟,理想的情況下,我們只希望瀏覽器只發生最后一個步驟:Composite(合成)。
CSS 的屬性是我們需要關注一下的模塊。這里有描述了哪些CSS 屬性會引起重繪、回流和合成。例如,讓我們給一個元素進行移動位置時:transform和opacity可以直接觸發合成,但是left和top卻會觸發 Layout、Paint、Composite3 個動作。所以顯然用 transform 時更好的方案。
但這并不是說我們不應該用 left 和 top 這些可能引起重繪回流的屬性,而是應該關注每個屬性在瀏覽器性能中引起的效果。
2. 看看經典:雅虎軍規
多年前雅虎的 Nicolas C. Zakas 提出 7 個類別 35 條軍規,至今為止很多前端優化準則都是圍繞著這個展開。如果嚴格按照這些規則去做,其實我們有很多優化工作可以做,只要認真踐行,性能提升不是問題。
我們來看看它 7 個分類都是圍繞哪些方面展開:
Server:與頁面發起請求的相關;
Cookie:與頁面發起請求相關;
Mobile:與頁面請求相關;
Content:與頁面渲染相關;
Image:與頁面渲染相關;
CSS:與頁面渲染相關;
Javascript:與頁面渲染和交互相關。
從上面的描述可以看到,其實雅虎軍規,是圍繞頁面發起請求那一刻,到頁面渲染完成,頁面開始交互這幾個方面來展開,提出的一些原則。
很多原則大家也都耳熟能詳,就不全部展開了,有興趣的同學可以去查看原文。這里主要想提一些忽略但是又值得注意的點:
減少 DOM 節點數量
為什么要減少 DOM 節點的數量?
當遍歷查詢 500 和 5000 個 DOM 節點,進行事件綁定時,會有所差別。
當一個頁面 DOM 節點過多,應該考慮使用無限滾動方案來使視窗節點可控。可以看看google 提的方案。
減少 cookie 大小
cookie 傳輸會造成帶寬浪費,影響響應時間,可以這樣做:
消除不必要的 cookies;
靜態資源不需要 cookie,可以采用其他的域名,不會主動帶上 cookie。
避免圖片 src 為空
圖片 src 為空時,不同瀏覽器會有不同的副作用,會重新發起一起請求。
3. 性能指標
3.1 什么樣的性能指標才能真正代表用戶體驗?
要衡量性能,我們必須有一些客觀的、可衡量的指標來進行監控。但是客觀且定量可衡量的指標不一定能反映用戶的真實體驗。
以前,我們會用 load 事件的觸發來衡量一個頁面是否加載或顯示完成。但是設想會不會有這樣的情況:一個頁面的 load 事件已經被觸發,但是卻在 load 事件之后幾秒才開始加載內容和渲染頁面,所以這個時候,load 事件并不能真實反映用戶看到內容的時刻。
在過去幾年,google 團隊和W3C 性能工作組致力于提供標準的性能 API 來真正衡量用戶的體驗。主要是從這 4 個方面思考:
| Is it happening? | 導航是否成功,服務器是否響應了 |
| Is it useful? | 是否已經渲染了足夠的內容,讓用戶可以開始參與其中 |
| Is it usable? | 用戶是否可以與頁面交互,頁面是否處于繁忙狀態 |
| Is it delightful? | 交互是否流暢、自然、沒有滯后反映或卡頓 |
通常有 2 種途徑來衡量性能。
本地實驗衡量:本地模擬用戶的網絡、設備等情況進行測試。通常在開發新功能的時候,實驗測量是很重要的,因為我們不知道這個功能發布到線上會有什么性能問題,所以提前進行性能測試,可以進行預防。
線上衡量:實驗測量固然可以反映一些問題,但無法反映在用戶那里真實的情況。同樣的,在用戶那里,性能問題會和用戶的設備、網絡情況有關,而且還跟用戶如何與頁面進行交互有關。
有這幾個類型與用戶感知性能相關。
頁面加載時間:頁面以多快的速度加載和渲染元素到頁面上。
加載后響應時間:頁面加載和執行 js 代碼后多久能響應用戶交互。
運行時響應:頁面加載完成后,對用戶的交互響應時間。
視覺穩定性:頁面元素是否會以用戶不期望的方式移動,并干擾用戶的交互。
流暢度:過渡和動畫是否以一致的幀率渲染,并從一種狀態流暢地過渡到另一種狀態。
對應上面幾種分類,Google 和 W3C 性能工作組提供了對應這幾種性能指標:
First contentful paint (FCP): 測量頁面開始加載到某一塊內容顯示在頁面上的時間。
Largest contentful paint (LCP): 測量頁面開始加載到最大文本塊內容或圖片顯示在頁面中的時間。
First input delay (FID): 測量用戶首次與網站進行交互(例如點擊一個鏈接、按鈕、js 自定義控件)到瀏覽器真正進行響應的時間。
Time to Interactive (TTI): 測量從頁面加載到可視化呈現、頁面初始化腳本已經加載,并且可以可靠地快速響應用戶的時間。
Total blocking time (TBT): 測量從 FCP 到 TTI 之間的時間,這個時間內主線程被阻塞無法響應用戶輸入。
Cumulative layout shift (CLS): 測量從頁面開始加載到狀態變為隱藏過程中,發生不可預期的 layout shifts 的累積分數。
這些指標能從一定程度上衡量頁面性能,但不一定都是有效的。舉個例子。LCP 指標主要用戶衡量頁面的主要內容是否完成加載,但會有這樣的情況,最大的元素并不是主要內容,那么這個時候 LCP 指標并不是那么重要。
每個不同的站點有自己的特殊性,可以參考以上角度進行衡量,也需要因地制宜。
3.2 Core Web Vitals
在以上列出的指標中,Google 定義了 3 個最核心的指標,作為 Core Web Vitals。它們分別代表著:加載、交互、視覺穩定性。
image-20210426192204425Largest Contentful Paint (LCP): 測量加載性能。為了能提供較好的用戶體驗,LCP 指標建議頁面首次加載要在 2.5s 內完成。
First Input Delay (FID): 測量交互性能。為了提供較好用戶體驗,交互時間建議在 100ms 或以內。
Cumulative Layout Shift (CLS): 測量視覺穩定性。為了提供較好用戶體驗,頁面應該維持 CLS 在 0.1 或以內。
當頁面訪問量有 75%的數據達到了以上以上 Good 的標準,則認為性能是不錯的了。
Core Web Vitals 是作為核心性能指標,但是其他指標也同樣在重要,是做為核心指標的一個輔助。例如,TTFB 和 FCP 都可以用來衡量加載性能(服務器響應時間和渲染時間),它們作為 LCP 的一個問題手段輔助。同樣的,TBT 和 TTI 對于衡量交互性能也很重要,是 FID 的一個輔助,但是它們無法在線上進行測量,也無法反映以用戶為中心的結果。
Google 官方提供了一個web-vitals庫,線上或本地都可以測量上面提到的 3 個指標:
import?{getCLS,?getFID,?getLCP}?from?'web-vitals';function?sendToAnalytics(metric)?{const?body?=?JSON.stringify(metric);//?Use?`navigator.sendBeacon()`?if?available,?falling?back?to?`fetch()`.(navigator.sendBeacon?&&?navigator.sendBeacon('/analytics',?body))?||fetch('/analytics',?{body,?method:?'POST',?keepalive:?true}); }getCLS(sendToAnalytics); getFID(sendToAnalytics); getLCP(sendToAnalytics);下面,分別講講這 3 個指標定義的原因、如何測量、如何優化。
3.2.1 Largest Contentful Paint (LCP)
3.2.1.1 LCP 如何定義
圖片來自LCPLCP 是指頁面開始加載到最大文本塊內容或圖片顯示在頁面中的時間。那么哪些元素可以被定義為最大元素呢?
<img>標簽
<image> 在 svg 中的 image 標簽
<video> video 標簽
CSS background url()加載的圖片
包含內聯或文本的塊級元素
3.2.1.2 如何測量 LCP
線上測量工具
Chrome User Experience Report
PageSpeed Insights
Search Console (Core Web Vitals report)
web-vitals JavaScript library
實驗室工具
Chrome DevTools
Lighthouse
WebPageTest
原生的 JS API 測量
LCP 還可以用 JS API 進行測量,主要使用 PerformanceObserver 接口,目前除了 IE 不支持,其他瀏覽器基本都支持了。
new?PerformanceObserver((entryList)?=>?{for?(const?entry?of?entryList.getEntries())?{console.log('LCP?candidate:',?entry.startTime,?entry);} }).observe({type:?'largest-contentful-paint',?buffered:?true});我們看一下結果是怎樣的:
LCP-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細節問題。
3.2.1.3 如何優化 LCP
LCP 可能被這四個因素影響:
服務端響應時間
Javascript 和 CSS 引起的渲染卡頓
資源加載時間
客戶端渲染
更加詳細的優化建議就不展開了,可以參考這里。
3.2.2 First Input Delay (FID)
3.2.2.1 FID 如何定義
圖片來自FID我們都知道第一印象的重要性,比如初次遇到某人形成的印象,會在后續交往中起重要的影響。對于一個網站也是如此。
網站以多快的速度加載完成是其中一項指標,加載后以多快的速度對用戶進行響應也同樣重要。FID 就是指后者。
可以通過下面的圖來更詳細了解 FID 處于哪個位置:
圖片來自FID從上圖可以看出,當主線程處于繁忙的時候,FID 是指從瀏覽器接收到了用戶輸入,到瀏覽器對用戶的輸入進行響應的延遲時間。
通常,當我們在寫代碼的時候,會認為只要用戶輸入信息,我們的事件回調就會立刻響應,但實際上并不是這樣。這是主線程可能處于繁忙,瀏覽器正忙著解析和執行其他 js。如上圖所示的 FID 時間,主線程正在處理其他任務。
當 FID 的時間為 100ms 或以內,則為 Good。
上面的例子中,用戶剛好在主線程最繁忙的時刻進行了交互,但是如果用戶在主線程空閑的時候交互,那么瀏覽器可以立刻響應。所以 FID 的值需要重點查看它的分布情況。
FID 實際上測量的是輸入事件被感知到到主線程空閑的這段時間。這意味著即使沒有輸入事件被注冊,FID 也可以測量。因為用戶的輸入相應并不一定需要事件被執行,但一定需要主線程是空閑的。例如,下面這些 HTML 元素都需要在交互響應之前等待主線程上的正在執行的任務完成:
輸入框,例如<input>、<textarea>、<radio>、<checkbox>
下拉框,例如<select>
鏈接,例如<a>
為什么要考慮測量第一次的輸入延遲?有如下原因:
因為第一次輸入延遲是用戶對你的網站形成的第一個印象,網站是否有質量且可靠;
在今天,web 中最大的交互問題第一次加載之后;
對于網站應該如何解決較高的首次輸入延遲(例如代碼分割、減少 JavaScript 的預加載)的建議解決方案(TTI 是指衡量這一塊),不一定與在頁面加載后解決輸入延遲(FID 是指衡量這一塊)的解決方案相同。所以 FID 是在 TTI 的基礎上更精確的細分。
為什么 FID 只是包含從用戶輸入到主線程開始相應的時間?而沒有包含事件處理到瀏覽器繪制 UI 的時間?
盡管主線程處理和繪制的這段時間也很重要,但是如果 FID 把這段時間也包含進來,開發者可能會使用異步 API(例如setTimeout、requestAnimationFrame)來把這個 task 拆分到下一幀,以較少 FID 的時間,這樣不僅沒有提高用戶體驗,反而使用戶體驗降低。
3.2.2.2 如何測量 FID
FID 可以在實驗環境也可以在線上環境測量。
線上測量工具
Chrome User Experience Report
PageSpeed Insights
Search Console (Core Web Vitals report)
web-vitals JavaScript library
原生的 JS API 測量
new?PerformanceObserver((entryList)?=>?{for?(const?entry?of?entryList.getEntries())?{const?delay?=?entry.processingStart?-?entry.startTime;console.log('FID?candidate:',?delay,?entry);} }).observe({type:?'first-input',?buffered:?true});PerformanceObserver 目前除了在 IE 上沒有兼容,其他瀏覽器基本都兼容了。
我們看一下結果是怎樣的:
FID-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細節問題。
3.2.2.3 如何優化 FID
FID 可能被這四個因素影響:
減少第三方代碼的影響
減少 Javascript 的執行時間
最小化主線程工作
減小請求數量和請求文件大小
更加詳細的優化建議可以參考這里。
3.2.3 Cumulative Layout Shift (CLS)
3.2.3.1 CLS 如何定義
圖片來自CLSCLS 是一個非常重要的、以用戶為中心的測量指標。它能衡量頁面是否排版穩定。
頁面移動會經常發生在資源異步加載、或者 DOM 元素動態添加到已存在的頁面元素上面。這些元素有可能是圖片、視頻、第三方廣告或小圖標等。
但是我們開發過程中可能不會察覺到這些問題,因為調試過程中刷新頁面,圖片都已經緩存在本地。調試接口的時候我們使用的是 mock 或者在局域網,接口速度都很快,這些延遲都可能被我們忽略。
CLS 就是幫我們去發現這些真實發生在用戶端的問題的指標。
CLS 是測量頁面生命周期中,每個發生意外布局移動的分數。當一個可視元素在下一幀移動到另外一個位置,就是指布局移動。
CLS 的分數在 0.1 或以下,則為 Good。
那么意外布局移動的分數如何計算?
瀏覽器會監控兩楨之間發生移動的不穩定元素。布局移動分數由 2 個元素決定:impact fraction 和 distance fraction。
layout?shift?score?=?impact?fraction?*?distance?fraction可視區域內,在前一幀到下一幀之間所有不穩定的元素的并集,會影響當前幀的布局移動分數。
舉個例子,下面這張圖中,左邊是當前幀的一個元素,下一幀中,元素下移了可視區域內 25%的高度。紅色虛線框標出了兩楨中當前元素的并集,占適口的 75%,所以這個時候,impact faction 是 0.75。
另外一個影響布局移動分數的是 distance fraction,指這個元素相對視口移動的距離。不管是橫向還是豎向,取最大值。
下面例子中,豎向距離更大,該元素相對適口移動了 25%的距離,所以 distance fraction 是 0.25。所以布局移動分數是 0.75 * 0.25 = 0.1875.
impact-fraction-example但是要注意的是,并不是所有的布局移動都是不好的,很多 web 網站都會改變元素的開始位置。只有當布局移動是非用戶預期的,才是不好的。
換句話說,當用戶點擊了按鈕,布局進行了改動,這是 ok 的,CLS 的 JS API 中有一個字段hadRecentInput,用來標識 500ms 內是否有用戶數據,視情況而定,可以忽略這個計算。
3.2.3.2 如何測量 CLS
線上測量工具
Chrome User Experience Report
PageSpeed Insights
Search Console (Core Web Vitals report)
web-vitals JavaScript library
實驗室工具
Chrome DevTools
Lighthouse
WebPageTest
原生的 JS API 測量
let?cls?=?0;new?PerformanceObserver((entryList)?=>?{for?(const?entry?of?entryList.getEntries())?{if?(!entry.hadRecentInput)?{cls?+=?entry.value;console.log('Current?CLS?value:',?cls,?entry);}} }).observe({type:?'layout-shift',?buffered:?true});我們看一下結果是怎樣的:
CLS-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細節問題。
3.2.3.3 如何優化 CLS
我們可以根據這些原則來避免非預期布局移動:
圖片或視屏元素有大小屬性,或者給他們保留一個空間大小,設置 width、height,或者使用unsized-media feature policy。
不要在一個已存在的元素上面插入內容,除了相應用戶輸入。
使用 animation 或 transition 而不是直接觸發布局改變。
更詳細的內容可以看這里。
4. 性能工具:工欲善其事,必先利其器
Google 開發的所有工具都支持 Core Web Vitals 的測量。工具如下:
Lighthouse
PageSpeed Insights
Chrome DevTools
Search Console
web.dev's 提供的測量工具
Web Vitals 擴展
Chrome UX Report API
這些工具對 Core Web Vitals 的支持如下:
tools4.1 Lighthouse
打開 F12,就可以看到 Lighthouse,點擊 Generate Report,即可生成報告。當然也可以添加 chrome 插件使用。
lighthouseLighthhouse 是一個實驗室工具,本地模擬移動端和 PC 端對這幾個方面進行測試。同時 lighthouse 還會針對這幾個方面提出建議,在產品上線前值得一測。
lighthouse-funcLighthouse 還提供了Lighthouse CI,把 Lighthouse 集成到 CI 流水線中。舉個例子,每次在上線之前,跑 50 次流水線對 Lighthouse 的各項指標進行測試取平均值,一旦發現異常,立刻進行排查。把性能問題排查提前到發布之前。這塊后面會細講。
4.2 PageSpeed Insights
PageSpeed Insights(PSI)是一個可以分析線上和實驗室數據的工具。它是根據線上環境用戶真實的數據(在 Chrome UX 報告中)和 Lighthouse 結合出一份報告。和 Lighthouse 類似,它也會給出一些分析建議,可以知道頁面的 Core Web Vitals 是否達標。
PageSpeed-demoPageSpeed 只是提供對單個頁面的性能測試,而 Search Console 是正對整個網站的性能測試。
PageSpeed Insights 也提供了API供我們使用。同樣的,我們也可以把它集成到 CI 中。
4.3 CrUX
Chrome UX Report (CrUX)是指匯聚了成千上萬條用戶體驗數據的數據報告集,它是經過用戶同意才進行上報的,目前存儲在 Google BigQuery 上,可以使用賬號登陸進行查詢。它測量了所有的 Core Web Vitals 指標。
上面提到的 PageSpeed Insights 工具就是結合 CrUX 的數據進行分析給出的結論。
當然 CrUX 現在也提供了 API 共我們進行查詢,可以查詢的數據包括:
Largest Contentful Paint
Cumulative Layout Shift
First Input Delay
First Contentful Paint
原理如下:
CrUX通過 API 的查詢的數據每日都更新,并匯集了過去 28 天的數據。
具體的使用方式可以參考官方給出的demo。
4.4 Chrome DevTools Performance 面板
Performance 是我們最常用的本地性能分析工具。
devtools-panel這里像提幾點可以關注下的功能:Frame、Timings、Main、Layers、FPS。下面一一講解。
4.4.1 Frame
點擊 Frame 展開后,會看到有一個一個紅色或綠色小塊,這些代表著每幀的消耗時間。目前大多數設備的屏幕刷新率為 60 次/秒,瀏覽器渲染頁面的每一幀的速率如果與設備屏幕的刷新率保持一致,即 60fps 時,我們是不會感知到頁面卡的情況的。
我們把鼠標移上去看看:
frame-58這種是體驗順暢的情況。
再比如:
frame-32提示這一幀耗時了 30.9ms,當前是 32fps 并且是掉幀狀態。
4.4.2 Timings
這里可以看到幾個關鍵指標的時間點。
FP:First Paint;
FCP:First Contentful Paint;
LCP:Largest Contenful Paint;
DCL:DOMContentLoaded Event
L:OnLoad Event。
timings4.4.3 Main
Main 是 DevTools 中最常用也是最重要的功能。
main通過 record,我們可以查看頁面上所有操作在主線程中的執行過程。也就是我們常說的流程:
main-thread一旦有任何一個流程時間過長或頻繁發生,比如 Update Layer Tree 時間過長、頻繁出現 RecalcStyles、Layout(重繪回流),那么需要引起注意。后面會舉一個例子。
4.4.4 Layers
Layers 是瀏覽器在繪制過程中生成的一個層。因為瀏覽器底層渲染的本質是縱向分層、橫向分塊。這一塊的知識點是發生在 Renderer Process 進程中。后面會以一個例子展開講。
這里想提 Layers 的原因是,Layer 的渲染也會影響性能問題,而且有時候還不容易被發現!
Layers 面板一般不會默認展示出來,點擊更多->more tools->Layers 即可打開。
點擊 Layers 面板,點擊左邊下三角展開按鈕,可以看見頁面最終生成的合成層。右邊左上角可以選擇不同緯度進行查看。
layers-detail選中某個層,可以查看該層生成的原因。
layer-reasonChrome 的 Blink 內核給出了 54 種會生成合成層的原因:
constexpr?CompositingReasonStringMap?kCompositingReasonsStringMap[]?=?{{CompositingReason::k3DTransform,?"transform3D",?"Has?a?3d?transform"},{CompositingReason::kTrivial3DTransform,?"trivialTransform3D","Has?a?trivial?3d?transform"},{CompositingReason::kVideo,?"video",?"Is?an?accelerated?video"},{CompositingReason::kCanvas,?"canvas","Is?an?accelerated?canvas,?or?is?a?display?list?backed?canvas?that?was?""promoted?to?a?layer?based?on?a?performance?heuristic."},{CompositingReason::kPlugin,?"plugin",?"Is?an?accelerated?plugin"},{CompositingReason::kIFrame,?"iFrame",?"Is?an?accelerated?iFrame"},{CompositingReason::kSVGRoot,?"SVGRoot",?"Is?an?accelerated?SVG?root"},{CompositingReason::kBackfaceVisibilityHidden,?"backfaceVisibilityHidden","Has?backface-visibility:?hidden"},{CompositingReason::kActiveTransformAnimation,?"activeTransformAnimation","Has?an?active?accelerated?transform?animation?or?transition"},{CompositingReason::kActiveOpacityAnimation,?"activeOpacityAnimation","Has?an?active?accelerated?opacity?animation?or?transition"},{CompositingReason::kActiveFilterAnimation,?"activeFilterAnimation","Has?an?active?accelerated?filter?animation?or?transition"},{CompositingReason::kActiveBackdropFilterAnimation,"activeBackdropFilterAnimation","Has?an?active?accelerated?backdrop?filter?animation?or?transition"},{CompositingReason::kXrOverlay,?"xrOverlay","Is?DOM?overlay?for?WebXR?immersive-ar?mode"},{CompositingReason::kScrollDependentPosition,?"scrollDependentPosition","Is?fixed?or?sticky?position"},{CompositingReason::kOverflowScrolling,?"overflowScrolling","Is?a?scrollable?overflow?element"},{CompositingReason::kOverflowScrollingParent,?"overflowScrollingParent","Scroll?parent?is?not?an?ancestor"},{CompositingReason::kOutOfFlowClipping,?"outOfFlowClipping","Has?clipping?ancestor"},{CompositingReason::kVideoOverlay,?"videoOverlay","Is?overlay?controls?for?video"},{CompositingReason::kWillChangeTransform,?"willChangeTransform","Has?a?will-change:?transform?compositing?hint"},{CompositingReason::kWillChangeOpacity,?"willChangeOpacity","Has?a?will-change:?opacity?compositing?hint"},{CompositingReason::kWillChangeFilter,?"willChangeFilter","Has?a?will-change:?filter?compositing?hint"},{CompositingReason::kWillChangeBackdropFilter,?"willChangeBackdropFilter","Has?a?will-change:?backdrop-filter?compositing?hint"},{CompositingReason::kWillChangeOther,?"willChangeOther","Has?a?will-change?compositing?hint?other?than?transform?and?opacity"},{CompositingReason::kBackdropFilter,?"backdropFilter","Has?a?backdrop?filter"},{CompositingReason::kBackdropFilterMask,?"backdropFilterMask","Is?a?mask?for?backdrop?filter"},{CompositingReason::kRootScroller,?"rootScroller","Is?the?document.rootScroller"},{CompositingReason::kAssumedOverlap,?"assumedOverlap","Might?overlap?other?composited?content"},{CompositingReason::kOverlap,?"overlap","Overlaps?other?composited?content"},{CompositingReason::kNegativeZIndexChildren,?"negativeZIndexChildren","Parent?with?composited?negative?z-index?content"},{CompositingReason::kSquashingDisallowed,?"squashingDisallowed","Layer?was?separately?composited?because?it?could?not?be?squashed."},{CompositingReason::kOpacityWithCompositedDescendants,"opacityWithCompositedDescendants","Has?opacity?that?needs?to?be?applied?by?compositor?because?of?composited?""descendants"},{CompositingReason::kMaskWithCompositedDescendants,"maskWithCompositedDescendants","Has?a?mask?that?needs?to?be?known?by?compositor?because?of?composited?""descendants"},{CompositingReason::kReflectionWithCompositedDescendants,"reflectionWithCompositedDescendants","Has?a?reflection?that?needs?to?be?known?by?compositor?because?of?""composited?descendants"},{CompositingReason::kFilterWithCompositedDescendants,"filterWithCompositedDescendants","Has?a?filter?effect?that?needs?to?be?known?by?compositor?because?of?""composited?descendants"},{CompositingReason::kBlendingWithCompositedDescendants,"blendingWithCompositedDescendants","Has?a?blending?effect?that?needs?to?be?known?by?compositor?because?of?""composited?descendants"},{CompositingReason::kPerspectiveWith3DDescendants,"perspectiveWith3DDescendants","Has?a?perspective?transform?that?needs?to?be?known?by?compositor?because?""of?3d?descendants"},{CompositingReason::kPreserve3DWith3DDescendants,"preserve3DWith3DDescendants","Has?a?preserves-3d?property?that?needs?to?be?known?by?compositor?because?""of?3d?descendants"},{CompositingReason::kIsolateCompositedDescendants,"isolateCompositedDescendants","Should?isolate?descendants?to?apply?a?blend?effect"},{CompositingReason::kFullscreenVideoWithCompositedDescendants,"fullscreenVideoWithCompositedDescendants","Is?a?fullscreen?video?element?with?composited?descendants"},{CompositingReason::kRoot,?"root",?"Is?the?root?layer"},{CompositingReason::kLayerForHorizontalScrollbar,"layerForHorizontalScrollbar","Secondary?layer,?the?horizontal?scrollbar?layer"},{CompositingReason::kLayerForVerticalScrollbar,?"layerForVerticalScrollbar","Secondary?layer,?the?vertical?scrollbar?layer"},{CompositingReason::kLayerForScrollCorner,?"layerForScrollCorner","Secondary?layer,?the?scroll?corner?layer"},{CompositingReason::kLayerForScrollingContents,?"layerForScrollingContents","Secondary?layer,?to?house?contents?that?can?be?scrolled"},{CompositingReason::kLayerForSquashingContents,?"layerForSquashingContents","Secondary?layer,?home?for?a?group?of?squashable?content"},{CompositingReason::kLayerForForeground,?"layerForForeground","Secondary?layer,?to?contain?any?normal?flow?and?positive?z-index?""contents?on?top?of?a?negative?z-index?layer"},{CompositingReason::kLayerForMask,?"layerForMask","Secondary?layer,?to?contain?the?mask?contents"},{CompositingReason::kLayerForDecoration,?"layerForDecoration","Layer?painted?on?top?of?other?layers?as?decoration"},{CompositingReason::kLayerForOther,?"layerForOther","Layer?for?link?highlight,?frame?overlay,?etc."},{CompositingReason::kBackfaceInvisibility3DAncestor,"BackfaceInvisibility3DAncestor","Ancestor?in?same?3D?rendering?context?has?a?hidden?backface"}, };4.4.5 Rendering
Rendering 面板也隱藏了很多好用的功能。
4.4.5.1 Paint flashing
勾選了 Paint flashing 后,我們就會看到頁面上有哪些內容被重繪了:
paint-flashing4.4.5.2 Layout Shift6 Regions
勾選了 Layout Shift Regions 后,進行交互,就可以看到哪些元素進行了布局移動:
layout-shift-regions4.4.5.3 Frame Rendering Stats
這個工具有一個小插曲。
Frame Rendering Stats 的前身是 FPS meter,在 Google 版本85.0.4181.0改成了 Frame Rendering Stats,但是迫于用戶抱怨,在 90 版本的時候又改回來了。
Frame Rendering Stats 主要顯示不掉幀率。而 FPS 側重于顯示每秒的刷新率 fps。
Chrome 為什么要改成不掉幀率,是因為認為不掉幀率更能反映頁面的順暢度。而 FPS 顯示每一秒渲染的幀數雖然能一定程度反映頁面順暢度,但是在一些特殊情況例如沒有激活或空閑的頁面,fps 會比較低,這樣并不能反映真實情況。
frame-vs-fps(圖片引自blink dev 論壇討論)
4.4.6 Memory
在大型項目中,內存問題也是有發生。DevTools 也提供了內存分析工具供我們使用。
點擊 Memory 面板,點擊錄制按鈕。
memory點擊錄制后,會看到當前狀態下內存的占用情況,根據大小排序,我們可以定位到內存占用過多的地方。
memory-snapshot4.5 Search Console
Google Search Console 其實就是監控和維護網站在 Google 搜索結果中的展示情況以及排查問題的平臺。數據來源是 CrUX。
它會展示 3 個 Core Web Vitals metrics: LCP, FID, CLS。如果發現有問題,可以配合 PageSpeed 一起使用,分析問題。
search-console(圖片引自 vital-tools)
4.6 web.dev
web.dev/measure是 google 官方提供的測量性能工具,也會提供類似 PageSpeed Insight 的指標,還會提供一些具體代碼更改建議。
web-dev-1web-dev-24.7 Web Vitals extension
Google 也提供了擴展工具去測量 Core Web Vitals。可以從Store中進行安裝。
web-vitals-extension4.8 工具:思考與總結
當我們了解了這么多工具之后,琳瑯滿目,我們該如何選擇?如何使用好這些工具進行分析?
首先我們可以使用 Lighthouse,在本地進行測量,根據報告給出的一些建議進行優化;
發布之后,我們可以使用 PageSpeed Insights 去看下線上的性能情況;
接著,我們可以使用 Chrome User Experience Report API 去撈取線上過去 28 天的數據;
發現數據有異常,我們可以使用 DevTools 工具進行具體代碼定位分析;
使用 Search Console's Core Web Vitals report 查看網站功能整體情況;
使用 Web Vitals 擴展方便的看頁面核心指標情況;
5. 談談監控
最后一個章節想來談談監控。
我們在做性能優化的時候,常常會通過各種線上打點,來收集用戶數據,進行性能分析。沒錯,這是一種監控手段,更精確的說,這是一種"事后"監控手段。
"事后"監控固然重要,但我們也應該考慮"事前"監控,否則,每次發布一個需求后,去線上看數據。咦,發現數據下降了,然后我們去查代碼,去查數據,去查原因。這樣性能優化的同學永遠處于"追趕者"的角色,永遠跟在屁股后面查問題。
舉個例子,我們可以這樣去做"事前"監控。
建立流水線機制。流水線上如何做呢?
Lighthouse CI 或 PageSpeed Insights API:把 Lighthouse 或 PageSpeed Insights API 集成到 CI 流水線中,輸出報告分析。
Puppeteer 或 Playwright:使用 E2E 自動化測試工具集成到流水線模擬用戶操作,得到 Chrome Trace Files,也就是我們平常錄制 Performance 后,點擊左上角下載的文件。Puppeteer 和 Playwright 底層都是基于Chrome DevTools Protocol。
perf-downloadChrome Trace Files:根據規則分析 Trace 文件,可以得到每個函數執行的時間。如果函數執行時間超過了一個臨界值,可以拋出異常。如果一個函數每次的執行時間都超過了臨界值,那么就值得注意了。但是還有一點需要思考的是:函數執行的時間是否超過臨界值固然重要,但更重要的是這是不是用戶的輸入響應函數,與用戶體驗是否有關。
圖片來自Flo Sloot的Jank: You can measure what your users can feel.
輸出報告。定義異常臨界值。如果異常過多,考慮是否卡發布流程。
6. 總結
我們來回顧一下前面的內容:
第一部分,講了瀏覽器整體架構和渲染相關進程.為什么把這個章節也放到這篇性能優化的文章中?瀏覽器對于我們前端開發來說,是一個 sandbox 或者 darkbox。我們知道 js、html、css 結合起來就能實現我們的需求,但如果知道它是如何去渲染、執行、處理我們的代碼,不管是對做需求還是性能優化,都能更知其然和所以然。
第二部分,雅虎軍規是多年前提出的非常經典的優化建議。至今對于我們異常有很強的指導作用。你會發現它是從頁面加載、頁面渲染、到頁面交互全面的一個指導建議。與今天 Chrome 和 W3c 提出的 Web Vitals 思路依然類似。
第三部分,性能指標。參考標準與業內標桿的建議,能更好地指導我們進行優化。
第四部分,性能工具。工欲善其事,必先利其器。這個道理大家都懂,運用好工具,才能讓我們更加事半功倍。
第五部分,監控在性能優化中占很重要的部分,"事前"監控更重要,防患于未然。讓性能優化成為一個預防者而不是追趕者。
羅里吧嗦說了很多,當然還有很多性能優化的細節沒有講到,如果有錯誤的地方歡迎指正。或者有什么好方法好建議也強烈歡迎私聊交流一下。
沒有困難的工作參考文章:
https://web.dev/learn-web-vitals/
https://developers.google.com/web/updates/2018/09/inside-browser-part1
https://aerotwist.com/blog/the-anatomy-of-a-frame/
https://www.chromium.org/developers/how-tos/getting-around-the-chrome-source-code
https://medium.com/punching-performance/jank-you-can-measure-what-your-users-can-feel-e5713df2845f
視頻號最新視頻
總結
- 上一篇: 5月18发布会,这次TDSQL又有什么大
- 下一篇: 企业微信万亿级日志检索系统