迄今為止最全的前端監(jiān)控體系搭建篇(長文預警)
點擊上方?前端Q,關(guān)注公眾號
回復加群,加入前端Q技術(shù)交流群
原文鏈接: https://juejin.cn/post/7078512301665419295?share_token=37a59eca-da19-4cfc-aeb2-67d4c0d9b0c2
作者: 火車頭
概覽
為什么要做前端監(jiān)控
前端監(jiān)控目標
前端監(jiān)控流程
編寫采集腳本
日志系統(tǒng)監(jiān)控
錯誤監(jiān)控
接口異常
白屏監(jiān)控
加載時間
性能指標
卡頓
pv
擴展問題
性能監(jiān)控指標
前端怎么做性能監(jiān)控
線上錯誤監(jiān)控怎么做
導致內(nèi)存泄漏的方法,怎么監(jiān)控內(nèi)存泄漏
Node 怎么做性能監(jiān)控
源碼地址 ? ? ?(https://github.com/miracle90/monitor)
1. 為什么要做前端監(jiān)控
更快的發(fā)現(xiàn)問題和解決問題
做產(chǎn)品的決策依據(jù)
為業(yè)務(wù)擴展提供了更多可能性
提升前端工程師的技術(shù)深度和廣度,打造簡歷亮點
2. 前端監(jiān)控目標
2.1 穩(wěn)定性 stability
js錯誤:js執(zhí)行錯誤、promise異常
資源錯誤:js、css資源加載異常
接口錯誤:ajax、fetch請求接口異常
白屏:頁面空白
2.2 用戶體驗 experience
2.3 業(yè)務(wù) business
pv:頁面瀏覽量和點擊量
uv:訪問某個站點的不同ip的人數(shù)
用戶在每一個頁面的停留時間
3. 前端監(jiān)控流程
前端埋點
數(shù)據(jù)上報
加工匯總
可視化展示
監(jiān)控報警

3.1 常見的埋點方案
3.1.1 代碼埋點
嵌入代碼的形式
優(yōu)點:精確(任意時刻,數(shù)據(jù)量全面)
缺點:代碼工作量點
3.1.2 可視化埋點
通過可視化交互的手段,代替代碼埋點
將業(yè)務(wù)代碼和埋點代碼分離,提供一個可視化交互的頁面,輸入為業(yè)務(wù)代碼,通過這個系統(tǒng),可以在業(yè)務(wù)代碼中自定義的增加埋點事件等等,最后輸出的代碼耦合了業(yè)務(wù)代碼和埋點代碼
用系統(tǒng)來代替手工插入埋點代碼
3.1.3 無痕埋點
前端的任意一個事件被綁定一個標識,所有的事件都被記錄下來
通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數(shù)據(jù),并生成可視化報告供專業(yè)人員分析
無痕埋點的優(yōu)點是采集全量數(shù)據(jù),不會出現(xiàn)漏埋和誤埋等現(xiàn)象
缺點是給數(shù)據(jù)傳輸和服務(wù)器增加壓力,也無法靈活定制數(shù)據(jù)結(jié)構(gòu)
4. 編寫采集腳本
4.1 接入日志系統(tǒng)
各公司一般都有自己的日志系統(tǒng),接收數(shù)據(jù)上報,例如:阿里云
4.2 監(jiān)控錯誤
4.2.1 錯誤分類
js錯誤(js執(zhí)行錯誤,promise異常)
資源加載異常:監(jiān)聽error
4.2.2 數(shù)據(jù)結(jié)構(gòu)分析
1. jsError
{
"title": "前端監(jiān)控系統(tǒng)", // 頁面標題
"url": "http://localhost:8080/", // 頁面URL
"timestamp": "1590815288710", // 訪問時間戳
"userAgent": "Chrome", // 用戶瀏覽器類型
"kind": "stability", // 大類
"type": "error", // 小類
"errorType": "jsError", // 錯誤類型
"message": "Uncaught TypeError: Cannot set property 'error' of undefined", // 類型詳情
"filename": "http://localhost:8080/", // 訪問的文件名
"position": "0:0", // 行列信息
"stack": "btnClick (http://localhost:8080/:20:39)^HTMLInputElement.onclick (http://localhost:8080/:14:72)", // 堆棧信息
"selector": "HTML BODY #container .content INPUT" // 選擇器
}
2. promiseError
{
...
"errorType": "promiseError",//錯誤類型
"message": "someVar is not defined",//類型詳情
"stack": "http://localhost:8080/:24:29^new Promise ()^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)" ,//堆棧信息
"selector": "HTML BODY #container .content INPUT"//選擇器
}
3. resourceError
...
"errorType": "resourceError",//錯誤類型
"filename": "http://localhost:8080/error.js",//訪問的文件名
"tagName": "SCRIPT",//標簽名
"timeStamp": "76",//時間
4.2.3 實現(xiàn)
1. 資源加載錯誤 + js執(zhí)行錯誤
//一般JS運行時錯誤使用window.onerror捕獲處理
window.addEventListener(
"error",
function (event) {
let lastEvent = getLastEvent();
// 有 e.target.src(href) 的認定為資源加載錯誤
if (event.target && (event.target.src || event.target.href)) {
tracker.send({
//資源加載錯誤
kind: "stability", //穩(wěn)定性指標
type: "error", //resource
errorType: "resourceError",
filename: event.target.src || event.target.href, //加載失敗的資源
tagName: event.target.tagName, //標簽名
timeStamp: formatTime(event.timeStamp), //時間
selector: getSelector(event.path || event.target), //選擇器
});
} else {
tracker.send({
kind: "stability", //穩(wěn)定性指標
type: "error", //error
errorType: "jsError", //jsError
message: event.message, //報錯信息
filename: event.filename, //報錯鏈接
position: (event.lineNo || 0) + ":" + (event.columnNo || 0), //行列號
stack: getLines(event.error.stack), //錯誤堆棧
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "", //CSS選擇器
});
}
},
true
); // true代表在捕獲階段調(diào)用,false代表在冒泡階段捕獲,使用true或false都可以
2. promise異常
//當Promise 被 reject 且沒有 reject 處理器的時候,會觸發(fā) unhandledrejection 事件
window.addEventListener(
"unhandledrejection",
function (event) {
let lastEvent = getLastEvent();
let message = "";
let line = 0;
let column = 0;
let file = "";
let stack = "";
if (typeof event.reason === "string") {
message = event.reason;
} else if (typeof event.reason === "object") {
message = event.reason.message;
}
let reason = event.reason;
if (typeof reason === "object") {
if (reason.stack) {
var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
if (matchResult) {
file = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = getLines(reason.stack);
}
}
tracker.send({
//未捕獲的promise錯誤
kind: "stability", //穩(wěn)定性指標
type: "error", //jsError
errorType: "promiseError", //unhandledrejection
message: message, //標簽名
filename: file,
position: line + ":" + column, //行列
stack,
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
},
true
); // true代表在捕獲階段調(diào)用,false代表在冒泡階段捕獲,使用true或false都可以
4.3 接口異常采集腳本
4.3.1 數(shù)據(jù)設(shè)計
{
"title": "前端監(jiān)控系統(tǒng)", //標題
"url": "http://localhost:8080/", //url
"timestamp": "1590817024490", //timestamp
"userAgent": "Chrome", //瀏覽器版本
"kind": "stability", //大類
"type": "xhr", //小類
"eventType": "load", //事件類型
"pathname": "/success", //路徑
"status": "200-OK", //狀態(tài)碼
"duration": "7", //持續(xù)時間
"response": "{\"id\":1}", //響應內(nèi)容
"params": "" //參數(shù)
}
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590817025617",
"userAgent": "Chrome",
"kind": "stability",
"type": "xhr",
"eventType": "load",
"pathname": "/error",
"status": "500-Internal Server Error",
"duration": "7",
"response": "",
"params": "name=zhufeng"
}
4.3.2 實現(xiàn)
使用webpack devServer模擬請求
重寫xhr的open、send方法
監(jiān)聽load、error、abort事件
import tracker from "../util/tracker";
export function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (
method,
url,
async,
username,
password
) {
// 上報的接口不用處理
if (!url.match(/logstores/) && !url.match(/sockjs/)) {
this.logData = {
method,
url,
async,
username,
password,
};
}
return oldOpen.apply(this, arguments);
};
let oldSend = XMLHttpRequest.prototype.send;
let start;
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
start = Date.now();
let handler = (type) => (event) => {
let duration = Date.now() - start;
let status = this.status;
let statusText = this.statusText;
tracker.send({
//未捕獲的promise錯誤
kind: "stability", //穩(wěn)定性指標
type: "xhr", //xhr
eventType: type, //load error abort
pathname: this.logData.url, //接口的url地址
status: status + "-" + statusText,
duration: "" + duration, //接口耗時
response: this.response ? JSON.stringify(this.response) : "",
params: body || "",
});
};
this.addEventListener("load", handler("load"), false);
this.addEventListener("error", handler("error"), false);
this.addEventListener("abort", handler("abort"), false);
}
oldSend.apply(this, arguments);
};
}
4.4 白屏
白屏就是頁面上什么都沒有
4.4.1 數(shù)據(jù)設(shè)計
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大類
"type": "blank", //小類
"emptyPoints": "0", //空白點
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //視口
"selector": "HTML BODY #container" //選擇器
}
4.4.2 實現(xiàn)
elementsFromPoint方法可以獲取到當前視口內(nèi)指定坐標處,由里到外排列的所有元素
根據(jù) elementsFromPoint api,獲取屏幕水平中線和豎直中線所在的元素
import tracker from "../util/tracker";
import onload from "../util/onload";
function getSelector(element) {
var selector;
if (element.id) {
selector = `#${element.id}`;
} else if (element.className && typeof element.className === "string") {
selector =
"." +
element.className
.split(" ")
.filter(function (item) {
return !!item;
})
.join(".");
} else {
selector = element.nodeName.toLowerCase();
}
return selector;
}
export function blankScreen() {
const wrapperSelectors = ["body", "html", "#container", ".content"];
let emptyPoints = 0;
function isWrapper(element) {
let selector = getSelector(element);
if (wrapperSelectors.indexOf(selector) >= 0) {
emptyPoints++;
}
}
onload(function () {
let xElements, yElements;
debugger;
for (let i = 1; i <= 9; i++) {
xElements = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
yElements = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
}
if (emptyPoints >= 0) {
let centerElements = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
);
tracker.send({
kind: "stability",
type: "blank",
emptyPoints: "" + emptyPoints,
screen: window.screen.width + "x" + window.screen.height,
viewPoint: window.innerWidth + "x" + window.innerHeight,
selector: getSelector(centerElements[0]),
});
}
});
}
//screen.width 屏幕的寬度 screen.height 屏幕的高度
//window.innerWidth 去除工具條與滾動條的窗口寬度 window.innerHeight 去除工具條與滾動條的窗口高度
4.5 加載時間
PerformanceTiming
DOMContentLoaded
FMP
4.5.1 階段含義

| 字段 | 含義 |
|---|---|
| navigationStart | 初始化頁面,在同一個瀏覽器上下文中前一個頁面unload的時間戳,如果沒有前一個頁面的unload,則與fetchStart值相等 |
| redirectStart | 第一個HTTP重定向發(fā)生的時間,有跳轉(zhuǎn)且是同域的重定向,否則為0 |
| redirectEnd | 最后一個重定向完成時的時間,否則為0 |
| fetchStart | 瀏覽器準備好使用http請求獲取文檔的時間,這發(fā)生在檢查緩存之前 |
| domainLookupStart | DNS域名開始查詢的時間,如果有本地的緩存或keep-alive則時間為0 |
| domainLookupEnd | DNS域名結(jié)束查詢的時間 |
| connectStart | TCP開始建立連接的時間,如果是持久連接,則與fetchStart值相等 |
| secureConnectionStart | https 連接開始的時間,如果不是安全連接則為0 |
| connectEnd | TCP完成握手的時間,如果是持久連接則與fetchStart值相等 |
| requestStart | HTTP請求讀取真實文檔開始的時間,包括從本地緩存讀取 |
| requestEnd | HTTP請求讀取真實文檔結(jié)束的時間,包括從本地緩存讀取 |
| responseStart | 返回瀏覽器從服務(wù)器收到(或從本地緩存讀?。┑谝粋€字節(jié)時的Unix毫秒時間戳 |
| responseEnd | 返回瀏覽器從服務(wù)器收到(或從本地緩存讀取,或從本地資源讀取)最后一個字節(jié)時的Unix毫秒時間戳 |
| unloadEventStart | 前一個頁面的unload的時間戳 如果沒有則為0 |
| unloadEventEnd | 與unloadEventStart相對應,返回的是unload函數(shù)執(zhí)行完成的時間戳 |
| domLoading | 返回當前網(wǎng)頁DOM結(jié)構(gòu)開始解析時的時間戳,此時document.readyState變成loading,并將拋出readyStateChange事件 |
| domInteractive | 返回當前網(wǎng)頁DOM結(jié)構(gòu)結(jié)束解析、開始加載內(nèi)嵌資源時時間戳,document.readyState?變成interactive,并將拋出readyStateChange事件(注意只是DOM樹解析完成,這時候并沒有開始加載網(wǎng)頁內(nèi)的資源) |
| domContentLoadedEventStart | 網(wǎng)頁domContentLoaded事件發(fā)生的時間 |
| domContentLoadedEventEnd | 網(wǎng)頁domContentLoaded事件腳本執(zhí)行完畢的時間,domReady的時間 |
| domComplete | DOM樹解析完成,且資源也準備就緒的時間,document.readyState變成complete.并將拋出readystatechange事件 |
| loadEventStart | load 事件發(fā)送給文檔,也即load回調(diào)函數(shù)開始執(zhí)行的時間 |
| loadEventEnd | load回調(diào)函數(shù)執(zhí)行完成的時間 |
4.5.2 階段計算
| 字段 | 描述 | 計算方式 | 意義 |
|---|---|---|---|
| unload | 前一個頁面卸載耗時 | unloadEventEnd – unloadEventStart | - |
| redirect | 重定向耗時 | redirectEnd – redirectStart | 重定向的時間 |
| appCache | 緩存耗時 | domainLookupStart – fetchStart | 讀取緩存的時間 |
| dns | DNS 解析耗時 | domainLookupEnd – domainLookupStart | 可觀察域名解析服務(wù)是否正常 |
| tcp | TCP 連接耗時 | connectEnd – connectStart | 建立連接的耗時 |
| ssl | SSL 安全連接耗時 | connectEnd – secureConnectionStart | 反映數(shù)據(jù)安全連接建立耗時 |
| ttfb | Time to First Byte(TTFB)網(wǎng)絡(luò)請求耗時 | responseStart – requestStart | TTFB是發(fā)出頁面請求到接收到應答數(shù)據(jù)第一個字節(jié)所花費的毫秒數(shù) |
| response | 響應數(shù)據(jù)傳輸耗時 | responseEnd – responseStart | 觀察網(wǎng)絡(luò)是否正常 |
| dom | DOM解析耗時 | domInteractive – responseEnd | 觀察DOM結(jié)構(gòu)是否合理,是否有JS阻塞頁面解析 |
| dcl | DOMContentLoaded 事件耗時 | domContentLoadedEventEnd – domContentLoadedEventStart | 當 HTML 文檔被完全加載和解析完成之后,DOMContentLoaded 事件被觸發(fā),無需等待樣式表、圖像和子框架的完成加載 |
| resources | 資源加載耗時 | domComplete – domContentLoadedEventEnd | 可觀察文檔流是否過大 |
| domReady | DOM階段渲染耗時 | domContentLoadedEventEnd – fetchStart | DOM樹和頁面資源加載完成時間,會觸發(fā)domContentLoaded事件 |
| 首次渲染耗時 | 首次渲染耗時 | responseEnd-fetchStart | 加載文檔到看到第一幀非空圖像的時間,也叫白屏時間 |
| 首次可交互時間 | 首次可交互時間 | domInteractive-fetchStart | DOM樹解析完成時間,此時document.readyState為interactive |
| 首包時間耗時 | 首包時間 | responseStart-domainLookupStart | DNS解析到響應返回給瀏覽器第一個字節(jié)的時間 |
| 頁面完全加載時間 | 頁面完全加載時間 | loadEventStart - fetchStart | - |
| onLoad | onLoad事件耗時 | loadEventEnd – loadEventStart |

4.5.3 數(shù)據(jù)結(jié)構(gòu)
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590828364183",
"userAgent": "chrome",
"kind": "experience",
"type": "timing",
"connectTime": "0",
"ttfbTime": "1",
"responseTime": "1",
"parseDOMTime": "80",
"domContentLoadedTime": "0",
"timeToInteractive": "88",
"loadTime": "89"
}
4.5.4 實現(xiàn)
import onload from "../util/onload";
import tracker from "../util/tracker";
import formatTime from "../util/formatTime";
import getLastEvent from "../util/getLastEvent";
import getSelector from "../util/getSelector";
export function timing() {
onload(function () {
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = performance.timing;
tracker.send({
kind: "experience",
type: "timing",
connectTime: connectEnd - connectStart, //TCP連接耗時
ttfbTime: responseStart - requestStart, //ttfb
responseTime: responseEnd - responseStart, //Response響應耗時
parseDOMTime: loadEventStart - domLoading, //DOM解析渲染耗時
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, //DOMContentLoaded事件回調(diào)耗時
timeToInteractive: domInteractive - fetchStart, //首次可交互時間
loadTime: loadEventStart - fetchStart, //完整的加載時間
});
}, 3000);
});
}
4.6 性能指標
PerformanceObserver.observe方法用于觀察傳入的參數(shù)中指定的性能條目類型的集合。當記錄一個指定類型的性能條目時,性能監(jiān)測對象的回調(diào)函數(shù)將會被調(diào)用
entryType
paint-timing
event-timing
LCP
FMP
time-to-interactive
| 字段 | 描述 | 備注 | 計算方式 |
|---|---|---|---|
| FP | First Paint(首次繪制) | 包括了任何用戶自定義的背景繪制,它是首先將像素繪制到屏幕的時刻 | |
| FCP | First Content Paint(首次內(nèi)容繪制) | 是瀏覽器將第一個 DOM 渲染到屏幕的時間,可能是文本、圖像、SVG等,這其實就是白屏時間 | |
| FMP | First Meaningful Paint(首次有意義繪制) | 頁面有意義的內(nèi)容渲染的時間 | |
| LCP | (Largest Contentful Paint)(最大內(nèi)容渲染) | 代表在viewport中最大的頁面元素加載的時間 | |
| DCL | (DomContentLoaded)(DOM加載完成) | 當 HTML 文檔被完全加載和解析完成之后, DOMContentLoaded?事件被觸發(fā),無需等待樣式表、圖像和子框架的完成加載 | |
| L | (onLoad) | 當依賴的資源全部加載完畢之后才會觸發(fā) | |
| TTI | (Time to Interactive) 可交互時間 | 用于標記應用已進行視覺渲染并能可靠響應用戶輸入的時間點 | |
| FID | First Input Delay(首次輸入延遲) | 用戶首次和頁面交互(單擊鏈接,點擊按鈕等)到頁面響應交互的時間 |


4.6.1 數(shù)據(jù)結(jié)構(gòu)設(shè)計
1. paint
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}
2. firstInputDelay
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590828477284",
"userAgent": "chrome",
"kind": "experience",
"type": "firstInputDelay",
"inputDelay": "3",
"duration": "8",
"startTime": "4812.344999983907",
"selector": "HTML BODY #container .content H1"
}
4.6.2 實現(xiàn)
關(guān)鍵時間節(jié)點通過window.performance.timing獲取

import tracker from "../utils/tracker";
import onload from "../utils/onload";
import getLastEvent from "../utils/getLastEvent";
import getSelector from "../utils/getSelector";
export function timing() {
let FMP, LCP;
// 增加一個性能條目的觀察者
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
FMP = perfEntries[0];
observer.disconnect(); // 不再觀察了
}).observe({ entryTypes: ["element"] }); // 觀察頁面中有意義的元素
// 增加一個性能條目的觀察者
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
const lastEntry = perfEntries[perfEntries.length - 1];
LCP = lastEntry;
observer.disconnect(); // 不再觀察了
}).observe({ entryTypes: ["largest-contentful-paint"] }); // 觀察頁面中最大的元素
// 增加一個性能條目的觀察者
new PerformanceObserver((entryList, observer) => {
const lastEvent = getLastEvent();
const firstInput = entryList.getEntries()[0];
if (firstInput) {
// 開始處理的時間 - 開始點擊的時間,差值就是處理的延遲
let inputDelay = firstInput.processingStart - firstInput.startTime;
let duration = firstInput.duration; // 處理的耗時
if (inputDelay > 0 || duration > 0) {
tracker.send({
kind: "experience", // 用戶體驗指標
type: "firstInputDelay", // 首次輸入延遲
inputDelay: inputDelay ? formatTime(inputDelay) : 0, // 延遲的時間
duration: duration ? formatTime(duration) : 0,
startTime: firstInput.startTime, // 開始處理的時間
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
}
}
observer.disconnect(); // 不再觀察了
}).observe({ type: "first-input", buffered: true }); // 第一次交互
// 剛開始頁面內(nèi)容為空,等頁面渲染完成,再去做判斷
onload(function () {
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = window.performance.timing;
// 發(fā)送時間指標
tracker.send({
kind: "experience", // 用戶體驗指標
type: "timing", // 統(tǒng)計每個階段的時間
connectTime: connectEnd - connectStart, // TCP連接耗時
ttfbTime: responseStart - requestStart, // 首字節(jié)到達時間
responseTime: responseEnd - responseStart, // response響應耗時
parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的時間
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回調(diào)耗時
timeToInteractive: domInteractive - fetchStart, // 首次可交互時間
loadTime: loadEventStart - fetchStart, // 完整的加載時間
});
// 發(fā)送性能指標
let FP = performance.getEntriesByName("first-paint")[0];
let FCP = performance.getEntriesByName("first-contentful-paint")[0];
console.log("FP", FP);
console.log("FCP", FCP);
console.log("FMP", FMP);
console.log("LCP", LCP);
tracker.send({
kind: "experience",
type: "paint",
firstPaint: FP ? formatTime(FP.startTime) : 0,
firstContentPaint: FCP ? formatTime(FCP.startTime) : 0,
firstMeaningfulPaint: FMP ? formatTime(FMP.startTime) : 0,
largestContentfulPaint: LCP
? formatTime(LCP.renderTime || LCP.loadTime)
: 0,
});
}, 3000);
});
}
4.7 卡頓
響應用戶交互的響應時間如果大于100ms,用戶就會感覺卡頓
4.7.1 數(shù)據(jù)設(shè)計 longTask
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590828656781",
"userAgent": "chrome",
"kind": "experience",
"type": "longTask",
"eventType": "mouseover",
"startTime": "9331",
"duration": "200",
"selector": "HTML BODY #container .content"
}
4.7.2 實現(xiàn)
new PerformanceObserver
entry.duration > 100 判斷大于100ms,即可認定為長任務(wù)
使用 requestIdleCallback上報數(shù)據(jù)
import tracker from "../util/tracker";
import formatTime from "../util/formatTime";
import getLastEvent from "../util/getLastEvent";
import getSelector from "../util/getSelector";
export function longTask() {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 100) {
let lastEvent = getLastEvent();
requestIdleCallback(() => {
tracker.send({
kind: "experience",
type: "longTask",
eventType: lastEvent.type,
startTime: formatTime(entry.startTime), // 開始時間
duration: formatTime(entry.duration), // 持續(xù)時間
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
});
}
});
}).observe({ entryTypes: ["longtask"] });
}
4.8 PV、UV、用戶停留時間
4.8.1 數(shù)據(jù)設(shè)計 business
{
"title": "前端監(jiān)控系統(tǒng)",
"url": "http://localhost:8080/",
"timestamp": "1590829304423",
"userAgent": "chrome",
"kind": "business",
"type": "pv",
"effectiveType": "4g",
"rtt": "50",
"screen": "2049x1152"
}
4.8.2 PV、UV、用戶停留時間
PV(page view) 是頁面瀏覽量,UV(Unique visitor)用戶訪問量。PV 只要訪問一次頁面就算一次,UV 同一天內(nèi)多次訪問只算一次。
對于前端來說,只要每次進入頁面上報一次 PV 就行,UV 的統(tǒng)計放在服務(wù)端來做,主要是分析上報的數(shù)據(jù)來統(tǒng)計得出 UV。
import tracker from "../util/tracker";
export function pv() {
tracker.send({
kind: "business",
type: "pv",
startTime: performance.now(),
pageURL: getPageURL(),
referrer: document.referrer,
uuid: getUUID(),
});
let startTime = Date.now();
window.addEventListener(
"beforeunload",
() => {
let stayTime = Date.now() - startTime;
tracker.send({
kind: "business",
type: "stayTime",
stayTime,
pageURL: getPageURL(),
uuid: getUUID(),
});
},
false
);
}
擴展問題
性能監(jiān)控指標
前端怎么做性能監(jiān)控
線上錯誤監(jiān)控怎么做
導致內(nèi)存泄漏的方法,怎么監(jiān)控內(nèi)存泄漏
Node 怎么做性能監(jiān)控
1.?性能監(jiān)控指標
| 指標 | 名稱 | 解釋 |
|---|---|---|
| FP | First-Paint 首次渲染 | 表示瀏覽器從開始請求網(wǎng)站到屏幕渲染第一個像素點的時間 |
| FCP | First-Contentful-Paint 首次內(nèi)容渲染 | 表示瀏覽器渲染出第一個內(nèi)容的時間,這個內(nèi)容可以是文本、圖片或SVG元素等等,不包括iframe和白色背景的canvas元素 |
| SI | Speed Index 速度指數(shù) | 表明了網(wǎng)頁內(nèi)容的可見填充速度 |
| LCP | Largest Contentful Paint 最大內(nèi)容繪制 | 標記了渲染出最大文本或圖片的時間 |
| TTI | Time to Interactive 可交互時間 | 頁面從開始加載到主要子資源完成渲染,并能夠快速、可靠的響應用戶輸入所需的時間 |
| TBT | Total Blocking Time 總阻塞時間 | 測量 FCP 與 TTI 之間的總時間,這期間,主線程被阻塞的時間過長,無法作出輸入響應 |
| FID | First Input Delay 首次輸入延遲 | 測量加載響應度的一個以用戶為中心的重要指標 |
| CLS | Cumulative Layout Shift 累積布局偏移 | 測量的是整個頁面生命周期內(nèi)發(fā)生的所有意外布局偏移中最大一連串的布局偏移分數(shù) |
| DCL | DOMContentLoaded | 當初始的?HTML 文檔被完全加載和解析完成之后,DOMContentLoaded 事件被觸發(fā),而無需等待樣式表、圖像和子框架的完成加載 |
| L | Load | 檢測一個完全加載的頁面,頁面的html、css、js、圖片等資源都已經(jīng)加載完之后才會觸發(fā) load 事件 |
2. 前端怎么做性能監(jiān)控
FP、FCP、LCP、CLS、FID、FMP 可通過 PerformanceObserver獲取
TCP連接耗時、首字節(jié)到達時間、response響應耗時、DOM解析渲染的時間、TTI、DCL、L等可通過performance.timing獲取
長任務(wù)監(jiān)聽,PerformanceObserver 監(jiān)聽 longTask
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = window.performance.timing;
const obj = {
kind: "experience", // 用戶體驗指標
type: "timing", // 統(tǒng)計每個階段的時間
dnsTime: domainLookupEnd - domainLookupStart, // DNS查詢時間
connectTime: connectEnd - connectStart, // TCP連接耗時
ttfbTime: responseStart - requestStart, // 首字節(jié)到達時間
responseTime: responseEnd - responseStart, // response響應耗時
parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的時間
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回調(diào)耗時
timeToInteractive: domInteractive - fetchStart, // 首次可交互時間
loadTime: loadEventStart - fetchStart, // 完整的加載時間
}

3. 線上錯誤監(jiān)控怎么做
資源加載錯誤 window.addEventListener('error') 判斷e.target.src || href
js運行時錯誤 window.addEventListener('error')
promise異常 window.addEventListener('unhandledrejection')
接口異常 重寫xhr 的 open send方法,監(jiān)控 load、error、abort,進行上報
4. 導致內(nèi)存泄漏的方法,怎么監(jiān)控內(nèi)存泄漏
全局變量
被遺忘的定時器
脫離Dom的引用
閉包
監(jiān)控內(nèi)存泄漏
window.performance.memory
開發(fā)階段
瀏覽器的 Performance
移動端可使用 PerformanceDog

5. Node 怎么做性能監(jiān)控
日志監(jiān)控 可以通過監(jiān)控異常日志的變動,將新增的異常類型和數(shù)量反映出來 監(jiān)控日志可以實現(xiàn)pv和uv的監(jiān)控,通過pv/uv的監(jiān)控,可以知道使用者們的使用習慣,預知訪問高峰
響應時間 響應時間也是一個需要監(jiān)控的點。一旦系統(tǒng)的某個子系統(tǒng)出現(xiàn)異?;蛘咝阅芷款i將會導致系統(tǒng)的響應時間變長。響應時間可以在nginx一類的反向代理上監(jiān)控,也可以通過應用自己產(chǎn)生訪問日志來監(jiān)控
進程監(jiān)控 監(jiān)控日志和響應時間都能較好地監(jiān)控到系統(tǒng)的狀態(tài),但是它們的前提是系統(tǒng)是運行狀態(tài)的,所以監(jiān)控進程是比前兩者更為緊要的任務(wù)。監(jiān)控進程一般是檢查操作系統(tǒng)中運行的應用進程數(shù),比如對于采用多進程架構(gòu)的web應用,就需要檢查工作進程的數(shù),如果低于低估值,就應當發(fā)出警報
磁盤監(jiān)控 磁盤監(jiān)控主要是監(jiān)控磁盤的用量。由于寫日志頻繁的緣故,磁盤空間漸漸被用光。一旦磁盤不夠用將會引發(fā)系統(tǒng)的各種問題,給磁盤的使用量設(shè)置一個上限,一旦磁盤用量超過警戒值,服務(wù)器的管理者應該整理日志或者清理磁盤
內(nèi)存監(jiān)控 對于node而言,一旦出現(xiàn)內(nèi)存泄漏,不是那么容易排查的。監(jiān)控服務(wù)器的內(nèi)存使用情況。如果內(nèi)存只升不降,那么鐵定存在內(nèi)存泄漏問題。符合正常的內(nèi)存使用應該是有升有降,在訪問量大的時候上升,在訪問量回落的時候,占用量也隨之回落。監(jiān)控內(nèi)存異常時間也是防止系統(tǒng)出現(xiàn)異常的好方法。如果突然出現(xiàn)內(nèi)存異常,也能夠追蹤到近期的哪些代碼改動導致的問題
cpu占用監(jiān)控 服務(wù)器的cpu占用監(jiān)控也是必不可少的項,cpu的使用分為用戶態(tài)、內(nèi)核態(tài)、IOWait等。如果用戶態(tài)cpu使用率較高,說明服務(wù)器上的應用需要大量的cpu開銷;如果內(nèi)核態(tài)cpu使用率較高,說明服務(wù)器需要花費大量時間進行進程調(diào)度或者系統(tǒng)調(diào)用;IOWait使用率反映的是cpu等待磁盤I/O操作;cpu的使用率中,用戶態(tài)小于70%,內(nèi)核態(tài)小于35%且整體小于70%,處于正常范圍。監(jiān)控cpu占用情況,可以幫助分析應用程序在實際業(yè)務(wù)中的狀況。合理設(shè)置監(jiān)控閾值能夠很好地預警
cpu load監(jiān)控 cpu load又稱cpu平均負載。它用來描述操作系統(tǒng)當前的繁忙程度,又簡單地理解為cpu在單位時間內(nèi)正在使用和等待使用cpu的平均任務(wù)數(shù)。它有3個指標,即1分鐘的平均負載、5分鐘的平均負載,15分鐘的平均負載。cpu load過高說明進程數(shù)量過多,這在node中可能體現(xiàn)在用于進程模塊反復啟動新的進程。監(jiān)控該值可以防止意外發(fā)生
I/O負載 I/O負載指的主要是磁盤I/O。反應的是磁盤上的讀寫情況,對于node編寫的應用,主要是面向網(wǎng)絡(luò)業(yè)務(wù),是不太可能出現(xiàn)I/O負載過高的情況,大多數(shù)的I/O壓力來自于數(shù)據(jù)庫。不管node進程是否與數(shù)據(jù)庫或其他I/O密集的應用共同處理相同的服務(wù)器,我們都應該監(jiān)控該值防止意外情況
網(wǎng)絡(luò)監(jiān)控 雖然網(wǎng)絡(luò)流量監(jiān)控的優(yōu)先級沒有上述項目那么高,但還是需要對流量進行監(jiān)控并設(shè)置流量上限值。即便應用突然受到用戶的青睞,流量暴漲的時候也可以通過數(shù)值感知到網(wǎng)站的宣傳是否有效。一旦流量超過警戒值,開發(fā)者就應當找出流量增長的原因。對于正常增長,應當評估是否該增加硬件設(shè)備來為更多用戶提供服務(wù)。網(wǎng)絡(luò)流量監(jiān)控的兩個主要指標是流入流量和流出流量
應用狀態(tài)監(jiān)控 除了這些硬性需要檢測的指標之外,應用還應該提供一種機制來反饋其自身的狀態(tài)信息,外部監(jiān)控將會持續(xù)性地調(diào)用應用地反饋接口來檢查它地健康狀態(tài)。
dns監(jiān)控 dns是網(wǎng)絡(luò)應用的基礎(chǔ),在實際的對外服務(wù)產(chǎn)品中,多數(shù)都對域名有依賴。dns故障導致產(chǎn)品出現(xiàn)大面積影響的事件并不少見。由于dns服務(wù)通常是穩(wěn)定的,容易讓人忽略,但是一旦出現(xiàn)故障,就可能是史無前例的故障。對于產(chǎn)品的穩(wěn)定性,域名dns狀態(tài)也需要加入監(jiān)控。
歡迎加我微信,拉你進技術(shù)群,長期交流學習...
歡迎關(guān)注「前端Q」,認真學前端,做個專業(yè)的技術(shù)人...
往期推薦
秒啊!答好這5個問題,就入門Docker了 (字節(jié)/華為/美團)前端面經(jīng)記錄冷冷清清的金三銀四 構(gòu)建大型高質(zhì)量的前端工程完全指南
最后
點個在看支持我吧





