深入淺出前端監控

背景

近期主要工作內容是進校開放平臺(簡稱開平)相關業務,開平簡單來說就是一個可爲第三方應用提供接入主端(例如微信、飛書)應用能力的平臺,爲了讓第三方應用穩定可靠地接入開平,需要爲其提供一些底層的基礎能力,其中應用監控就是其中不可或缺的一環。目前如何在進校開平中做三方應用的監控管理還在初步預研階段,爲此瞭解了一下前端監控相關背景知識。鑑於我司已有一套非常完善的 APM 平臺,因此下文諸多理論和源碼參考自我司 APM Web SDK 源碼。

監控流程

  1. 數據採集:明確需要採集哪些指標以及採集的方式。

  2. 數據上報:將上一步採集的數據以一定的策略進行上報。

  3. 數據清洗、存儲:服務端在接收到上報數據後需要對數據進行清洗和存儲。

  4. 數據消費:數據最終會在類似 Slardar Web 這樣的監控平臺以圖、表等形式分類別地進行可視化展示,並提供諸如監控報警等消費能力。

上述流程看似不復雜但每個環節的技術細節都非常多,本文主要關注前端視角下的數據採集和上報環節。

數據採集

做好前端監控的第一步要明確哪些數據是值得我們採集的,前端環境下監控數據從大的維度上可劃分成環境信息、異常數據和性能數據:

環境信息

採集的監控數據一般都會設置一些通用的環境信息,這些環境信息可以提供更多的維度以幫助用戶發現問題和解決問題。下圖列舉了一些常見的環境信息:

異常監控

JS 異常

Script Error

先拋開如何採集 JS 異常信息不談,在採集之前如果連報錯信息都不全那麼即使採集到了這樣的數據也是無效的。巧的是,確實存在這樣一個場景:當頁面加載自不同域的腳本(例如頁面的 JS 託管在 CDN)中發生語法錯誤時,瀏覽器基於安全機制考慮,不會給出語法錯誤的細節,而是簡單的 Script error.

因此,如果你希望自己頁面的詳細報錯信息被監控 SDK 捕獲你需要爲頁面中的腳本 script 添加 crossorigin= anonymous 屬性,且腳本所在的服務設置 CORS 響應頭 Access-Control-Allow-Origin: * ,這是 JS 異常監控的第一項準備工作。

編譯時與運行時錯誤

常見的 JS 錯誤可分爲編譯時錯誤和運行時錯誤,其中編譯時錯誤在 IDE 層面就會給出提示,一般不會流入到線上,因此編譯時錯誤不在監控範圍。

有的同學說在 Slardar 上時常看到 SyntaxError 的字樣,這種情況一般都是 JSON.parse 解析出錯或瀏覽器兼容性問題導致,屬於運行時錯誤並非編譯時錯誤。

對於異常監控我們主要關注 JS 運行時錯誤,多數場景下的處理手段如下:

r3jPYz

整體來看,監控 SDK 會在全局幫助用戶去捕獲他們沒有自行感知的異常並上報,對於自行捕獲的異常一般會提供手動上報接口進行上報。

SourceMap

假設現在已經採集到頁面目前存在的 JS 異常並做了上報,最終消費時你我們當然希望看到的是錯誤的初始來源和調用堆棧,但實際發生報錯的 JS 代碼都經過各種轉換混淆壓縮,早已面目全非了,因此這裏需要藉助打包階段生成的 SourceMap 做一個反向解析得到原始報錯信息的上下文。

以 Sentry (Slardar 也有用到 Sentry)爲例大致流程如下:

  1. 採集側收集錯誤信息發送到監控平臺服務端。

  2. 接入的業務方自行上傳 SourceMap 文件到監控平臺服務端,上傳完成後刪除本地的 SourceMap 文件,且打包後的 js 文件末尾不需要 SourceMap URL,最大程度避免 SourceMap 泄漏。

  3. 服務端通過 source-map 工具結合 SourceMap 和原始錯誤信息定位到源碼具體位置。

靜態資源加載異常

靜態資源加載異常的捕獲存在兩種方式:

  1. 在出現靜態資源加載異常的元素的 onerror 方法中處理。

  2. 資源加載異常觸發的error事件不會冒泡,因此使window.addEventListener('error', cb, true) 在事件捕獲階段進行捕獲。

第一種方式侵入性太強,不夠優雅,目前主流方案均採用第二種方式進行監控:

捕獲靜態資源加載異常

APM 平臺一般會有所有靜態資源加載的明細,其原理是通過 PerformanceResourceTiming API 來採集靜態資源加載的基本情況,這裏不做展開。

請求異常

業務中的 AJAX 請求或者 Fetch 請求在不同的網絡環境或者客戶端環境會有不穩定的表現,這些不穩定的情況我們很難通過本地測試的途徑進行測試或者感知得到,所以我們需要對 HTTP 請求進行線上監控,通過將 HTTP 請求異常上報的方式對錯誤日誌進行採集,然後進行一系列的分析和監控。

請求異常通常泛指 HTTP 請求失敗或者 HTTP 請求返回的狀態碼非 20X。

那麼請求異常監控怎麼做呢?普遍採用的方式是對原生的 XMLHttpRequest 對象和 fetch 方法進行重寫,從而在代理對象中實現狀態碼的監聽和錯誤上報:

重寫 XMLHttpRequest 對象

重寫 fetch 方法

當然了,重寫上述方法後除了異常請求可以被監控到之外,正常響應的請求狀態自然也能被採集到,比如 Slardar 會將對所有上報請求的持續時間進行分析從而得出慢請求的佔比:

PS:如果通過 XHR 或 fetch 來上報監控數據的話,上報請求也會被被攔截,可以有選擇地做一層過濾處理。

卡頓異常

卡頓指的是顯示器刷新時下一幀的畫面還沒有準備好,導致連續多次展示同樣的畫面,從而讓用戶感覺到頁面不流暢,也就是所謂的掉幀,衡量一個頁面是否卡頓的指標就是我們熟知的 FPS。

如何獲取 FPS

Chrome DevTool 中有一欄 Rendering 中包含 FPS 指標,但目前瀏覽器標準中暫時沒有提供相應 API ,只能手動實現。這裏需要藉助 requestAnimationFrame 方法模擬實現,瀏覽器會在下一次重繪之前執行 rAF 的回調,因此可以通過計算每秒內 rAF 的執行次數來計算當前頁面的 FPS

通過 rAF 計算 FPS

如何上報 “真實卡頓”

從技術角度看 FPS 低於 60 即視爲卡頓,但在真實環境中用戶很多行爲都可能造成 FPS 的波動,並不能無腦地把 FPS 低於 60 以下的 case 全部上報,會造成非常多無效數據,因此需要結合實際的用戶體驗重新定義 “真正的卡頓”,這裏貼一下司內 APM 平臺的上報策略

  1. 頁面 FPS 持續低於預期:當前頁面連續 3s FPS 低於 20。

  2. 用戶操作帶來的卡頓:當用戶進行交互行爲後,渲染新的一幀的時間超過 16ms + 100ms。

崩潰異常

Web 頁面崩潰指在網頁運行過程頁面完全無響應的現象,通常有兩種情況會造成頁面崩潰:

  1. JS 主線程出現無限循環,觸發瀏覽器的保護策略,結束當前頁面的進程。

  2. 內存不足

發生崩潰時主線程被阻塞,因此對崩潰的監控只能在獨立於 JS 主線程的 Worker 線程中進行,我們可以採用 Web Worker 心跳檢測的方式來對主線程進行不斷的探測,如果主線程崩潰,就不會有任何響應,那就可以在 Worker 線程中進行崩潰異常的上報。這裏繼續貼一下 Slardar 的檢測策略:

崩潰檢測

性能監控

性能監控並不只是簡單的監控 “頁面速度有多快”,需要從用戶體驗的角度全面衡量性能指標。(就是所謂的 RUM 指標)目前業界主流標準是 Google 最新定義的 Core Web Vitals:

可以看到最新標準中,以往熟知的 FP、FCP、FMP、TTI 等指標都被移除了,個人認爲這些指標還是具備一定的參考價值,因此下文還是會將這些指標進行相關介紹。(谷歌的話不聽不聽🙉)

Loading 加載

和 Loading 相關的指標有 FPFCPFMPLCP,首先來看一下我們相對熟悉的幾個指標:

FP/FCP/FMP

一張流傳已久的圖

這兩個指標都通過 PerformancePaintTiming API 獲取:

通過 PerformancePaintTiming 獲取 FP 和 FCP

下面再來看 FMP 的定義和獲取方式:

FMP 的計算相對複雜,因爲瀏覽器並未提供相應的 API,在此之前我們先看一組圖:

從圖中可以發現頁面渲染過程中的一些規律:

  1. 在 1.577 秒,頁面渲染了一個搜索框,此時已經有 60 個佈局對象被添加到了佈局樹中。

  2. 在 1.760 秒,頁面頭部整體渲染完成,此時佈局對象總數是 103 個。

  3. 在 1.907 秒,頁面主體內容已經繪製完成,此時有 261 個佈局對象被添加到佈局樹中從用戶體驗的角度看,此時的時間點就是是 FMP。

可以看到佈局對象的數量與頁面完成度高度相關。業界目前比較認可的一個計算 FMP 的方式就是——「頁面在加載和渲染過程中最大布局變動之後的那個繪製時間即爲當前頁面的 FMP 」

實現原理則需要通過 MutationObserver 監聽 document 整體的 DOM 變化,在回調計算出當前 DOM 樹的分數,分數變化最劇烈的時刻,即爲 FMP 的時間點

至於如何計算當前頁面 DOM 🌲的分數,LightHouse 的源碼中會根據當前節點深度作爲變量做一個權重的計算,具體實現可以參考 LightHouse 源碼。

const curNodeScore = 1 + 0.5 * depth;
const domScore = 所有子節點分數求和

上述計算方式性能開銷大且未必準確,LightHouse 6.0 已明確廢棄了 FMP 打分項,建議在具體業務場景中根據實際情況手動埋點來確定 FMP 具體的值,更準確也更高效。

LCP

沒錯,LCP (Largest Contentful Paint) 是就是用來代替 FMP 的一個性能指標 ,用於度量視口中最大的內容元素何時可見,可以用來確定頁面的主要內容何時在屏幕上完成渲染。

使用 Largest Contentful Paint API 和 PerformanceObserver 即可獲取 LCP 指標的值:

獲取 LCP

Interactivity 交互

TTI

TTI(Time To Interactive) 表示從頁面加載開始到頁面處於完全可交互狀態所花費的時間, TTI 值越小,代表用戶可以更早地操作頁面,用戶體驗就更好。

這裏定義一下什麼是完全可交互狀態的頁面

  1. 頁面已經顯示有用內容。

  2. 頁面上的可見元素關聯的事件響應函數已經完成註冊。

  3. 事件響應函數可以在事件發生後的 50ms 內開始執行(主線程無 Long Task)。

TTI 的算法略有些複雜,結合下圖看一下具體步驟:

TTI 示意圖

Long Task: 阻塞主線程達 50 毫秒或以上的任務。

  1. 從 FCP 時間開始,向前搜索一個不小於 5s 的靜默窗口期。(靜默窗口期定義:窗口所對應的時間內沒有 Long Task,且進行中的網絡請求數不超過 2 個)

  2. 找到靜默窗口期後,從靜默窗口期向後搜索到最近的一個 Long Task,Long Task 的結束時間即爲 TTI。

  3. 如果一直找到 FCP 時刻仍然沒有找到 Long Task,以 FCP 時間作爲 TTI。

其實現需要支持 Long Tasks API 和 Resource Timing API,具體實現感興趣的同學可以按照上述流程嘗試手動實現。

FID

FID(First Input Delay) 用於度量用戶第一次與頁面交互的延遲時間,是用戶第一次與頁面交互到瀏覽器真正能夠開始處理事件處理程序以響應該交互的時間。

其實現使用簡潔的 PerformanceEventTiming API 即可,回調的觸發時機是用戶首次與頁面發生交互並得到瀏覽器響應(點擊鏈接、輸入文字等)。

獲取 FID

至於爲何新的標準中採用 FID 而非 TTI,可能存在以下幾個因素:

Visual Stability 視覺穩定

CLS

CLS(Cumulative Layout Shift) 是對在頁面的整個生命週期中發生的每一次意外佈局變化的最大布局變化得分的度量,佈局變化得分越小證明你的頁面越穩定

聽起來有點複雜,這裏做一個簡單的解釋:

舉個例子,一個佔據頁面高度 50% 的元素,向下偏移了 25%,那麼其得分爲 0.75 * 0.25,大於標準定義的 0.1 分,該頁面就視爲視覺上沒那麼穩定的頁面。

使用 Layout Instability API 和 PerformanceObserver 來獲取 CLS:

獲取 CLS

一點感受:在翻閱諸多參考資料後,私以爲性能監控是一件長期實踐、以實際業務爲導向的事情,業內主流標準日新月異,到底監控什麼指標是最貼合用戶體驗的我們不得而知,對於 FMP、FPS 這類瀏覽器未提供 API 獲取方式的指標花費大量力氣去探索實現是否有足夠的收益也存在一定的疑問,但毋容置疑的是從自身頁面的業務屬性出發,結合一些用戶反饋再進行相關手段的優化可能是更好的選擇。(更推薦深入瞭解瀏覽器渲染原理,寫出性能極佳的頁面,讓 APM 同學失業

數據上報

得到所有錯誤、性能、用戶行爲以及相應的環境信息後就要考慮如何進行數據上報,理論上正常使用 ajax 即可,但有一些數據上報可能出現在頁面關閉 (unload) 的時刻,這些請求會被瀏覽器的策略 cancel 掉,因此出現了以下幾種解決方案:

  1. 優先使用 Navigator.sendBeacon,這個 API 就是爲了解決上述問題而誕生,它通過 HTTP POST 將數據異步傳輸到服務器且不會影響頁面卸載。

  2. 如果不支持上述 API,動態創建一個 <img / > 標籤將數據通過 url 拼接的方式傳遞。

  3. 使用同步 XHR 進行上報以延遲頁面卸載,不過現在很多瀏覽器禁止了該行爲。

Slardar 採取了第一種方式,不支持 sendBeacon 則使用 XHR,偶爾丟日誌的原因找到了。

由於監控數據通常量級都十分龐大,因此不能簡單地採集一個就上報一個,需要一些優化手段:

總結

本文旨在提供一個相對體系的前端監控視圖,幫助各位瞭解前端監控領域我們能做什麼、需要做什麼。此外,如果能對頁面性能和異常處理有着更深入的認知,無論是在開發應用時的自我管理(減少 bug、有意識地書寫高性能代碼),還是自研監控 SDK 都有所裨益。

如何設計監控 SDK 不是本文的重點,部分監控指標的定義和實現細節也可能存在其他解法,實現一個完善且健壯的前端監控 SDK 還有很多技術細節,例如每個指標可以提供哪些配置項、如何設計上報的維度、如何做好兼容性等等,這些都需要在真實的業務場景中不斷打磨和優化才能趨於成熟。

參考

Google Developer

❤️ 謝謝支持

以上便是本次分享的全部內容,希望對你有所幫助 ^_^

歡迎關注公衆號 ELab 團隊 收貨大廠一手好文章~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/I9tTlYjKmnfrpyc6hibY1Q