效能優化的重要性不言而喻,Google 的研究表明,當網站達到核心 Web 指標(Core Web Vitals)閾值時,使用者放棄載入網頁的可能性會降低 24%。
如何科學地定位到網頁的效能瓶頸,就需要找到一個合理的方式來測量和監控頁面的效能,確定優化的方向。
前端的效能監控分為 2 種:
本文的範例程式碼摘取自 shin-monitor,一款開源的前端監控指令碼。
為了便於記憶,特將此係列的所有重點內容濃縮成一張思維導圖。
W3C 在 2012 年制訂了第一版測量網頁效能的規範:Navigation Timing。下圖提供了頁面各階段可用的效能計時引數。
注意,若重定向是非同源,那麼帶下劃線的 redirectStart、redirectEnd、unloadStart、unloadEnd 四個值將一直都是 0。
W3C 在幾年後又制訂了第二版的規範:Navigation Timing Level 2,如下圖所示。
注意,在瀏覽器中,讀取 unloadEventStart 的值後,會發現這個時間點並不會像圖中那樣在 fetchStart 之前,因為 unload 不會阻塞頁面載入。
接下來,會用程式碼來演示效能引數的計算,後文中的 navigationStart 引數其實就是 startTime。
1)效能物件
第一版獲取效能引數的方法是呼叫 performance.timing,第二版的方法是呼叫 performance.getEntriesByType('navigation')[0]。
前者得到一個 PerformanceTiming 物件,後者得到一個 PerformanceNavigationTiming 物件。
在下面的程式碼中,若當前瀏覽器不支援第二版,則回退到第一版。不過,目前主流的瀏覽器對第一版的支援也比較好。
const timing = performance.getEntriesByType('navigation')[0] || performance.timing;
以我公司為例,投放到線上的頁面,其中只有大概 5.5% 的使用者讀取的第一版。
2)fetchStart
從上面的計時圖中可知,在 fetchStart 之前,瀏覽器會先處理重定向。
重定向的本質是在伺服器第一次響應時,返回 301 或 302 狀態碼,讓使用者端進行下一步請求。
會多走一遍響應流程,若不是像鑑權、短鏈等有意義的重定向,都應該要避免。
比較常見的有瀏覽器強制從 HTTP 頁面重定向到對應的 HTTPS 頁面,以及主域名的重定向,例如從 https://pwstrick.com 重定向至 https://www.pwstrick.com。
由於瀏覽器安全策略的原因,不同域名之間的重定向時間,是無法精確計算的,只能統計 fetchStart 之前的總耗時。
fetchStart 還會包含新分頁初始化的時間,但並不包括上一個頁面的 unload 時間。
由此可知,startTime 其實是在解除安裝上個頁面之後開始統計的。 fetchStart 最主要的優化手段就是減少重定向次數。
例如若頁面需要登入,則做成一個彈框,不要做頁面跳轉,還例如在編寫頁面時,不要顯式地為 URL 新增協定。
3)TCP
TCP 在建立連線之前,要經過三次握手,若是 HTTPS 協定,還要包括 SSL 握手,計算規則如下所示。
/** * SSL連線耗時 */ const sslTime = timing.secureConnectionStart; connectSslTime = sslTime > 0 ? timing.connectEnd - sslTime : 0; /** * TCP連線耗時 */ connectTime = timing.connectEnd - timing.connectStart;
在建立連線後,TCP 就可複用,所以有時候計算得到的值是 0。
若要減少 TCP 的耗時,可通過減少物理距離、使用 HTTP/3 協定等方法實現。
還有一種方法是通過 preconnect 提前建立連線,如下所示,瀏覽器會搶先啟動與該來源的連線。
<link rel="preconnect" href="https://pwstrick.com" />
4)TTFB
TTFB(Time To First Byte)是指讀取頁面第一個位元組的時間,即從發起請求到伺服器響應後收到的第一個位元組的時間差,用於衡量伺服器處理能力和網路的延遲。
TTFB 包括重定向、DNS 解析、TCP 連線、網路傳輸、伺服器響應等時間消耗的總和,計算規則就是 responseStart 減去 redirectStart。
TTFB = timing.responseStart - timing.redirectStart;
其實,TTFB 計算的是整個通訊的往返時間(Round-Trip Time,RTT),以及伺服器的處理時間。
所以,裝置之間的距離、網路傳輸路徑、資料庫慢查詢等因素都會影響 TTFB。
一般來說,TTFB 保持在 75ms 以內會比較完美,而在 200ms 以內會比較理想,若超過 500ms,使用者就會感覺到明顯地白屏。
TTFB 常用的優化手段包括增加 CDN 動態加速、減少請求的資料量、伺服器硬體升級、優化後端程式碼(引入快取、慢查詢優化等伺服器端的工作)。
5)FP 和 FCP
白屏(First Paint,FP)也叫首次繪製,是指螢幕從空白到顯示第一個畫面的時間,即渲染樹轉換成螢幕畫素的時刻。
這是使用者可感知到的一個效能引數,1 秒內是比較理想的白屏時間。
白屏時間的計算規則有 2 種:
const paint = performance.getEntriesByType("paint"); if (paint && timing.entryType && paint[0]) { firstPaint = paint[0].startTime - timing.fetchStart; } else { firstPaint = timing.responseEnd - timing.fetchStart; }
在實踐中發現,每天有大概 2 千條記錄中的白屏時間為 0,而且清一色的都是蘋果手機。
一番搜尋後,瞭解到,當 iOS 裝置通過瀏覽器的前進或後退按鈕進入頁面時,fetchStart、responseEnd 等效能引數很可能為 0。
還發現當初始頁面的結構中,若包含漸變的效果時,1 秒內的白屏佔比會從最高 94% 降低到 85%。
注意,PerformancePaintTiming 包含兩個效能資料,FP 和 FCP,理想情況下,兩者的值可相同。
FCP(First Contentful Paint)是指首次有實際內容渲染的時間,測量頁面從開始載入到頁面內容的任何部分在螢幕上完成渲染的時間。
內容是指文字、影象(包括背景影象)、svg 元素或非白色的 canvas 元素,不包括 iframe 中的內容。
網站效能測試工具 GTmetrix 認為 FCP 比較理想的時間是控制在 943ms 以內,位元組的標準是控制在 1.8s 內。
if (paint && timing.entryType && paint[1]) { firstContentfulPaint = paint[1].startTime - timing.fetchStart; } else { firstContentfulPaint = 0; }
影響上述兩個指標的主要因素包括網路傳輸和頁面渲染,優化的核心就是降低網路延遲以及加速渲染。
優化手段包括剔除阻塞渲染的 JavaScript 和 CSS、優化影象、壓縮合並檔案、延遲載入非關鍵資源、使用 HTTP/2 協定、SSR 等。
6)DOM
在效能計時圖中,有 4 個與 DOM 相關的引數,包括 domInteractive、domComplete、domContentLoadedEventStart 和 domContentLoadedEventEnd。
domInteractive 記錄的是在載入 DOM 並執行網頁的阻塞指令碼的時間。
在這個階段,具有 defer 屬性的指令碼還沒有執行,某些樣式表載入可能仍在處理並阻止頁面呈現。
domComplete 記錄的是完成解析 DOM 樹結構的時間。
在這個階段,DOM 中的所有指令碼,包括具有 async 屬性的指令碼,都已執行。並且開始載入 DOM 中定義的所有頁面靜態資源,包括影象、iframe 等。
loadEventStart 會緊跟在 domComplete 之後。在大多數情況下,這 2 個指標是相等的。在 loadEventStart 之前可能的延遲將由 onReadyStateChange 引起。
由 domInteractive 和 domComplete 兩個引數可計算出兩個 DOM 階段的耗時,如下所示。
initDomTreeTime = timing.domInteractive - timing.responseEnd; // 請求完畢至 DOM 載入的耗時 parseDomTime = timing.domComplete - timing.domInteractive; // 解析 DOM 樹結構的耗時
若 initDomTreeTime 過長的話,就需要給指令碼瘦身了。若 parseDomTime過長的話,就需要減少資源的請求了。
DOMContentLoaded(DCL)緊跟在 domInteractive 之後,該事件包含開始和結束兩個引數,jQuery.ready() 就是封裝了此事件。
該事件會在 HTML 載入完畢,並且 HTML 所參照的內聯和外連的非 async/defer 的同步 JavaScript 指令碼和 CSS 樣式都執行完畢後觸發,無需等待影象和 iframe 完成載入。
由 domContentLoadedEventEnd 可計算出使用者可操作時間,即 DOM Ready 時間。
domReadyTime = timing.domContentLoadedEventEnd - navigationStart; // 使用者可操作時間(DOM Ready時間)
注意,若 domContentLoadedEventEnd 高於 domContentLoadedEventStart,則說明該頁面中也註冊了此事件。
與 DCL 相比,load 事件觸發的時機要晚的多。
它會在頁面的 HTML、CSS、JavaScript(包括 async/defer)、影象等靜態資源都已經載入完之後才觸發。
Google 在眾多的效能指標中選出了幾個核心 Web 指標(Core Web Vitals),讓網站開發人員可以專注於這幾個指標的優化。
下表是關鍵指標的基準線,來源於位元組和 Google 的標準,除了 CLS,其他資料的單位都是毫秒。
Metric Name | Good | Needs Improvement | Poor |
FP | 0-1000 | 1000-2500 | > 2500 |
FCP | 0-1800 | 1800-3000 | > 3000 |
LCP | 0-2500 | 2500-4000 | > 4000 |
FID | 0-100 | 100-300 | > 300 |
TTI | 0-3800 | 3800-7300 | > 7300 |
CLS | <= 0.1 | <= 0.25 | > 0.25 |
1)LCP
LCP(Largest Contentful Paint)是指最大的內容在可視區域內變得可見的時間點,理想的時間是 2.5s 以內。
一般情況下,LCP 的時間都會比 FCP 大(如上圖所示),除非頁面非常簡單,FCP 的重要性也比 LCP 低很多。
LCP 的讀取並不需要手動計算,瀏覽器已經提供了 PerformanceObserver.observe() 方法,如下所示。
/** * 判斷當前宿主環境是否支援 PerformanceObserver * 並且支援某個特定的型別 */ private checkSupportPerformanceObserver(type: string): boolean { if(!(window as any).PerformanceObserver) return false; const types = (PerformanceObserver as any).supportedEntryTypes; // 瀏覽器相容判斷,不存在或沒有關鍵字 if(!types || types.indexOf(type) === -1) { return false; } return true; } /** * 瀏覽器 LCP 計算 */ public observerLCP(): void { const lcpType = 'largest-contentful-paint'; const isSupport = this.checkSupportPerformanceObserver(lcpType); // 瀏覽器相容判斷 if(!isSupport) { return; } const po = new PerformanceObserver((entryList): void=> { const entries = entryList.getEntries(); const lastEntry = (entries as any)[entries.length - 1] as TypePerformanceEntry; this.lcp = { time: rounded(lastEntry.renderTime || lastEntry.loadTime), // 時間取整 url: lastEntry.url, // 資源地址 element: lastEntry.element ? removeQuote(lastEntry.element.outerHTML) : '' // 參照的元素 }; }); // buffered 為 true 表示呼叫 observe() 之前的也算進來 po.observe({ type: lcpType, buffered: true } as any); /** * 當有按鍵或點選(包括捲動)時,就停止 LCP 的取樣 * once 引數是指事件被呼叫一次後就會被移除 */ ['keydown', 'click'].forEach((type): void => { window.addEventListener(type, (): void => { // 斷開此觀察者的連線 po.disconnect(); }, { once: true, capture: true }); }); }
entries 是一組 LargestContentfulPaint 型別的物件,它有一個 url 屬性,如果記錄的元素是個影象,那麼會儲存其地址。
註冊 keydown 和 click 事件是為了停止 LCP 的取樣,once 引數會在事件被呼叫一次後將事件移除。
在 iOS 的 WebView 中,只支援三種型別的 entryType,不包括 largest-contentful-paint,所以加了段瀏覽器相容判斷。
在頁面轉移到後臺後,得停止 LCP 的計算,因此需要找到隱藏到後臺的時間。
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity; // 記錄頁面隱藏時間 iOS 不會觸發 visibilitychange 事件 const onVisibilityChange = (event) => { // 頁面不可見狀態 if (lcp && document.visibilityState === 'hidden') { firstHiddenTime = event.timeStamp; // 移除事件 document.removeEventListener('visibilitychange', onVisibilityChange, true); } } document.addEventListener('visibilitychange', onVisibilityChange, true);
利用 visibilitychange 事件,就能準確得到隱藏時間,然後在讀取 LCP 時,大於這個時間的就直接忽略掉。不過在實踐中發現,iOS 的 WebView 並不支援此事件。
注意,largest-contentful-paint 不會計算 iframe 中的元素,返回上一頁也不會重新計算。
有個成熟的庫:web-vitals,提供了 LCP、FID、CLS、FCP 和 TTFB 指標,對上述所說的特殊場景做了處理,若要了解原理,可以參考其中的計算過程。
LCP 會被一直監控(其監控的元素如下所列),這樣會影響結果的準確性。
例如有個頁面首次進入是個彈框,確定後會出現動畫,增加些影象,DOM結構也都會跟著改變。
如果在等待一段時間,關閉頁面時才上報,那麼 LCP 將會很長,所以需要選擇合適的上報時機,例如 load 事件中。
2)FMP
FMP(First Meaningful Paint)是首次繪製有意義內容的時間,這是一個比較複雜的指標。
因為演演算法的通用性不夠高,探測結果也不理想,所以 Google 已經廢棄了 FMP,轉而採用含義更清晰的 LCP。
雖然如此,但網上仍然有很多開源的解決方案,畢竟 Google 是要找出一套通用方案,但我們並不需要通用。
只要結合那些方案,再寫出最適合自己環境的演演算法就行了,目前初步總結了一套計算 FMP 的步驟(僅供參考)。
首先,通過 MutationObserver 監聽每一次頁面整體的 DOM 變化,觸發 MutationObserver 的回撥。
然後在回撥中,為每個 HTML 元素(不包括忽略的元素)打上標記,記錄元素是在哪一次回撥中增加的,並且用陣列記錄每一次的回撥時間。
const IGNORE_TAG_SET = ['SCRIPT', 'STYLE', 'META', 'HEAD', 'LINK']; const WW = window.innerWidth; const WH = window.innerHeight; const FMP_ATTRIBUTE = '_ts'; class FMP { private cacheTrees: TypeTree[]; private callbackCount: number; private observer: MutationObserver; public constructor() { this.cacheTrees = []; // 快取每次更新的DOM元素 this.callbackCount = 0; // DOM 變化的計數 // 開始監控DOM的變化 this.observer = new MutationObserver((): void => { const mutationsList = []; // 從 body 元素開始遍歷 document.body && this.doTag(document.body, this.callbackCount++, mutationsList); this.cacheTrees.push({ ts: performance.now(), children: mutationsList }); // console.log("mutationsList", performance.now(), mutationsList); }); this.observer.observe(document, { childList: true, // 監控子元素 subtree: true // 監控後代元素 }); } /** * 為 HTML 元素打標記,記錄是哪一次的 DOM 更新 */ private doTag(target: Element, callbackCount: number, mutationsList: Element[]): void { const childrenLen = target.children ? target.children.length : 0; // 結束遞迴 if(childrenLen === 0) return; for (let children = target.children, i = childrenLen - 1; i >= 0; i--) { const child = children[i]; const tagName = child.tagName; if (child.getAttribute(FMP_ATTRIBUTE) === null && IGNORE_TAG_SET.indexOf(tagName) === -1 // 過濾掉忽略的元素 ) { child.setAttribute(FMP_ATTRIBUTE, callbackCount.toString()); mutationsList.push(child); // 記錄更新的元素 } // 繼續遞迴 this.doTag(child, callbackCount, mutationsList); } } }
接著在觸發 load 事件時,先過濾掉首屏外和沒有高度的元素,以及元素列表之間有包括關係的祖先元素,再計算各次變化時剩餘元素的總分。
一開始是隻記錄沒有後代的元素,但是後面發現有時候 DOM 變化時,沒有這類元素。
/** * 是否超出螢幕外 */ private isOutScreen(node: Element): boolean { const { left, top } = node.getBoundingClientRect(); return WH < top || WW < left; } /** * 讀取 FMP 資訊 */ public getFMP(): TypeMaxElement { this.observer.disconnect(); // 停止監聽 const maxObj = { score: -1, //最高分 elements: [], // 首屏元素 ts: 0 // DOM變化時的時間戳 }; // 遍歷DOM陣列,並計算它們的得分 this.cacheTrees.forEach((tree): void => { let score = 0; // 首屏內的元素 let firstScreenElements = []; tree.children.forEach((node): void => { // 只記錄元素 if(node.nodeType !== 1 || IGNORE_TAG_SET.indexOf(node.tagName) >= 0) { return; } const { height } = node.getBoundingClientRect(); // 過濾高度為 0,在首屏外的元素 if(height > 0 && !this.isOutScreen(node)) { firstScreenElements.push(node); } }); // 若首屏中的一個元素是另一個元素的後代,則過濾掉該祖先元素 firstScreenElements = firstScreenElements.filter((node): boolean => { // 只要找到一次包含關係,就過濾掉 const notFind = !firstScreenElements.some((item ): boolean=> node !== item && node.contains(item)); // 計算總得分 if(notFind) { score += this.caculateScore(node); } return notFind; }); // 得到最高值 if(maxObj.score < score) { maxObj.score = score; maxObj.elements = firstScreenElements; maxObj.ts = tree.ts; } }); // 在得分最高的首屏元素中,找出最長的耗時 return this.getElementMaxTimeConsuming(maxObj.elements, maxObj.ts); }
不同型別的元素,權重也是不同的,權重越高,對頁面呈現的影響也越大。
在 caculateScore() 函數中,通過getComputedStyle得到 CSS 類中的背景圖屬性,注意,node.style 只能得到內聯樣式中的屬性。
const TAG_WEIGHT_MAP = { SVG: 2, IMG: 2, CANVAS: 4, OBJECT: 4, EMBED: 4, VIDEO: 4 }; /** * 計算元素分值 */ private caculateScore(node: Element): number { const { width, height } = node.getBoundingClientRect(); let weight = TAG_WEIGHT_MAP[node.tagName] || 1; if (weight === 1 && window.getComputedStyle(node)['background-image'] && // 讀取CSS樣式中的背景圖屬性 window.getComputedStyle(node)['background-image'] !== 'initial' ) { weight = TAG_WEIGHT_MAP['IMG']; //將有影象背景的普通元素 權重設定為img } return width * height * weight; }
最後在得到分數最大值後,從這些元素中挑選出最長的耗時,作為 FMP。
/** * 讀取首屏內元素的最長耗時 */ private getElementMaxTimeConsuming(elements: Element[], observerTime: number): TypeMaxElement { // 記錄靜態資源的響應結束時間 const resources = {}; // 遍歷靜態資源的時間資訊 performance.getEntries().forEach((item: PerformanceResourceTiming): void => { resources[item.name] = item.responseEnd; }); const maxObj: TypeMaxElement = { ts: observerTime, element: '' }; elements.forEach((node: Element): void => { const stage = node.getAttribute(FMP_ATTRIBUTE); let ts = stage ? this.cacheTrees[stage].ts : 0; // 從快取中讀取時間 switch(node.tagName) { case 'IMG': ts = resources[(node as HTMLImageElement).src]; break; case 'VIDEO': ts = resources[(node as HTMLVideoElement).src]; !ts && (ts = resources[(node as HTMLVideoElement).poster]); // 讀取封面 break; default: { // 讀取背景圖地址 const match = window.getComputedStyle(node)['background-image'].match(/url\(\"(.*?)\"\)/); if(!match) break; let src: string; // 判斷是否包含協定 if (match && match[1]) { src = match[1]; } if (src.indexOf('http') == -1) { src = location.protocol + match[1]; } ts = resources[src]; break; } } // console.log(node, ts) if(ts > maxObj.ts) { maxObj.ts = ts; maxObj.element = node; } }); return maxObj; }
在將 LCP 和 FMP 兩個指標都算出後,就會取這兩者的較大值,作為首屏的時間。
在還未完成 FMP 演演算法之前,首屏採用的是兩種有明顯缺陷的計算方式。
3)FID
FID(First Input Delay)是使用者第一次與頁面互動(例如點選連結、按鈕等操作)到瀏覽器對互動作出響應的時間,比較理想的時間是控制在 100ms 以內。
FID 只關注不連續的操作,例如點選、觸控和按鍵,不包含捲動和縮放之類的連續操作。
這個指標是使用者對網站響應的第一印象,若延遲時間越長,那就會降低使用者對網站的整體印象。
減少站點初始化時間(即加速渲染)和消除冗長的任務(避免阻塞主執行緒)有助於消除首次輸入延遲。
在下圖的 Chrome DevTools Performance 面板中,描繪了一個繁忙的主執行緒。
如果使用者在較長的幀(600.9 毫秒和 994.5 毫秒)期間嘗試互動,那麼頁面的響應需要等待比較長的時間。
FID 的計算方式和 LCP 類似,也是藉助 PerformanceObserver 實現,如下所示。
public observerFID(): void { const fidType = 'first-input'; const isSupport = this.checkSupportPerformanceObserver(fidType); // 瀏覽器相容判斷 if(!isSupport) { return; } const po = new PerformanceObserver((entryList, obs): void => { const entries = entryList.getEntries(); const firstInput = (entries as any)[0] as TypePerformanceEntry; // 測量第一個輸入事件的延遲 this.fid = rounded(firstInput.processingStart - firstInput.startTime); // 斷開此觀察者的連線,因為回撥僅觸發一次 obs.disconnect(); }); po.observe({ type: fidType, buffered: true } as any); // po.observe({ entryTypes: [fidType] }); }
INP(Interaction to Next Paint)是 Google 的一項新指標,用於衡量頁面對使用者輸入的響應速度。
它測量使用者互動(如單擊或按鍵)與螢幕的下一次更新之間經過的時間,如下圖所示。
在未來,INP 將會取代 FID,因為 FID 有兩個限制:
4)TTI
TTI(Time to Interactive)是一個與互動有關的指標,它可測量頁面從開始載入到主要子資源完成渲染,並能夠快速、可靠地響應使用者輸入所需的時間。
它的計算規則比較繁瑣:
下圖有助於更直觀的瞭解上述步驟,其中數位與步驟對應,豎的橙色虛線就是 TTI 的時間點。
TBT(Total Blocking Time)是指頁面從 FCP 到 TTI 之間的阻塞時間,一般用來量化主執行緒在空閒之前的繁忙程度。
它的計算方式就是取 FCP 和 TTI 之間的所有長任務消耗的時間總和。
不過網上有些資料認為 TTI 可能會受當前環境的影響而導致測量結果不準確,因此更適合在實驗工具中測量,例如 LightHouse、WebPageTest 等
Google 的 TTI Polyfill 庫的第一句話就是不建議線上上搜集 TTI,建議使用 FID。
5)CLS
CLS(Cumulative Layout Shift)會測量頁面意外產生的累積佈局的偏移分數,即衡量佈局的穩定性。
佈局不穩定會影響使用者體驗,例如按鈕在使用者試圖點選時四處移動,或者文字在使用者開始閱讀後四處移動,而這類移動的元素會被定義成不穩定元素。
在下圖中,描繪了內容在頁面中四處移動的場景。
佈局偏移分數 = 影響分數 * 距離分數,而這個 CLS 分數應儘可能低,最好低於 0.1。
若要計算 CLS,可以參考 Layout Instability Metric 給出的思路或 onCLS.ts,藉助 PerformanceObserver 偵聽 layout-shift 的變化,如下所示。
let clsValue = 0; let clsEntries = []; let sessionValue = 0; let sessionEntries = []; new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { // 只將不帶有最近使用者輸入標誌的佈局偏移計算在內。 if (!entry.hadRecentInput) { const firstSessionEntry = sessionEntries[0]; const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; // 如果條目與上一條目的相隔時間小於 1 秒且 // 與對談中第一個條目的相隔時間小於 5 秒,那麼將條目 // 包含在當前對談中。否則,開始一個新對談。 if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) { sessionValue += entry.value; sessionEntries.push(entry); } else { sessionValue = entry.value; sessionEntries = [entry]; } // 如果當前對談值大於當前 CLS 值, // 那麼更新 CLS 及其相關條目。 if (sessionValue > clsValue) { clsValue = sessionValue; clsEntries = sessionEntries; // 將更新值(及其條目)記錄在控制檯中。 console.log('CLS:', clsValue, clsEntries) } } } }).observe({type: 'layout-shift', buffered: true});
優化 CLS 的手段有很多,例如一次性呈現所有內容、在某些內容仍在載入時使用預留位置、影象或視訊預設尺寸等。
在開篇就提出了量化效能的重要性,隨後就引出了兩個版本的效能規範,目前主流的是第二個版本。
根據瀏覽器提供的效能引數,分析了 fetchStart、TCP、TTFB、白屏的計算細節,並且說明了幾個影響 DOM 的效能引數。
最後詳細介紹了 Google 的核心Web指標,例如 LCP、FID、CLS 等。還介紹了一個已經廢棄,但還在廣泛使用的 FMP 指標。