🧑?💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
前端監控主要分三個方向
前端性能(用戶體驗優化)
異常監控
業務指標跟
下面我來分別介紹三類指標如何獲取
1)前端性能指標:
一、用戶體驗相關的:
頁面加載時間(Page Load Time) :
定義:從用戶請求頁面到頁面完全加載的時間。
測量方法:使用Performance API中的performance.timing對象。
首次內容繪制(First Contentful Paint, FCP) :
定義:瀏覽器從DOM中渲染出第一個內容的時間點。
測量方法:使用PerformanceObserver來監聽paint條目。
最大內容繪制(Largest Contentful Paint, LCP) :
定義:頁面加載過程中最大可見內容的繪制時間。
測量方法:使用PerformanceObserver來監聽largest-contentful-paint條目。
首次輸入延遲(First Input Delay, FID) :
定義:用戶首次與頁面交互(如點擊按鈕)到瀏覽器開始響應交互的時間。
測量方法:使用EventListeners結合PerformanceObserver。
累積布局偏移(Cumulative Layout Shift, CLS) :
定義:頁面布局在加載過程中發生的意外變化的累積得分。
測量方法:使用PerformanceObserver來監聽layout-shift條目。
總阻塞時間(Total Blocking Time, TBT):
定義: TBT 是一個網頁性能指標,用于衡量從首次內容繪制(First Contentful Paint, FCP)到可交互時間(Time to Interactive, TTI)之間,所有超過 50 毫秒的長任務阻塞主線程的時間總和。TBT 反映了頁面在加載過程中用戶交互的延遲情況。
測量方法: 使用PerformanceObserver來監聽longtask條目。通過這種方式,可以捕獲并計算頁面在加載過程中所有長任務的阻塞時間。
交互時間(Time to Interactive, TTI) :
定義:頁面從開始加載到完全可交互的時間。
測量方法:通常通過分析資源加載和長任務來估算。雖然沒有直接的API,但可以結合Long Tasks API和其他指標來推測。
示例代碼
以下是一個簡單的示例代碼,展示如何使用Performance API和PerformanceObserver來測量這些指標:
// 頁面加載時間
window.addEventListener('load', () => {const timing = performance.timing;const pageLoadTime = timing.loadEventEnd - timing.navigationStart;console.log(`Page Load Time: ${pageLoadTime} ms`);
});// 首次內容繪制和最大內容繪制
const observer = new PerformanceObserver((list) => {const entries = list.getEntries();entries.forEach((entry) => {if (entry.name === 'first-contentful-paint') {console.log(`First Contentful Paint: ${entry.startTime} ms`);}if (entry.entryType === 'largest-contentful-paint') {console.log(`Largest Contentful Paint: ${entry.startTime} ms`);}});
});
observer.observe({ type: 'paint', buffered: true });
observer.observe({ type: 'largest-contentful-paint', buffered: true });// 首次輸入延遲
let firstInputDelay = 0;
const fidObserver = new PerformanceObserver((list) => {const entries = list.getEntries();for (const entry of entries) {firstInputDelay = entry.processingStart - entry.startTime;console.log(`First Input Delay: ${firstInputDelay} ms`);}
});
fidObserver.observe({ type: 'first-input', buffered: true });// 累積布局偏移
let cumulativeLayoutShiftScore = 0;
const clsObserver = new PerformanceObserver((list) => {for (const entry of list.getEntries()) {if (!entry.hadRecentInput) {cumulativeLayoutShiftScore += entry.value;console.log(`Cumulative Layout Shift: ${cumulativeLayoutShiftScore}`);}}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });// 長任務
const ttiObserver = new PerformanceObserver((entryList) => {const entries = entryList.getEntries();entries.forEach(entry => {console.log(`Long Task detected: ${entry.startTime} ms, duration: ${entry.duration} ms`);});
});ttiObserver.observe({ entryTypes: ['longtask'] });
總結:上面的指標performance.timing主要是采集一些基礎的時間,目前都是前后端分離的項目參考意義已經不大,除非是ssr和服務端的項目這部分指標有一些參考價值。像最大內容繪制largest-contentful-paint 是一個持續變動的指標,最后計算出最大時間,位移數據也是如此,長任務也是如此持續監聽過程。
二、頁面資源相關
在前端監控中資源監控方面,通常使用的是瀏覽器提供的Performance API。這個 API 提供了一組接口,對于資源監控,具體來說,可以使用以下接口:
PerformanceResourceTiming:
這個接口提供了關于每個資源加載的詳細計時信息。通過它,你可以獲取到資源加載的各個階段的時間,例如 DNS 查詢時間、TCP 連接時間、請求時間、響應時間等。
PerformanceObserver:
這是一個更為通用的接口,可以用來監聽各種性能條目(performance entry)。通過PerformanceObserver,你可以監聽到資源加載事件(resource類型),并在資源加載完成時獲取到相應的PerformanceResourceTiming對象。
以下是一個簡單的示例,展示如何使用PerformanceObserver來監控資源加載:
if ('PerformanceObserver' in window) {const observer = new PerformanceObserver((list) => {const entries = list.getEntriesByType('resource');entries.forEach((entry) => {console.log(`Resource: ${entry.name}`);console.log(`Start Time: ${entry.startTime}`);console.log(`Duration: ${entry.duration}`);console.log(`Initiator Type: ${entry.initiatorType}`);});});observer.observe({ type: 'resource', buffered: true });
}
總結:他可以對頁面所有的資源加載進行監控,比如 js,css,圖片還能夠對xmlhttprequest和fetch監控。也就是說他能夠對接口進行監控,當我們的頁面有一些超長的請求可以單獨識別出來進行報警。
下面是對請求接口的監控:
if ('PerformanceObserver' in window) {const observer = new PerformanceObserver((list) => {const entries = list.getEntriesByType('resource');entries.forEach((entry) => {if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {console.log(`API Request: ${entry.name}`);console.log(`Start Time: ${entry.startTime}`);console.log(`Duration: ${entry.duration}`);console.log(`Fetch Start: ${entry.fetchStart}`);console.log(`Response End: ${entry.responseEnd}`);console.log(`Transfer Size: ${entry.transferSize}`);}});});observer.observe({ type: 'resource', buffered: true });
}
三、其他一些不太重要的監控(不細講)
navigator.getBattery() : 電池監控
Performance Memory API: 內存監控
2)異常監控
一、監聽形式
監聽形式主要是通過捕獲全局錯誤事件來實現。這種方式可以捕獲大部分未處理的異常,并可以將其記錄或發送到服務器進行分析。
1. 使用window.onerror
window.onerror是最傳統的異常捕獲方法,可以捕獲運行時的 JavaScript 錯誤。
window.onerror = function(message, source, lineno, colno, error) {console.error('Error caught:', message, source, lineno, colno, error);// 可以在這里將錯誤信息發送到服務器sendErrorToServer({message,source,lineno,colno,error: error ? error.stack : null});
};function sendErrorToServer(errorInfo) {fetch('/log-error', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(errorInfo)});
}
2. 使用window.addEventListener(‘error’)
這種方法可以捕獲資源加載錯誤(如圖片、腳本加載失敗)。
window.addEventListener('error', function(event) {console.error('Resource Error:', event);// 處理資源加載錯誤if (event.target instanceof HTMLImageElement) {console.error('Image failed to load:', event.target.src);}// 可以在這里將錯誤信息發送到服務器sendErrorToServer({message: event.message,source: event.filename,lineno: event.lineno,colno: event.colno,error: event.error ? event.error.stack : null});
}, true);
3. 使用window.addEventListener(‘unhandledrejection’)
捕獲未處理的 Promise 異常,此處主要用來捕獲Promise沒有被catch的異常,屬于最后的兜底。
window.addEventListener('unhandledrejection', function(event) {console.error('Unhandled promise rejection:', event.reason);// 可以在這里將錯誤信息發送到服務器sendErrorToServer({message: event.reason ? event.reason.toString() : 'Unknown reason',error: event.reason && event.reason.stack ? event.reason.stack : null});
});
二、主動上報形式
主動上報形式是指在代碼中手動捕獲異常并上報。適用于需要在特定邏輯中捕獲異常的場景。
1. 使用try…catch
在代碼中使用try…catch來捕獲異常,并在catch塊中進行上報。
function riskyOperation() {try {// 執行可能拋出異常的代碼let result = potentiallyFailingFunction();console.log('Operation successful:', result);} catch (error) {console.error('Caught an error:', error);// 在這里上報錯誤sendErrorToServer({message: error.message,error: error.stack});}
}function potentiallyFailingFunction() {// 模擬可能拋出異常的代碼if (Math.random() > 0.5) {throw new Error('Random failure');}return 'Success';
}
2. 集成到應用框架
在現代前端框架(如 React、Vue)中,可以利用框架的錯誤邊界或錯誤處理機制來捕獲和上報異常。
React: 使用 Error Boundary
class ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {return { hasError: true };}componentDidCatch(error, errorInfo) {console.error('ErrorBoundary caught an error:', error, errorInfo);// 在這里上報錯誤sendErrorToServer({message: error.message,error: error.stack,componentStack: errorInfo.componentStack});}render() {if (this.state.hasError) {return <h1>Something went wrong.</h1>;}return this.props.children;}
}// 使用 ErrorBoundary 包裹組件
<ErrorBoundary><YourComponent />
</ErrorBoundary>
總結:我本人更加傾向于主動上報形式,因為在現在的開發形式里面,編譯工具和開發工具能夠幫我們避免絕大多數的異常情況,我們的監控往往因為業務邏輯和接口數據返回不對導致對業務的影響,所以主動上報能夠更加準確和清晰的給出異常,這樣利于我們去監控,往往比較多的異常問題不是錯誤導致更多就是接口不符合預期這樣window.onerror形式不太全,最多只能當一個輔助使用。
3、業務指標跟蹤
業務指標跟蹤也是前端監控的一部分,主要用于收集和分析用戶在應用中的行為數據,以幫助理解用戶的使用模式、評估功能效果、優化用戶體驗等。常見的業務指標包括頁面訪問量、點擊事件、用戶停留時間、轉化率等,很多公司都是單獨做,我所在的公司就是這樣,其實原理很簡單就是調用一個接口把想要上報的數據給帶過去就行。
let clickCount = 0;// 獲取按鈕元素
const button = document.getElementById('trackButton');// 為按鈕添加點擊事件監聽器
button.addEventListener('click', () => {clickCount++;console.log('按鈕點擊次數:', clickCount);
});// 頁面卸載時發送數據
window.addEventListener('beforeunload', () => {const data = JSON.stringify({ clickCount });// 使用 sendBeacon 發送數據,確保在頁面卸載時也能發送成功if (navigator.sendBeacon) {navigator.sendBeacon('/track-clicks', data);} else {// 備用方案,使用 fetch 發送數據fetch('/track-clicks', {method: 'POST',headers: {'Content-Type': 'application/json'},body: data});}
});
有個問題,為什么用navigator.sendBeacon去發送數據 ?這就涉及到如何上報數據的問題,下面我具體講下。
一、五種數據上報的形式
XMLHttpRequest:經典的 AJAX 請求方式,用于發送數據到服務器。可以配置為同步或異步請求,但在頁面卸載時可能不可靠。
Fetch API:現代瀏覽器中推薦的方式,用于發送網絡請求。相比XMLHttpRequest,它提供了更簡潔的語法,但在頁面卸載時仍可能會被中斷。
Navigator.sendBeacon:專門用于在頁面卸載時發送數據的 API。sendBeacon是非阻塞的,確保數據在頁面關閉時也能可靠地發送。
Image Ping:通過動態創建圖像對象并設置其src屬性來發送 GET 請求。這種方法常用于簡單的數據上報,不會受到跨域限制,但只能發送少量數據。
WebSocket:用于建立持久連接,實現雙向通信。適合需要實時數據傳輸的場景,但對于簡單的數據上報來說可能過于復雜。
一共有5種形式上報,我來分析下各有什么優劣勢:
其中可以XMLHttpRequest和Fetch沒有什么太大本質的區別屬于一類,他們都是比較常規的上報形式,但是他有個致命問題可能會被中斷,導致數據丟失,特別在頁面跳出的情況,還有就是在請求里面優先級高,在頁面加載的前期當有大量的上報的時候會阻塞頁面的請求看下圖:
Image Ping圖片上報,在跨域和調用方式上比較好,但是有個致命的缺點數據量帶不多。
WebSocket我見過的很少,可能在某些場景用的多,但是我很少見到
Navigator.sendBeacon專為頁面卸載設計,確保數據在頁面關閉時發送。非阻塞:不會影響頁面卸載速度。在上圖里面的優先級屬于低,使用簡單,但是有個問題,兼容性有點問題,可以配合fetch使用。他不會應為頁面切換丟數據,調用起來非常方便。
二、上報時機
數據上報的形式可以從不同的角度來分類和理解。一般來說,數據上報主要有以下幾種形式:
實時上報:
數據在產生后立即發送到服務器。這種方式確保數據的實時性,適用于需要即時分析和處理的場景。
批量上報:
數據在客戶端累積到一定數量或滿足特定條件后,再一次性發送到服務器。這種方式可以減少網絡請求次數,適用于對時效性要求不高的場景。
定時上報:
客戶端在固定的時間間隔內發送數據到服務器。這種方式可以平衡實時性和網絡資源的消耗。
頁面卸載上報: 在頁面關閉或卸載時發送數據,通常使用sendBeaconAPI。這種方式確保在用戶離開頁面時也能可靠地將數據發送到服務器。
事件驅動上報:
基于特定事件觸發數據上報,例如用戶點擊按鈕、表單提交等。這種方式適用于需要對特定用戶行為進行監控的場景。
一共有5種形式時機,我來分析下各有什么優劣勢:
實時上報:
優點:
實時性:數據在產生后立即發送,適合需要即時分析的場景。
準確性:可以立即捕捉用戶行為,減少數據丟失的風險。
缺點:
網絡開銷:頻繁發送請求可能增加網絡負擔。
服務器壓力:需要服務器能夠處理高頻率的數據請求。
批量上報:
優點:
減少請求次數:通過合并多條數據減少網絡請求。
提高效率:降低網絡和服務器的負擔。
缺點:
時效性:數據不是實時發送,可能導致分析滯后。
數據丟失風險:如果客戶端崩潰或斷網,未發送的數據可能丟失。
定時上報:
優點:
平衡實時性和效率:通過定時發送,減少頻繁請求。
可控性:可以根據需求調整上報頻率。
缺點:
復雜性:需要實現定時器邏輯。
數據可能不夠實時:與實時上報相比,數據可能有延遲。
頁面卸載上報:
優點:
可靠性:利用sendBeacon確保數據在頁面關閉時發送。
簡單性:不需要復雜的邏輯,只需在頁面卸載時觸發。
缺點:
數據量限制:通常只能發送較小的數據量。
依賴瀏覽器支持:如果瀏覽器不支持sendBeacon,需要備用方案。
事件驅動上報:
優點:
精確性:針對特定用戶行為進行上報,適合精準分析。
靈活性:可以根據業務需求定制上報邏輯。
缺點:
復雜性:需要為每個事件設置監聽器和上報邏輯。
潛在的性能問題:過多的事件監聽可能影響頁面性能。
看你業務需要那種形式,去評估那種形式上報。
我來總結下我在公司遇到的幾種情況:公司H5目前用的最多的就是第一種,但是在客戶端里面是批量,我理解客戶端的形式是最好的,H5的頁面如果在端里面可以借助端的能力去實現批量上報。我們也可以在h5里面頁面卸載的時候去上報,我自己測試過好像有一些兼容問題,最后還是放棄了用的實時上報。
三、最后來個小問題,如何對你的頁面程序方法運行時間進行監控?
頁面程序方法的運行時間進行監控是性能優化的重要一環,可以幫助識別性能瓶頸和優化點。以下是一些常用的方法來監控方法的運行時間:
使用console.time和console.timeEnd:
這是最簡單的方法之一,適用于開發和調試階段。
使用console.time(‘label’)開始計時,console.timeEnd(‘label’)結束計時,并在控制臺輸出運行時間。
console.time('myFunction');myFunction();console.timeEnd('myFunction');
使用performance.now() :
performance.now()提供高精度的時間戳,可以用于精確測量函數的執行時間。
const start = performance.now();myFunction();const end = performance.now();console.log(`myFunction took ${end - start} milliseconds`);
使用PerformanceAPI:
瀏覽器的PerformanceAPI 提供了更全面的性能測量工具,可以記錄和分析多個時間點。
可以使用performance.mark()和performance.measure()來標記和測量代碼片段。
performance.mark('start');myFunction();
performance.mark('end');
performance.measure('myFunction', 'start', 'end');const measures = performance.getEntriesByName('myFunction');console.log(measures[0].duration);
使用第三方庫:
有一些第三方庫可以幫助監控和分析性能,如stats.js或New Relic等。
這些工具通常提供更詳細的性能分析和報告功能。
自定義監控工具:
如果需要更復雜的監控,可以編寫自定義的監控工具,結合performance.now()和日志記錄,將數據發送到服務器進行分析。
目前用的比較多的是performance.mark()因為他是用的比new date提供高精度的時間戳,具體的可以自己去查文檔,我懶得講performance.mark()是 Web Performance API 提供的一個方法,用于在瀏覽器的性能時間線上創建一個標記。這個標記可以幫助開發者測量特定代碼段的執行時間,尤其是在需要精確測量性能的復雜應用中。
用法
創建標記: 使用performance.mark(name)來創建一個標記,其中name是一個字符串,用于標識這個標記。例如:
performance.mark('start');// 執行一些代碼
performance.mark('end');
測量時間: 使用performance.measure(name, startMark, endMark)方法來測量兩個標記之間的時間間隔。name是測量的名稱,startMark和endMark是之前創建的標記。例如:
performance.measure('myMeasure', 'start', 'end');
獲取測量結果: 使用performance.getEntriesByName(name)來獲取測量結果。這個方法返回一個數組,其中包含所有與指定名稱匹配的測量結果。例如:
const measures = performance.getEntriesByName('myMeasure');
measures.forEach((measure) => {console.log(`${measure.name}: ${measure.duration}ms`);
});
我以前用過一個蠢方法就是用mark形式對接口進行打點監控,其實前面見過用資源監控就能完成,后面我為了精準的監控頁面真實的渲染時間,用過mack在頁面渲染完成的時間點去打mark,后面發現有lcp后放棄,但是我認為在有些特殊場景還是能用上,特別在性能優化到后期實在找不到優化點的時候可以對具體的某些方法進行監控優化。
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。