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”, //稳定性指标
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 数据设计
{
“title”: “前端监控系统”,
“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 实现
• elementsFromPoint[1]方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
• 根据 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[2]
  • DOMContentLoaded[3]
  • FMP[4]
4.5.1 阶段含义

前端稳定性监控规范_前端

image.png

| 字段 | 含义 |

| — | — |

| navigationStart | 初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面的unload,则与fetchStart值相等 |

| redirectStart | 第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0 |

| redirectEnd | 最后一个重定向完成时的时间,否则为0 |

| fetchStart | 浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前 |

| domainLookupStart | DNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0 |

| domainLookupEnd | DNS域名结束查询的时间 |

| connectStart | TCP开始建立连接的时间,如果是持久连接,则与fetchStart值相等 |

| secureConnectionStart | https 连接开始的时间,如果不是安全连接则为0 |

| connectEnd | TCP完成握手的时间,如果是持久连接则与fetchStart值相等 |

| requestStart | HTTP请求读取真实文档开始的时间,包括从本地缓存读取 |

| requestEnd | HTTP请求读取真实文档结束的时间,包括从本地缓存读取 |

| responseStart | 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳 |

| responseEnd | 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的Unix毫秒时间戳 |

| unloadEventStart | 前一个页面的unload的时间戳 如果没有则为0 |

| unloadEventEnd | 与unloadEventStart相对应,返回的是unload函数执行完成的时间戳 |

| domLoading | 返回当前网页DOM结构开始解析时的时间戳,此时document.readyState变成loading,并将抛出readyStateChange事件 |

| domInteractive | 返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive,并将抛出readyStateChange事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源) |

| domContentLoadedEventStart | 网页domContentLoaded事件发生的时间 |

| domContentLoadedEventEnd | 网页domContentLoaded事件脚本执行完毕的时间,domReady的时间 |

| domComplete | DOM树解析完成,且资源也准备就绪的时间,document.readyState变成complete.并将抛出readystatechange事件 |

| loadEventStart | load 事件发送给文档,也即load回调函数开始执行的时间 |

| loadEventEnd | load回调函数执行完成的时间 |

4.5.2 阶段计算

| 字段 | 描述 | 计算方式 | 意义 |

| — | — | — | — |

| unload | 前一个页面卸载耗时 | unloadEventEnd – unloadEventStart | - |

| redirect | 重定向耗时 | redirectEnd – redirectStart | 重定向的时间 |

| appCache | 缓存耗时 | domainLookupStart – fetchStart | 读取缓存的时间 |

| dns | DNS 解析耗时 | domainLookupEnd – domainLookupStart | 可观察域名解析服务是否正常 |

| tcp | TCP 连接耗时 | connectEnd – connectStart | 建立连接的耗时 |

| ssl | SSL 安全连接耗时 | connectEnd – secureConnectionStart | 反映数据安全连接建立耗时 |

| ttfb | Time to First Byte(TTFB)网络请求耗时 | responseStart – requestStart | TTFB是发出页面请求到接收到应答数据第一个字节所花费的毫秒数 |

| response | 响应数据传输耗时 | responseEnd – responseStart | 观察网络是否正常 |

| dom | DOM解析耗时 | domInteractive – responseEnd | 观察DOM结构是否合理,是否有JS阻塞页面解析 |

| dcl | DOMContentLoaded 事件耗时 | domContentLoadedEventEnd – domContentLoadedEventStart | 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 |

| resources | 资源加载耗时 | domComplete – domContentLoadedEventEnd | 可观察文档流是否过大 |

| domReady | DOM阶段渲染耗时 | domContentLoadedEventEnd – fetchStart | DOM树和页面资源加载完成时间,会触发domContentLoaded事件 |

| 首次渲染耗时 | 首次渲染耗时 | responseEnd-fetchStart | 加载文档到看到第一帧非空图像的时间,也叫白屏时间 |

| 首次可交互时间 | 首次可交互时间 | domInteractive-fetchStart | DOM树解析完成时间,此时document.readyState为interactive |

| 首包时间耗时 | 首包时间 | responseStart-domainLookupStart | DNS解析到响应返回给浏览器第一个字节的时间 |

| 页面完全加载时间 | 页面完全加载时间 | loadEventStart - fetchStart | - |

| onLoad | onLoad事件耗时 | loadEventEnd – loadEventStart |

|

前端稳定性监控规范_前端稳定性监控规范_02

image.png

4.5.3 数据结构
{
“title”: “前端监控系统”,
“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 实现
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事件回调耗时
timeToInteractive: domInteractive - fetchStart, //首次可交互时间
loadTime: loadEventStart - fetchStart, //完整的加载时间
});
}, 3000);
});
}
4.6 性能指标
• PerformanceObserver.observe[5]方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
• entryType[6]
• paint-timing[7]
• event-timing[8]
• LCP[9]
• FMP[10]
• time-to-interactive[11]
| 字段 | 描述 | 备注 | 计算方式 |
| — | — | — | — |
| FP | First Paint(首次绘制) | 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻 |
|
| FCP | First Content Paint(首次内容绘制) | 是浏览器将第一个 DOM 渲染到屏幕的时间,可能是文本、图像、SVG等,这其实就是白屏时间 |
|
| FMP | First Meaningful Paint(首次有意义绘制) | 页面有意义的内容渲染的时间 |
|
| LCP | (Largest Contentful Paint)(最大内容渲染) | 代表在viewport中最大的页面元素加载的时间 |
|
| DCL | (DomContentLoaded)(DOM加载完成) | 当 HTML 文档被完全加载和解析完成之后, DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 |
|
| L | (onLoad) | 当依赖的资源全部加载完毕之后才会触发 |
|
| TTI | (Time to Interactive) 可交互时间 | 用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点 |
|
| FID | First Input Delay(首次输入延迟) | 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 |
|

前端稳定性监控规范_加载_03

image.png

前端稳定性监控规范_重定向_04

image.png

4.6.1 数据结构设计
1. paint

{

“title”: “前端监控系统”,

“url”: “http://localhost:8080/”,

“timestamp”: “1590828364186”,

“userAgent”: “chrome”,

“kind”: “experience”,

“type”: “paint”,

“firstPaint”: “102”,

“firstContentPaint”: “2130”,

“firstMeaningfulPaint”: “2130”,

“largestContentfulPaint”: “2130”

}

2. firstInputDelay

{

“title”: “前端监控系统”,

“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 实现

关键时间节点通过window.performance.timing获取

前端稳定性监控规范_重定向_05

image.png

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 }); // 第一次交互

// 刚开始页面内容为空,等页面渲染完成,再去做判断

onload(function () {

setTimeout(() => {

const {

fetchStart,

connectStart,

connectEnd,

requestStart,

responseStart,

responseEnd,

domLoading,

domInteractive,

domContentLoadedEventStart,

domContentLoadedEventEnd,

loadEventStart,

} = window.performance.timing;

// 发送时间指标

tracker.send({

kind: “experience”, // 用户体验指标

type: “timing”, // 统计每个阶段的时间

connectTime: connectEnd - connectStart, // TCP连接耗时

ttfbTime: responseStart - requestStart, // 首字节到达时间

responseTime: responseEnd - responseStart, // response响应耗时

parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的时间

domContentLoadedTime:

domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回调耗时

timeToInteractive: domInteractive - fetchStart, // 首次可交互时间

loadTime: loadEventStart - fetchStart, // 完整的加载时间

});

// 发送性能指标

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 数据设计 longTask

{

“title”: “前端监控系统”,

“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 实现
  • new PerformanceObserver
  • entry.duration > 100 判断大于100ms,即可认定为长任务
  • 使用 requestIdleCallback上报数据

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), // 持续时间

selector: lastEvent

? getSelector(lastEvent.path || lastEvent.target)

: “”,

});

});

}

});

}).observe({ entryTypes: [“longtask”] });

}

4.8 PV、UV、用户停留时间

4.8.1 数据设计 business

{

“title”: “前端监控系统”,

“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 同一天内多次访问只算一次。

对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 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

);

}


扩展问题

  1. 性能监控指标
  2. 前端怎么做性能监控
  3. 线上错误监控怎么做
  4. 导致内存泄漏的方法,怎么监控内存泄漏
  5. Node 怎么做性能监控
1. 性能监控指标
| 指标 | 名称 | 解释 |
| — | — | — |
| FP | First-Paint 首次渲染 | 表示浏览器从开始请求网站到屏幕渲染第一个像素点的时间 |
| FCP | First-Contentful-Paint 首次内容渲染 | 表示浏览器渲染出第一个内容的时间,这个内容可以是文本、图片或SVG元素等等,不包括iframe和白色背景的canvas元素 |
| SI | Speed Index 速度指数 | 表明了网页内容的可见填充速度 |
| LCP | Largest Contentful Paint 最大内容绘制 | 标记了渲染出最大文本或图片的时间 |
| TTI | Time to Interactive 可交互时间 | 页面从开始加载到主要子资源完成渲染,并能够快速、可靠的响应用户输入所需的时间 |
| TBT | Total Blocking Time 总阻塞时间 | 测量 FCP 与 TTI 之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应 |
| FID | First Input Delay 首次输入延迟 | 测量加载响应度的一个以用户为中心的重要指标 |
| CLS | Cumulative Layout Shift 累积布局偏移 | 测量的是整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数 |
| DCL | DOMContentLoaded | 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载 |
| L | Load | 检测一个完全加载的页面,页面的html、css、js、图片等资源都已经加载完之后才会触发 load 事件 |
2. 前端怎么做性能监控
• FP、FCP、LCP、CLS、FID、FMP 可通过 PerformanceObserver获取
• TCP连接耗时、首字节到达时间、response响应耗时、DOM解析渲染的时间、TTI、DCL、L等可通过performance.timing获取
• 长任务监听,PerformanceObserver 监听 longTask
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = window.performance.timing;
const obj = {
kind: “experience”, // 用户体验指标
type: “timing”, // 统计每个阶段的时间
dnsTime: domainLookupEnd - domainLookupStart, // DNS查询时间
connectTime: connectEnd - connectStart, // TCP连接耗时
ttfbTime: responseStart - requestStart, // 首字节到达时间
responseTime: responseEnd - responseStart, // response响应耗时
parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的时间
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
loadTime: loadEventStart - fetchStart, // 完整的加载时间
}

前端稳定性监控规范_加载_06

image.png

3. 线上错误监控怎么做
  • 资源加载错误 window.addEventListener(‘error’) 判断e.target.src || href
  • js运行时错误 window.addEventListener(‘error’)
  • promise异常 window.addEventListener(‘unhandledrejection’)
  • 接口异常 重写xhr 的 open send方法,监控 load、error、abort,进行上报
4. 导致内存泄漏的方法,怎么监控内存泄漏
  • 全局变量
  • 被遗忘的定时器


tart, // 完整的加载时间

}

前端稳定性监控规范_加载_06

image.png

3. 线上错误监控怎么做
  • 资源加载错误 window.addEventListener(‘error’) 判断e.target.src || href
  • js运行时错误 window.addEventListener(‘error’)
  • promise异常 window.addEventListener(‘unhandledrejection’)
  • 接口异常 重写xhr 的 open send方法,监控 load、error、abort,进行上报
4. 导致内存泄漏的方法,怎么监控内存泄漏
  • 全局变量
  • 被遗忘的定时器