浏览器性能优化实战
作者:rosefang,騰訊 PCG 前端開發(fā)工程師
當(dāng)我們在做性能優(yōu)化的時候,我們究竟在優(yōu)化什么?瀏覽器底層是一個什么架構(gòu)?瀏覽器渲染的本質(zhì)究竟是什么?哪些方面對用戶的體驗影響才是最大的?有沒有業(yè)內(nèi)一些通用的標(biāo)準(zhǔn)或標(biāo)桿參考?都 1202 年了,雅虎軍規(guī)還有沒有用?性能分析工具都有哪些?我們怎么進(jìn)行打點分析才是合適的?
本文為你一一講解這些。了解了這些問題,可能你在做性能優(yōu)化的時候才能更加得心應(yīng)手。
1. 性能優(yōu)化的本質(zhì)
1.1 展示更快,響應(yīng)更快
性能優(yōu)化的目的,就是為了提供給用戶更好的體驗,這些體驗包含這幾個方面:展示更快、交互響應(yīng)快、頁面無卡頓情況。
更詳細(xì)的說,就是指,在用戶輸入 url 到站點完整把整個頁面展示出來的過程中,通過各種優(yōu)化策略和方法,讓頁面加載更快;在用戶使用過程中,讓用戶的操作響應(yīng)更及時,有更好的用戶體驗。
對于前端工程師來說,要做好性能優(yōu)化,需要理解瀏覽器加載和渲染的本質(zhì)。理解了本質(zhì)原理,才能更好的去做優(yōu)化。所以我們先來看看瀏覽器架構(gòu)是怎樣的。
1.2 理解瀏覽器多進(jìn)程架構(gòu)
從大的方面來說,瀏覽器是一個多進(jìn)程架構(gòu)。
它可以是一個進(jìn)程包含多個線程,也可以是多個進(jìn)程中,每個進(jìn)程有多個線程,線程之間通過 IPC 通訊。每個瀏覽器有不同的實現(xiàn)細(xì)節(jié),并沒有標(biāo)準(zhǔn)規(guī)定瀏覽器必須如何去實現(xiàn)。
這里我們只談?wù)?chrome 架構(gòu)。
下面這張圖是目前 chrome 的多進(jìn)程架構(gòu)圖。
圖片引自 Mariko Kosaka 的《Inside look at modern web browser》我們來看看這些進(jìn)程分別對應(yīng)瀏覽器窗口中的哪一部分:
圖片引自 Mariko Kosaka 的《Inside look at modern web browser》那么,怎么看瀏覽器對應(yīng)啟動了什么進(jìn)程呢?
chrome 中,我們可以通過更多->More Tools->Task Manager 看到啟動的進(jìn)程。
從 chrome 官網(wǎng)和源碼,我們也可以得知,多進(jìn)程架構(gòu)中包含這些進(jìn)程:
Browser 進(jìn)程:打開瀏覽器后,始終只有一個。該進(jìn)程有 UI 線程、Network 線程、Storage 線程等。用戶輸入 url 后,首先是 Browser 進(jìn)程進(jìn)行響應(yīng)和請求服務(wù)器獲取數(shù)據(jù)。然后傳遞給 Renderer 進(jìn)程。
Renderer 進(jìn)程:每一個 tab 一個,負(fù)責(zé) html、css、js 執(zhí)行的整個過程。前端性能優(yōu)化也與這個進(jìn)程有關(guān)。
Plugin 進(jìn)程:與瀏覽器插件相關(guān),例如 flash 等。
GPU 進(jìn)程:瀏覽器共用一個。主要負(fù)責(zé)把 Renderer 進(jìn)程中繪制好的 tile 位圖作為紋理上傳到 GPU,并調(diào)用 GPU 相關(guān)方法把紋理 draw 到屏幕上。
這里的話只是簡單介紹一下瀏覽器的多進(jìn)程架構(gòu),讓大家對瀏覽器整體架構(gòu)有個初步認(rèn)識,其實背后的細(xì)節(jié)還有很多,這里就不一一展開。有興趣可以細(xì)看這一系列文章和chrome 官網(wǎng)介紹。
1.3 理解頁面渲染相關(guān)進(jìn)程
1.3.1 Renderer Process & GPU Process
從以上的多架構(gòu),我們了解到,與前端渲染、性能優(yōu)化相關(guān)的,其實主要是 Renderer 進(jìn)程和 GPU 進(jìn)程。那么,它們又是什么架構(gòu)呢?
來看一下這張我們再熟悉不過的圖。
圖片引自 Paul 的《The Anatomy of a Frame》Renderer 進(jìn)程:包括 3 個線程。合成線程(Compositor Thread)、主線程(Main Thread)、Compositor Tile Worker。
GPU 進(jìn)程:只有 GPU 線程,負(fù)責(zé)接收從 Renderer 進(jìn)程中的 Compositor Thread 傳過來的紋理,顯示到屏幕上。
1.3.2 Renderer Process 詳解
Renderer 進(jìn)程中 3 個線程的作用為:
Compositor Thread:首先接收 vsync 信號(vsync 信號是指操作系統(tǒng)指示瀏覽器去繪制新的幀),任何事件都會先到達(dá) Compositor 線程。如果主線程沒有綁定事件,那么 Compositor 線程將避免進(jìn)入主線程,并嘗試將輸入轉(zhuǎn)換為屏幕上的移動。它將更新的圖層位置信息作為幀通過 GPU 線程傳遞給 GPU 進(jìn)行繪制。
當(dāng)用戶在快速滑動過程中,如果主線程沒有綁定事件,Compositor 線程是可以快速響應(yīng)并繪制的,這是瀏覽器做的一個優(yōu)化。
Main Thread:主線程就是我們前端工程師熟知的線程,這里會執(zhí)行解析 Html、樣式計算、布局、繪制、合成等動作。所以關(guān)于性能的問題,都發(fā)生在了這里。所以應(yīng)該重點關(guān)注這里。
Compositor Tile Worker:由合成線程產(chǎn)生一個或多個 worker 來處理光柵化的工作。
Service Workers 和 Web Workers 可以暫時理解也在 Renderer 進(jìn)程中,這里不展開討論。
1.3.2.1 Main Thread
main-thread主線程需要重點講下。因為這是我們的代碼真實存在的環(huán)境。
從上一小節(jié) Render 進(jìn)程和 GPU 進(jìn)程的圖中,我們可以看到有個紅色的箭頭,從 Recal Styles 和 Layout 指向了 requestAnimationFrame,這意味著有 Forced Synchronous Layout (or Styles)(強(qiáng)制回流和重繪)發(fā)生,這一點在性能方面特別要注意。
在 Main Thread 中,有這幾個需要注意一下:
requestAnimationFrame:因為布局和樣式計算是在 rAF 之后,所以在 rAF 是進(jìn)行元素變更的理想時機(jī)。如果在這里對一個元素變更 100 個類,不會進(jìn)行 100 次計算,它們會分批以后處理。需要注意的是,不能在 rAF 中查詢?nèi)魏斡嬎銟邮胶筒季值膶傩?#xff08;例如:el.style.backgroundImage 或 el.style.offsetWidth),因為這樣會導(dǎo)致重繪和回流。
Layout:布局的計算通常是針對整個文檔的,并且與 DOM 元素的大小成正比!(這點特別要注意,如果一個頁面 DOM 元素太多,也會導(dǎo)致性能問題)
主線程的順序始終都是:
Input?Event?Handler->requestAnimationFrame->ParseHtml->ReculateStyles->Layout-?>Update?Layer?Tree->Paint->Composite->commit->requestIdleCallback只能從前往后,例如,必須先是 ReculateStyles,然后 Layout、然后 Paint。但是,如果它只需要做最后一步 Paint,那么這就是它全部要做的事情,不會再發(fā)生前面的 ReculateStyles 和 Layout。
這里其實給了我們一個啟示:如果要讓 fps 保持 60,即每幀的 js 執(zhí)行時間少于 16.66ms,那么讓這個主線程執(zhí)行的過程盡可能地少,是我們的性能優(yōu)化目標(biāo)。
根據(jù)主線程的這些步驟,理想的情況下,我們只希望瀏覽器只發(fā)生最后一個步驟:Composite(合成)。
CSS 的屬性是我們需要關(guān)注一下的模塊。這里有描述了哪些CSS 屬性會引起重繪、回流和合成。例如,讓我們給一個元素進(jìn)行移動位置時:transform和opacity可以直接觸發(fā)合成,但是left和top卻會觸發(fā) Layout、Paint、Composite3 個動作。所以顯然用 transform 時更好的方案。
但這并不是說我們不應(yīng)該用 left 和 top 這些可能引起重繪回流的屬性,而是應(yīng)該關(guān)注每個屬性在瀏覽器性能中引起的效果。
2. 看看經(jīng)典:雅虎軍規(guī)
多年前雅虎的 Nicolas C. Zakas 提出 7 個類別 35 條軍規(guī),至今為止很多前端優(yōu)化準(zhǔn)則都是圍繞著這個展開。如果嚴(yán)格按照這些規(guī)則去做,其實我們有很多優(yōu)化工作可以做,只要認(rèn)真踐行,性能提升不是問題。
我們來看看它 7 個分類都是圍繞哪些方面展開:
Server:與頁面發(fā)起請求的相關(guān);
Cookie:與頁面發(fā)起請求相關(guān);
Mobile:與頁面請求相關(guān);
Content:與頁面渲染相關(guān);
Image:與頁面渲染相關(guān);
CSS:與頁面渲染相關(guān);
Javascript:與頁面渲染和交互相關(guān)。
從上面的描述可以看到,其實雅虎軍規(guī),是圍繞頁面發(fā)起請求那一刻,到頁面渲染完成,頁面開始交互這幾個方面來展開,提出的一些原則。
很多原則大家也都耳熟能詳,就不全部展開了,有興趣的同學(xué)可以去查看原文。這里主要想提一些忽略但是又值得注意的點:
減少 DOM 節(jié)點數(shù)量
為什么要減少 DOM 節(jié)點的數(shù)量?
當(dāng)遍歷查詢 500 和 5000 個 DOM 節(jié)點,進(jìn)行事件綁定時,會有所差別。
當(dāng)一個頁面 DOM 節(jié)點過多,應(yīng)該考慮使用無限滾動方案來使視窗節(jié)點可控??梢钥纯磄oogle 提的方案。
減少 cookie 大小
cookie 傳輸會造成帶寬浪費,影響響應(yīng)時間,可以這樣做:
消除不必要的 cookies;
靜態(tài)資源不需要 cookie,可以采用其他的域名,不會主動帶上 cookie。
避免圖片 src 為空
圖片 src 為空時,不同瀏覽器會有不同的副作用,會重新發(fā)起一起請求。
3. 性能指標(biāo)
3.1 什么樣的性能指標(biāo)才能真正代表用戶體驗?
要衡量性能,我們必須有一些客觀的、可衡量的指標(biāo)來進(jìn)行監(jiān)控。但是客觀且定量可衡量的指標(biāo)不一定能反映用戶的真實體驗。
以前,我們會用 load 事件的觸發(fā)來衡量一個頁面是否加載或顯示完成。但是設(shè)想會不會有這樣的情況:一個頁面的 load 事件已經(jīng)被觸發(fā),但是卻在 load 事件之后幾秒才開始加載內(nèi)容和渲染頁面,所以這個時候,load 事件并不能真實反映用戶看到內(nèi)容的時刻。
在過去幾年,google 團(tuán)隊和W3C 性能工作組致力于提供標(biāo)準(zhǔn)的性能 API 來真正衡量用戶的體驗。主要是從這 4 個方面思考:
| Is it happening? | 導(dǎo)航是否成功,服務(wù)器是否響應(yīng)了 |
| Is it useful? | 是否已經(jīng)渲染了足夠的內(nèi)容,讓用戶可以開始參與其中 |
| Is it usable? | 用戶是否可以與頁面交互,頁面是否處于繁忙狀態(tài) |
| Is it delightful? | 交互是否流暢、自然、沒有滯后反映或卡頓 |
通常有 2 種途徑來衡量性能。
本地實驗衡量:本地模擬用戶的網(wǎng)絡(luò)、設(shè)備等情況進(jìn)行測試。通常在開發(fā)新功能的時候,實驗測量是很重要的,因為我們不知道這個功能發(fā)布到線上會有什么性能問題,所以提前進(jìn)行性能測試,可以進(jìn)行預(yù)防。
線上衡量:實驗測量固然可以反映一些問題,但無法反映在用戶那里真實的情況。同樣的,在用戶那里,性能問題會和用戶的設(shè)備、網(wǎng)絡(luò)情況有關(guān),而且還跟用戶如何與頁面進(jìn)行交互有關(guān)。
有這幾個類型與用戶感知性能相關(guān)。
頁面加載時間:頁面以多快的速度加載和渲染元素到頁面上。
加載后響應(yīng)時間:頁面加載和執(zhí)行 js 代碼后多久能響應(yīng)用戶交互。
運行時響應(yīng):頁面加載完成后,對用戶的交互響應(yīng)時間。
視覺穩(wěn)定性:頁面元素是否會以用戶不期望的方式移動,并干擾用戶的交互。
流暢度:過渡和動畫是否以一致的幀率渲染,并從一種狀態(tài)流暢地過渡到另一種狀態(tài)。
對應(yīng)上面幾種分類,Google 和 W3C 性能工作組提供了對應(yīng)這幾種性能指標(biāo):
First contentful paint (FCP): 測量頁面開始加載到某一塊內(nèi)容顯示在頁面上的時間。
Largest contentful paint (LCP): 測量頁面開始加載到最大文本塊內(nèi)容或圖片顯示在頁面中的時間。
First input delay (FID): 測量用戶首次與網(wǎng)站進(jìn)行交互(例如點擊一個鏈接、按鈕、js 自定義控件)到瀏覽器真正進(jìn)行響應(yīng)的時間。
Time to Interactive (TTI): 測量從頁面加載到可視化呈現(xiàn)、頁面初始化腳本已經(jīng)加載,并且可以可靠地快速響應(yīng)用戶的時間。
Total blocking time (TBT): 測量從 FCP 到 TTI 之間的時間,這個時間內(nèi)主線程被阻塞無法響應(yīng)用戶輸入。
Cumulative layout shift (CLS): 測量從頁面開始加載到狀態(tài)變?yōu)殡[藏過程中,發(fā)生不可預(yù)期的 layout shifts 的累積分?jǐn)?shù)。
這些指標(biāo)能從一定程度上衡量頁面性能,但不一定都是有效的。舉個例子。LCP 指標(biāo)主要用戶衡量頁面的主要內(nèi)容是否完成加載,但會有這樣的情況,最大的元素并不是主要內(nèi)容,那么這個時候 LCP 指標(biāo)并不是那么重要。
每個不同的站點有自己的特殊性,可以參考以上角度進(jìn)行衡量,也需要因地制宜。
3.2 Core Web Vitals
在以上列出的指標(biāo)中,Google 定義了 3 個最核心的指標(biāo),作為 Core Web Vitals。它們分別代表著:加載、交互、視覺穩(wěn)定性。
image-20210426192204425Largest Contentful Paint (LCP): 測量加載性能。為了能提供較好的用戶體驗,LCP 指標(biāo)建議頁面首次加載要在 2.5s 內(nèi)完成。
First Input Delay (FID): 測量交互性能。為了提供較好用戶體驗,交互時間建議在 100ms 或以內(nèi)。
Cumulative Layout Shift (CLS): 測量視覺穩(wěn)定性。為了提供較好用戶體驗,頁面應(yīng)該維持 CLS 在 0.1 或以內(nèi)。
當(dāng)頁面訪問量有 75%的數(shù)據(jù)達(dá)到了以上以上 Good 的標(biāo)準(zhǔn),則認(rèn)為性能是不錯的了。
Core Web Vitals 是作為核心性能指標(biāo),但是其他指標(biāo)也同樣在重要,是做為核心指標(biāo)的一個輔助。例如,TTFB 和 FCP 都可以用來衡量加載性能(服務(wù)器響應(yīng)時間和渲染時間),它們作為 LCP 的一個問題手段輔助。同樣的,TBT 和 TTI 對于衡量交互性能也很重要,是 FID 的一個輔助,但是它們無法在線上進(jìn)行測量,也無法反映以用戶為中心的結(jié)果。
Google 官方提供了一個web-vitals庫,線上或本地都可以測量上面提到的 3 個指標(biāo):
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 個指標(biāo)定義的原因、如何測量、如何優(yōu)化。
3.2.1 Largest Contentful Paint (LCP)
3.2.1.1 LCP 如何定義
圖片來自LCPLCP 是指頁面開始加載到最大文本塊內(nèi)容或圖片顯示在頁面中的時間。那么哪些元素可以被定義為最大元素呢?
<img>標(biāo)簽
<image> 在 svg 中的 image 標(biāo)簽
<video> video 標(biāo)簽
CSS background url()加載的圖片
包含內(nèi)聯(lián)或文本的塊級元素
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 進(jìn)行測量,主要使用 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});我們看一下結(jié)果是怎樣的:
LCP-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細(xì)節(jié)問題。
3.2.1.3 如何優(yōu)化 LCP
LCP 可能被這四個因素影響:
服務(wù)端響應(yīng)時間
Javascript 和 CSS 引起的渲染卡頓
資源加載時間
客戶端渲染
更加詳細(xì)的優(yōu)化建議就不展開了,可以參考這里。
3.2.2 First Input Delay (FID)
3.2.2.1 FID 如何定義
圖片來自FID我們都知道第一印象的重要性,比如初次遇到某人形成的印象,會在后續(xù)交往中起重要的影響。對于一個網(wǎng)站也是如此。
網(wǎng)站以多快的速度加載完成是其中一項指標(biāo),加載后以多快的速度對用戶進(jìn)行響應(yīng)也同樣重要。FID 就是指后者。
可以通過下面的圖來更詳細(xì)了解 FID 處于哪個位置:
圖片來自FID從上圖可以看出,當(dāng)主線程處于繁忙的時候,FID 是指從瀏覽器接收到了用戶輸入,到瀏覽器對用戶的輸入進(jìn)行響應(yīng)的延遲時間。
通常,當(dāng)我們在寫代碼的時候,會認(rèn)為只要用戶輸入信息,我們的事件回調(diào)就會立刻響應(yīng),但實際上并不是這樣。這是主線程可能處于繁忙,瀏覽器正忙著解析和執(zhí)行其他 js。如上圖所示的 FID 時間,主線程正在處理其他任務(wù)。
當(dāng) FID 的時間為 100ms 或以內(nèi),則為 Good。
上面的例子中,用戶剛好在主線程最繁忙的時刻進(jìn)行了交互,但是如果用戶在主線程空閑的時候交互,那么瀏覽器可以立刻響應(yīng)。所以 FID 的值需要重點查看它的分布情況。
FID 實際上測量的是輸入事件被感知到到主線程空閑的這段時間。這意味著即使沒有輸入事件被注冊,FID 也可以測量。因為用戶的輸入相應(yīng)并不一定需要事件被執(zhí)行,但一定需要主線程是空閑的。例如,下面這些 HTML 元素都需要在交互響應(yīng)之前等待主線程上的正在執(zhí)行的任務(wù)完成:
輸入框,例如<input>、<textarea>、<radio>、<checkbox>
下拉框,例如<select>
鏈接,例如<a>
為什么要考慮測量第一次的輸入延遲?有如下原因:
因為第一次輸入延遲是用戶對你的網(wǎng)站形成的第一個印象,網(wǎng)站是否有質(zhì)量且可靠;
在今天,web 中最大的交互問題第一次加載之后;
對于網(wǎng)站應(yīng)該如何解決較高的首次輸入延遲(例如代碼分割、減少 JavaScript 的預(yù)加載)的建議解決方案(TTI 是指衡量這一塊),不一定與在頁面加載后解決輸入延遲(FID 是指衡量這一塊)的解決方案相同。所以 FID 是在 TTI 的基礎(chǔ)上更精確的細(xì)分。
為什么 FID 只是包含從用戶輸入到主線程開始相應(yīng)的時間?而沒有包含事件處理到瀏覽器繪制 UI 的時間?
盡管主線程處理和繪制的這段時間也很重要,但是如果 FID 把這段時間也包含進(jìn)來,開發(fā)者可能會使用異步 API(例如setTimeout、requestAnimationFrame)來把這個 task 拆分到下一幀,以較少 FID 的時間,這樣不僅沒有提高用戶體驗,反而使用戶體驗降低。
3.2.2.2 如何測量 FID
FID 可以在實驗環(huán)境也可以在線上環(huán)境測量。
線上測量工具
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 上沒有兼容,其他瀏覽器基本都兼容了。
我們看一下結(jié)果是怎樣的:
FID-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細(xì)節(jié)問題。
3.2.2.3 如何優(yōu)化 FID
FID 可能被這四個因素影響:
減少第三方代碼的影響
減少 Javascript 的執(zhí)行時間
最小化主線程工作
減小請求數(shù)量和請求文件大小
更加詳細(xì)的優(yōu)化建議可以參考這里。
3.2.3 Cumulative Layout Shift (CLS)
3.2.3.1 CLS 如何定義
圖片來自CLSCLS 是一個非常重要的、以用戶為中心的測量指標(biāo)。它能衡量頁面是否排版穩(wěn)定。
頁面移動會經(jīng)常發(fā)生在資源異步加載、或者 DOM 元素動態(tài)添加到已存在的頁面元素上面。這些元素有可能是圖片、視頻、第三方廣告或小圖標(biāo)等。
但是我們開發(fā)過程中可能不會察覺到這些問題,因為調(diào)試過程中刷新頁面,圖片都已經(jīng)緩存在本地。調(diào)試接口的時候我們使用的是 mock 或者在局域網(wǎng),接口速度都很快,這些延遲都可能被我們忽略。
CLS 就是幫我們?nèi)グl(fā)現(xiàn)這些真實發(fā)生在用戶端的問題的指標(biāo)。
CLS 是測量頁面生命周期中,每個發(fā)生意外布局移動的分?jǐn)?shù)。當(dāng)一個可視元素在下一幀移動到另外一個位置,就是指布局移動。
CLS 的分?jǐn)?shù)在 0.1 或以下,則為 Good。
那么意外布局移動的分?jǐn)?shù)如何計算?
瀏覽器會監(jiān)控兩楨之間發(fā)生移動的不穩(wěn)定元素。布局移動分?jǐn)?shù)由 2 個元素決定:impact fraction 和 distance fraction。
layout?shift?score?=?impact?fraction?*?distance?fraction可視區(qū)域內(nèi),在前一幀到下一幀之間所有不穩(wěn)定的元素的并集,會影響當(dāng)前幀的布局移動分?jǐn)?shù)。
舉個例子,下面這張圖中,左邊是當(dāng)前幀的一個元素,下一幀中,元素下移了可視區(qū)域內(nèi) 25%的高度。紅色虛線框標(biāo)出了兩楨中當(dāng)前元素的并集,占適口的 75%,所以這個時候,impact faction 是 0.75。
另外一個影響布局移動分?jǐn)?shù)的是 distance fraction,指這個元素相對視口移動的距離。不管是橫向還是豎向,取最大值。
下面例子中,豎向距離更大,該元素相對適口移動了 25%的距離,所以 distance fraction 是 0.25。所以布局移動分?jǐn)?shù)是 0.75 * 0.25 = 0.1875.
impact-fraction-example但是要注意的是,并不是所有的布局移動都是不好的,很多 web 網(wǎng)站都會改變元素的開始位置。只有當(dāng)布局移動是非用戶預(yù)期的,才是不好的。
換句話說,當(dāng)用戶點擊了按鈕,布局進(jìn)行了改動,這是 ok 的,CLS 的 JS API 中有一個字段hadRecentInput,用來標(biāo)識 500ms 內(nèi)是否有用戶數(shù)據(jù),視情況而定,可以忽略這個計算。
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});我們看一下結(jié)果是怎樣的:
CLS-exampleGoogle 官方 web-vitals 庫
Google 官方也提供了一個web-vitals庫,底層還是使用這個 API,只是幫我們處理了一些需要測量和不需測量的場景、以及一些細(xì)節(jié)問題。
3.2.3.3 如何優(yōu)化 CLS
我們可以根據(jù)這些原則來避免非預(yù)期布局移動:
圖片或視屏元素有大小屬性,或者給他們保留一個空間大小,設(shè)置 width、height,或者使用unsized-media feature policy。
不要在一個已存在的元素上面插入內(nèi)容,除了相應(yīng)用戶輸入。
使用 animation 或 transition 而不是直接觸發(fā)布局改變。
更詳細(xì)的內(nèi)容可以看這里。
4. 性能工具:工欲善其事,必先利其器
Google 開發(fā)的所有工具都支持 Core Web Vitals 的測量。工具如下:
Lighthouse
PageSpeed Insights
Chrome DevTools
Search Console
web.dev's 提供的測量工具
Web Vitals 擴(kuò)展
Chrome UX Report API
這些工具對 Core Web Vitals 的支持如下:
tools4.1 Lighthouse
打開 F12,就可以看到 Lighthouse,點擊 Generate Report,即可生成報告。當(dāng)然也可以添加 chrome 插件使用。
lighthouseLighthhouse 是一個實驗室工具,本地模擬移動端和 PC 端對這幾個方面進(jìn)行測試。同時 lighthouse 還會針對這幾個方面提出建議,在產(chǎn)品上線前值得一測。
lighthouse-funcLighthouse 還提供了Lighthouse CI,把 Lighthouse 集成到 CI 流水線中。舉個例子,每次在上線之前,跑 50 次流水線對 Lighthouse 的各項指標(biāo)進(jìn)行測試取平均值,一旦發(fā)現(xiàn)異常,立刻進(jìn)行排查。把性能問題排查提前到發(fā)布之前。這塊后面會細(xì)講。
4.2 PageSpeed Insights
PageSpeed Insights(PSI)是一個可以分析線上和實驗室數(shù)據(jù)的工具。它是根據(jù)線上環(huán)境用戶真實的數(shù)據(jù)(在 Chrome UX 報告中)和 Lighthouse 結(jié)合出一份報告。和 Lighthouse 類似,它也會給出一些分析建議,可以知道頁面的 Core Web Vitals 是否達(dá)標(biāo)。
PageSpeed-demoPageSpeed 只是提供對單個頁面的性能測試,而 Search Console 是正對整個網(wǎng)站的性能測試。
PageSpeed Insights 也提供了API供我們使用。同樣的,我們也可以把它集成到 CI 中。
4.3 CrUX
Chrome UX Report (CrUX)是指匯聚了成千上萬條用戶體驗數(shù)據(jù)的數(shù)據(jù)報告集,它是經(jīng)過用戶同意才進(jìn)行上報的,目前存儲在 Google BigQuery 上,可以使用賬號登陸進(jìn)行查詢。它測量了所有的 Core Web Vitals 指標(biāo)。
上面提到的 PageSpeed Insights 工具就是結(jié)合 CrUX 的數(shù)據(jù)進(jìn)行分析給出的結(jié)論。
當(dāng)然 CrUX 現(xiàn)在也提供了 API 共我們進(jìn)行查詢,可以查詢的數(shù)據(jù)包括:
Largest Contentful Paint
Cumulative Layout Shift
First Input Delay
First Contentful Paint
原理如下:
CrUX通過 API 的查詢的數(shù)據(jù)每日都更新,并匯集了過去 28 天的數(shù)據(jù)。
具體的使用方式可以參考官方給出的demo。
4.4 Chrome DevTools Performance 面板
Performance 是我們最常用的本地性能分析工具。
devtools-panel這里像提幾點可以關(guān)注下的功能:Frame、Timings、Main、Layers、FPS。下面一一講解。
4.4.1 Frame
點擊 Frame 展開后,會看到有一個一個紅色或綠色小塊,這些代表著每幀的消耗時間。目前大多數(shù)設(shè)備的屏幕刷新率為 60 次/秒,瀏覽器渲染頁面的每一幀的速率如果與設(shè)備屏幕的刷新率保持一致,即 60fps 時,我們是不會感知到頁面卡的情況的。
我們把鼠標(biāo)移上去看看:
frame-58這種是體驗順暢的情況。
再比如:
frame-32提示這一幀耗時了 30.9ms,當(dāng)前是 32fps 并且是掉幀狀態(tài)。
4.4.2 Timings
這里可以看到幾個關(guān)鍵指標(biāo)的時間點。
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,我們可以查看頁面上所有操作在主線程中的執(zhí)行過程。也就是我們常說的流程:
main-thread一旦有任何一個流程時間過長或頻繁發(fā)生,比如 Update Layer Tree 時間過長、頻繁出現(xiàn) RecalcStyles、Layout(重繪回流),那么需要引起注意。后面會舉一個例子。
4.4.4 Layers
Layers 是瀏覽器在繪制過程中生成的一個層。因為瀏覽器底層渲染的本質(zhì)是縱向分層、橫向分塊。這一塊的知識點是發(fā)生在 Renderer Process 進(jìn)程中。后面會以一個例子展開講。
這里想提 Layers 的原因是,Layer 的渲染也會影響性能問題,而且有時候還不容易被發(fā)現(xiàn)!
Layers 面板一般不會默認(rèn)展示出來,點擊更多->more tools->Layers 即可打開。
點擊 Layers 面板,點擊左邊下三角展開按鈕,可以看見頁面最終生成的合成層。右邊左上角可以選擇不同緯度進(jìn)行查看。
layers-detail選中某個層,可以查看該層生成的原因。
layer-reasonChrome 的 Blink 內(nèi)核給出了 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 后,我們就會看到頁面上有哪些內(nèi)容被重繪了:
paint-flashing4.4.5.2 Layout Shift6 Regions
勾選了 Layout Shift Regions 后,進(jìn)行交互,就可以看到哪些元素進(jìn)行了布局移動:
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 側(cè)重于顯示每秒的刷新率 fps。
Chrome 為什么要改成不掉幀率,是因為認(rèn)為不掉幀率更能反映頁面的順暢度。而 FPS 顯示每一秒渲染的幀數(shù)雖然能一定程度反映頁面順暢度,但是在一些特殊情況例如沒有激活或空閑的頁面,fps 會比較低,這樣并不能反映真實情況。
frame-vs-fps(圖片引自blink dev 論壇討論)
4.4.6 Memory
在大型項目中,內(nèi)存問題也是有發(fā)生。DevTools 也提供了內(nèi)存分析工具供我們使用。
點擊 Memory 面板,點擊錄制按鈕。
memory點擊錄制后,會看到當(dāng)前狀態(tài)下內(nèi)存的占用情況,根據(jù)大小排序,我們可以定位到內(nèi)存占用過多的地方。
memory-snapshot4.5 Search Console
Google Search Console 其實就是監(jiān)控和維護(hù)網(wǎng)站在 Google 搜索結(jié)果中的展示情況以及排查問題的平臺。數(shù)據(jù)來源是 CrUX。
它會展示 3 個 Core Web Vitals metrics: LCP, FID, CLS。如果發(fā)現(xiàn)有問題,可以配合 PageSpeed 一起使用,分析問題。
search-console(圖片引自 vital-tools)
4.6 web.dev
web.dev/measure是 google 官方提供的測量性能工具,也會提供類似 PageSpeed Insight 的指標(biāo),還會提供一些具體代碼更改建議。
web-dev-1web-dev-24.7 Web Vitals extension
Google 也提供了擴(kuò)展工具去測量 Core Web Vitals。可以從Store中進(jìn)行安裝。
web-vitals-extension4.8 工具:思考與總結(jié)
當(dāng)我們了解了這么多工具之后,琳瑯滿目,我們該如何選擇?如何使用好這些工具進(jìn)行分析?
首先我們可以使用 Lighthouse,在本地進(jìn)行測量,根據(jù)報告給出的一些建議進(jìn)行優(yōu)化;
發(fā)布之后,我們可以使用 PageSpeed Insights 去看下線上的性能情況;
接著,我們可以使用 Chrome User Experience Report API 去撈取線上過去 28 天的數(shù)據(jù);
發(fā)現(xiàn)數(shù)據(jù)有異常,我們可以使用 DevTools 工具進(jìn)行具體代碼定位分析;
使用 Search Console's Core Web Vitals report 查看網(wǎng)站功能整體情況;
使用 Web Vitals 擴(kuò)展方便的看頁面核心指標(biāo)情況;
5. 談?wù)劚O(jiān)控
最后一個章節(jié)想來談?wù)劚O(jiān)控。
我們在做性能優(yōu)化的時候,常常會通過各種線上打點,來收集用戶數(shù)據(jù),進(jìn)行性能分析。沒錯,這是一種監(jiān)控手段,更精確的說,這是一種"事后"監(jiān)控手段。
"事后"監(jiān)控固然重要,但我們也應(yīng)該考慮"事前"監(jiān)控,否則,每次發(fā)布一個需求后,去線上看數(shù)據(jù)。咦,發(fā)現(xiàn)數(shù)據(jù)下降了,然后我們?nèi)ゲ榇a,去查數(shù)據(jù),去查原因。這樣性能優(yōu)化的同學(xué)永遠(yuǎn)處于"追趕者"的角色,永遠(yuǎn)跟在屁股后面查問題。
舉個例子,我們可以這樣去做"事前"監(jiān)控。
建立流水線機(jī)制。流水線上如何做呢?
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:根據(jù)規(guī)則分析 Trace 文件,可以得到每個函數(shù)執(zhí)行的時間。如果函數(shù)執(zhí)行時間超過了一個臨界值,可以拋出異常。如果一個函數(shù)每次的執(zhí)行時間都超過了臨界值,那么就值得注意了。但是還有一點需要思考的是:函數(shù)執(zhí)行的時間是否超過臨界值固然重要,但更重要的是這是不是用戶的輸入響應(yīng)函數(shù),與用戶體驗是否有關(guān)。
圖片來自Flo Sloot的Jank: You can measure what your users can feel.
輸出報告。定義異常臨界值。如果異常過多,考慮是否卡發(fā)布流程。
6. 總結(jié)
我們來回顧一下前面的內(nèi)容:
第一部分,講了瀏覽器整體架構(gòu)和渲染相關(guān)進(jìn)程.為什么把這個章節(jié)也放到這篇性能優(yōu)化的文章中?瀏覽器對于我們前端開發(fā)來說,是一個 sandbox 或者 darkbox。我們知道 js、html、css 結(jié)合起來就能實現(xiàn)我們的需求,但如果知道它是如何去渲染、執(zhí)行、處理我們的代碼,不管是對做需求還是性能優(yōu)化,都能更知其然和所以然。
第二部分,雅虎軍規(guī)是多年前提出的非常經(jīng)典的優(yōu)化建議。至今對于我們異常有很強(qiáng)的指導(dǎo)作用。你會發(fā)現(xiàn)它是從頁面加載、頁面渲染、到頁面交互全面的一個指導(dǎo)建議。與今天 Chrome 和 W3c 提出的 Web Vitals 思路依然類似。
第三部分,性能指標(biāo)。參考標(biāo)準(zhǔn)與業(yè)內(nèi)標(biāo)桿的建議,能更好地指導(dǎo)我們進(jìn)行優(yōu)化。
第四部分,性能工具。工欲善其事,必先利其器。這個道理大家都懂,運用好工具,才能讓我們更加事半功倍。
第五部分,監(jiān)控在性能優(yōu)化中占很重要的部分,"事前"監(jiān)控更重要,防患于未然。讓性能優(yōu)化成為一個預(yù)防者而不是追趕者。
羅里吧嗦說了很多,當(dāng)然還有很多性能優(yōu)化的細(xì)節(jié)沒有講到,如果有錯誤的地方歡迎指正?;蛘哂惺裁春梅椒ê媒ㄗh也強(qiáng)烈歡迎私聊交流一下。
沒有困難的工作參考文章:
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
視頻號最新視頻
總結(jié)
- 上一篇: 5月18发布会,这次TDSQL又有什么大
- 下一篇: 2017年html5行业报告,云适配发布