你的網頁有多快 — 從 DOMReady 到 Element Timing

ES2049 發表於 2022-01-26

一些廢話

總所周知,寫文章需要一個標題。雖然我們搞程式碼的人一般都喜歡單刀直入,但是受制於文體的約束和發表載體的要求,有時不得不想一個標題。而起一個標題,不亞於起一個函式名或者變數名。單就這篇文章,我就有好幾個草稿標題,例如:《頁面載入指標演進之路》,《Element Timing:一種全新的頁面速度指標》,《如何最準確地測量網頁載入速度》,《新前端下的頁面載入速度》,甚至《Element Timing In Action》,《三分鐘學會測量頁面速度》。最後綜合考慮了讀者的承受能力,編輯的意見,以及最最重要的:本人的孱弱寫作實力,就取了個這樣的一個非常大眾化,既不會一眼就被當成垃圾,也不會被人挑出來仔細找茬的標題。

好了,廢話不能多說,直接進入正題。

DOMReady

上古時期(指距今 10+ 年前的 jQuery 紀元),我們開發網頁還停留在編寫靜態頁面結構,用 JS 指令碼對 DOM 進行直接操作,新增刪除一些額外頁面元素上。此時,DOMReady 基本就可以滿足計算頁面載入完成時間的需求,DOMReady (在 DOM 事件中是 DOMContentLoaded)代表頁面文件完全載入並解析完畢, 一般包含HTML文件分析以及DOM樹的建立、外鏈指令碼的載入、外鏈指令碼的執行以及內聯指令碼的執行,而不會等待樣式檔案,圖片檔案,子框架頁面的載入。一般在頁面 header 中打個時間戳,再在 jQuery 的 domReady 事件中打個時間戳,我們就可以計算到大致的 DOMReady 耗時了。

Navigation Timing

中古時期(指距今 10-5 年左右的 Ajax 紀元),網頁的互動形式更加豐富多樣,Gmail 為首的富網頁應用在使用者體驗大幅增強的同時,也給細粒度的網頁載入時間記錄帶來了需求。因此,從2010年開始Web 效能工作組就已經為 Web 引入了大量時間資訊記錄,可以通過 window 物件的 performance 屬性去獲取。這就是後來我們所熟知的 Navigation Timing。

Navigation Timing 介面所提供的資料大致如圖:
image.png
基本上囊括了從網頁開始網路請求到頁面完整載入並執行完資源並完成初始化 DOM 節點的時間。我們直接使用 performance.timing,就可以輕鬆獲得這些時間來幫助分析頁面的載入時間。

FP, FCP 和 LCP

近古時期(指距今 5-2 年左右的 React 紀元),由於各種前端框架(React,Vue 等)雨後春筍似的湧出,加上 Webpack 這種前端構建神器的出現,導致 Web 頁面的開發難度迅速下降,複雜度也直線上升。重前端的應用大行其道,頁面載入指令碼的時間也迅速變長,很多網站為了體驗採取了漸進式載入的策略,以解決等待指令碼執行時白屏時間過長的問題。因此,漸進式網頁渲染指標也應運而生。

漸進式網頁指標一般有這幾個:

  • 首次繪製(FP): 全稱 First Paint,標記瀏覽器渲染任何在視覺上不同於導航前螢幕內容之內容的時間點
  • 首次內容繪製(FCP):全稱 First Contentful Paint,標記的是瀏覽器渲染來自 DOM 第一位內容的時間點,該內容可能是文字、影像、SVG 甚至 <canvas> 元素。
  • 首次有效繪製(FMP):全稱 First Meaningful Paint,標記的是頁面主要內容繪製的時間點,例如視訊應用的視訊元件、天氣應用的天氣資訊、新聞應用中的新聞條目。
  • 最大內容繪製(LCP):全稱 Largest Contentful Paint,標記在可視區“內容”最大的可見元素開始繪製在螢幕上的時間點。

其中 FMP 因為依賴演算法猜測有效元素,所以目前已經基本被棄用。這幾個指標的視覺化意義可以參考以下兩張圖:

image.png

image.png

由於複雜頁面的元素往往很多,FCP 所觀測的元素可能只是 無足輕重 的一個 Loading 標記或者一個邊欄,因此對於真實的使用者來說,並不能代表頁面的“首屏時間”。反而,在某些邏輯複雜的頁面中,由於 JS 程式碼的執行時間長,或者依賴很多後端介面來渲染頁面,經常會導致頁面最重要的資料展示的時間遠遠長於頁面 OnLoadEvent 觸發的時間,此時,對於使用者來說最直觀感覺的到的“首屏時間”,往往就是 LCP 的時間。

image.png

這就是現在很多前端頁面效能工具都會把 LCP 列入一個重要的參考指標的原因。

Element Timing

現代時期(指距今 1-0 年左右的微前端紀元),LCP 的計算邏輯是瀏覽器給定的,在不同頁面中,瀏覽器所認為的 最大的可見元素 也未必是我們業務中 真正重要的 內容。並且在微前端流行的現代,不僅僅是同一應用的不同頁面採用單頁模式,甚至不同子應用的載入也可能通過 hash 路由來驅動。對於這種單頁應用來說,以上的各個指標其實都無法滿足在主體框架載入完成後切換不同頁面時的重新計算。那麼我們是不是隻能夠完全依賴業務開發本身去在程式碼裡主動打點和上報載入時間呢?那也未必。

讓我們來看看 W3C 的一個新草案,元素計時 API :https://wicg.github.io/element-timing/ 。儘管這個 API 還處於草案階段,但是 ChromeEdge 兩個瀏覽器其實早已在新版本給予了支援:相容性

Element Timing API 的目的是讓 Web 開發人員或分析工具能夠測量頁面上關鍵元素的渲染時間,比起 LCP ,我們能夠自己來定義關鍵元素,這正是Element Timing 的最大魅力。
​Element Timing 支援的元素有:

  • <img> 元素
  • <svg> 中的 <img> 元素
  • <video> 中的 poster image
  • 擁有 background-image 的元素
  • 一組文字節點

舉個例子:

<img src="image.jpg" elementtiming="big-image">
<p elementtiming="text" id="text-id">text here</p>
const observer = new PerformanceObserver((list) => {
  let entries = list.getEntries().forEach(function (entry) {
      console.log(entry);    
  });
});
observer.observe({ entryTypes: ["element"] });


// 輸出 entry 內容:
// {
//   duration: 0
//   element: p.aimake-site-name
//   entryType: "element"
//   id: ""
//   identifier: "text-id"
//   intersectionRect: DOMRectReadOnly {x: 236, y: 130, width: 144, height: 28, top: 130, …}
//   loadTime: 0
//   name: "text-paint"
//   naturalHeight: 0
//   naturalWidth: 0
//   renderTime: 10850.899999976158
//   startTime: 10850.899999976158
//   url: ""
// }

但是需要注意的是,並不是所有文字節點都可以通過新增 elementtiming="" 讓 Element Timing API 識別,WICG 的解釋中有一段注意事項:

We say that a text node belongs to the closest block-level Element ancestor of the node. This means that an element could have 0 or many associated text nodes with it.
We say that an element is text-painted if at least one text node belongs to and has been painted at least once. Thus, the text rendering timestamp of an element is the time when it becomes _text-painted_.
Let the text rect of a text node be the display rectangle of that node within the viewport. We define the text rect of an element as the smallest rectangle which contains the geometric union of the text rects of all text nodes which belong to the element.

讀起來比較難懂,但是實際上它想說明的是,只有滿足以下條件的文字節點才能夠被記錄:

  • 必須是塊級元素:如 <p><h1><div><section> 等,也就是說,單獨的 <span> 元素等行內元素,即時新增了 elementtiming="" 屬性也並不會被記錄。
  • 直接子節點必須包含一個或多個文字節點:例如 純文字,<span><i><b> 等,<p> 等塊級元素則不算,<img> 這種影像也不算。

舉一些例子就懂了:

<html>
  <p elementtiming = "p1"><p>1</p></p> <!-- 無效 -->
  <p elementtiming = "p2">2</p> <!-- 有效 -->
  <span elementtiming = "span1">span1</span> <!-- 無效 -->
  <div elementtiming = "div1"><image elementtiming = "img1" src="https://img.alicdn.com/tfs/xxx.png"></image></div> <!-- div1 無效,其中的 img1 有效 --> 
  <div elementtiming = "div2"><span>1</span></div> <!-- 有效 -->
  <div elementtiming = "div3"><p>2</p></div> <!-- 無效 -->
  <div elementtiming = "div4"><h1>3</h1></div> <!-- 無效 -->
  <div elementtiming = "div5"><b>4</b></div> <!-- 有效 -->
  <b elementtiming = "b1">b1</b> <!-- 無效 -->
  <i elementtiming = "i1">i1</i> <!-- 無效 -->
  <h1 elementtiming = "h1">h1</h1> <!-- 有效 -->
  <section elementtiming = "section1">section1</section> <!-- 有效 -->
</html>

在新增了自定義 elementtiming 屬性後,當所標記的影像或者文字節點被 真正渲染 時,瀏覽器就會記錄下時間。因此,我們可以在不同應用中讓開發同學直接給能夠標誌 首屏 的元素新增該屬性,即可由採集指令碼通過監聽 PerformanceObserver 來統一採集到元素繪製的時間點(renderTime)了。

通過使用 Element Timing API ,我們能夠更精確記錄到每個應用,頁面,甚至功能模組的載入時長。這才是最現代,最前沿的頁面載入時間方案,其餘方案最終都將被埋葬在歷史的塵埃中! 開個玩笑

image.png

另一些廢話

江山代有才人出,各領風騷數百年

一般來說,結尾並沒有標題那麼重要,因此我也不需要費腦子去想 N 個結尾了,直接簡單總結一下:無論前端發展到什麼程度,Timing 的標準總會追上你!

希望如果有讀者大神如果受到了一點點啟發,能夠給寫個包含 Element Timing 指標的效能庫造福我們。畢竟我就是懶,以上都是我自己 YY 的用法。在此先提前感謝了。

作者:ES2049 | 佚名

文章可隨意轉載,但請保留此原文連結。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 [email protected]