前言
- 上一篇主要说了脚本错误捕获,资源加载错误捕获,和promise捕获。本篇记录下接口异常捕获、白屏监测、加载时间、性能指标,卡顿指标,pv。
接口异常捕获
原理
- 重写xmlhttprequest的open和send方法,使其在上报前进行标记和计时,监听其load、error、about事件,当发生相应的事件进行上报。
- 注意!open事件中进行标记xhr需要排除上报url,否则会发生无限循环。
- axios同理,可以进行自行封装。排除上报地址后,对应的计时监测和上报。
export default function injectXHR() {
let xhr = window.XMLHttpRequest;
let open = xhr.prototype.open;
xhr.prototype.open = function (method, url, async, user, password) {
if (!url.match(/logstores/) && !url.match(/sockjs/)) {
this.logData = { method, url, async, user, password };
}
return open.apply(this, arguments);
};
let send = xhr.prototype.send;
xhr.prototype.send = function (body) {
if (this.logData) {
let startTime = Date.now();
let handler = (type) => (event) => {
let duration = Date.now() - startTime;
let status = this.status;
let statusText = this.statusText;
tracker.send({
kind: "stability",
type: "xhr",
eventType: type,
pathname: this.logData.url,
status: status + "-" + statusText,
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);
}
return send.apply(this, arguments);
};
}
白屏监测
原理
- 这个白屏需要和首屏渲染那些指标区分出来,这个是页面异常了白屏的反馈。
- 通过document.elementsFromPoint,进行取点,这个取点一般情况是根据页面设计搞得,为了方便也可以横着竖着取个十字。然后可以获取到里面的元素,因为这个能获取最里面的元素,所以就通过这个进行判断,如果排除html,body等容器标签,仍有标签存在,那就不是空白点,否则是空白点,根据业务需要设定空白点比值,大于这个值就是白屏。
export default function blankscreen() {
let wrapperElements = ["html", "body", "#container", ".content"];
let emptyPoints = 0;
function getSelector(element) {
if (element.id) {
return "#" + id;
} else if (element.className) {
return (
"." +
element.className
.split(" ")
.filter((i) => !!i)
.join(".")
);
} else {
return element.nodeName.toLowerCase();
}
}
function iswrapper(element) {
let selector = getSelector(element);
if (wrapperElements.indexOf(selector) != -1) {
emptyPoints++;
}
}
onload(function () {
for (let i = 1; i <= 9; i++) {
let xele = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
let yele = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
iswrapper(xele[0]);
iswrapper(yele[0]);
}
if (emptyPoints >= 10) {
let centerElement = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
);
tracker.send({
kind: "stability",
type: "blank",
emptyPoints,
screen: window.screen.width + "X" + window.screen.height,
viewPoint: window.innerWidth + "X" + window.innerHeight,
selector: getSelector(centerElement[0]),
});
}
});
}
加载时间
原理
- 主要利用浏览器的api performance.timing制作上报即可。
字段 |
含义 |
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回调函数执行完成的时间 |
阶段名 |
描述 |
计算方式 |
意义 |
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 |
|
export default 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,
ttfbTime: responseStart - requestStart,
responseTime: responseEnd - responseStart,
parseDomTime: loadEventStart - domLoading,
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart,
timeToInteractive: domInteractive - fetchStart,
loadTime: loadEventStart - fetchStart,
});
}, 3000);
});
}
性能指标
原理
- 主要利用浏览器api performanceObserver。
<html>
<head>
<script>
var observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (console) {
console.log("Name: " + entry.name +
", Type: " + entry.entryType +
", Start: " + entry.startTime +
", Duration: " + entry.duration + "\n");
}
})
});
observer.observe({entryTypes: ['resource', 'mark', 'measure']});
performance.mark('registered-observer');
function clicked(elem) {
performance.measure('button clicked');
}
script>
head>
<body>
<button onclick="clicked(this)">Measurebutton>
body>
html>
- 这个api可以很方便的对某些特定元素进行计算时间。比如你的某个dom是异步加载上来的,那么只要在这个dom上添加属性elementtiming,值随意,那么就能计算这个元素渲染到页面的FMP。
<img... elementtiming='foobar'/>
<p elementtiming='yehuozhili'>This is text I care about.</p>
...
<script>
const observer = new PerformanceObserver((list) => {
let perfEntries = list.getEntries();
});
observer.observe({type: 'element', buffered: true});
</script>
- 下面这个表的字段可以在chrome里看见,点击录制之后刷新网站在性能里。
字段 |
描述 |
备注 |
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(首次输入延迟) |
用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 |
- 这里面第一次交互时间就是first-input,它并不是input输入框,而是用户的第一次交互,比如第一次点击页面之类的那一下就会产生。
if (window.PerformanceObserver) {
let FMP, LCP;
let p1 = new Promise((res) => {
new PerformanceObserver((entryList, observer) => {
let perf = entryList.getEntries();
FMP = perf[0];
observer.disconnect();
res(FMP);
}).observe({ entryTypes: ["element"] });
});
let p2 = new Promise((res) => {
new PerformanceObserver((entryList, observer) => {
let perf = entryList.getEntries();
LCP = perf[0];
observer.disconnect();
res(LCP);
}).observe({ entryTypes: ["largest-contentful-paint"] });
});
new Promise((res) => {
new PerformanceObserver((entryList, observer) => {
let lastevnet = getLastEvent();
let perf = entryList.getEntries()[0];
if (perf) {
let inputDelay = perf.processingStart - perf.startTime;
let duration = perf.duration;
if (inputDelay > 0 || duration > 0) {
tracker.send({
kind: "experience",
type: "fistInputDelay",
inputDelay,
duration,
startTime: perf.startTime,
selector: lastevnet
? getSelector(lastevnet.path || lastevnet.target)
: "",
});
}
}
observer.disconnect();
res(perf);
}).observe({ type: "first-input", buffered: true });
});
Promise.all([p1, p2]).then(() => {
let FP = performance.getEntriesByName("first-paint")[0];
let FCP = performance.getEntriesByName("first-contentful-paint")[0];
tracker.send({
kind: "experience",
type: "paint",
firstPaint: FP.startTime,
firstContentFulPaint: FCP.startTime,
firstMeaningFulPaint: FMP.startTime,
largestContentFulPaint: LCP.startTime,
});
});
}
卡顿指标
原理
- 跟上面差不多,但是需要observe longtask。50ms以上的事件会被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: entry.startTime,
duration: entry.duration,
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
});
}
});
}).observe({ entryTypes: ["longtask"] });
PV
原理
- 这个ip方面可以通过sohu脚本获取,其他方面有个navigator.connection,可以获取到网络环境,往返时间之类。
- 用户停留时间就是需要监听unload事件,然后减去开始时间即可。
- 卸载页面会有几个问题,就是卸载页面不能异步发送请求,否则发送不到,所以需要同步发送请求。
- 为了减少同步发送请求所造成的性能问题使得用户下一次页面跳转过于墨迹,有个api叫Navigator.sendBeacon可以解决这个问题。这api就等于同步发送,只是用户体验更好。
export default function pv() {
var connection = navigator.connection;
tracker.send({
kind: "business",
type: "pv",
effectiveType: connection.effectiveType,
rtt: connection.rtt,
screen: `${window.screen.width}x${window.screen.height}`,
ip: window.userip,
});
let startTime = Date.now();
window.addEventListener(
"unload",
() => {
let stayTime = Date.now() - startTime;
tracker.send({
kind: "business",
type: "stayTime",
stayTime,
ip: window.userip,
});
},
false
);
}
完整demo代码
- https://github.com/yehuozhili/learnmonitor
- 注意!需要修改utils下的tracker.js里的project名和logstore名,改成自己阿里云的日志地址,不会的看第一篇。