前端监控分为性能监控和错误监控。其中监控又分为两个环节:数据采集和数据上报。
一.数据采集
性能采集
性能数据采集需要使用 window.performance API。
从 MDN 的文档可以看出,window.performance.timing 包含了页面加载各个阶段的起始及结束时间。
这些属性需要结合下图一起看,更好理解:
timing 各个属性的意义:
timing: {
navigationStart: 1543806782096,
unloadEventStart: 1543806782523,
unloadEventEnd: 1543806782523,
redirectStart: 0,
redirectEnd: 0,
fetchStart: 1543806782096,
domainLookupStart: 1543806782096,
domainLookupEnd: 1543806782096,
connectStart: 1543806782099,
connectEnd: 1543806782227,
secureConnectionStart: 1543806782162,
requestStart: 1543806782241,
responseStart: 1543806782516,
responseEnd: 1543806782537,
domLoading: 1543806782573,
domInteractive: 1543806783203,
domContentLoadedEventStart: 1543806783203,
domContentLoadedEventEnd: 1543806783216,
domComplete: 1543806783796,
loadEventStart: 1543806783796,
loadEventEnd: 1543806783802
}
简要分析几个时间:
redirect: timing.redirectEnd - timing.redirectStart,
dom: timing.domComplete - timing.domLoading,
load: timing.loadEventEnd - timing.navigationStart,
unload: timing.unloadEventEnd - timing.unloadEventStart,
request: timing.responseEnd - timing.requestStart,
time: new Date().getTime(),
还有一个白屏时间:它指从输入网址,到页面开始显示内容的时间。
将以下脚本放在 前面就能获取白屏时间。
<script>
whiteScreen = new Date() - performance.timing.navigationStart
whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
</script>
还可以通过 window.performance.getEntriesByType(‘resource’) 这个方法,我们还可以获取相关资源(js、css、img…)的加载时间,它会返回页面当前所加载的所有资源。
它一般包括以下几个类型:
- sciprt
- link
- img
- css
- fetch
- other
- xmlhttprequest
我们只需用到以下几个信息:
name: item.name,
duration: item.duration.toFixed(2),
size: item.transferSize,
protocol: item.nextHopProtocol,
代码中写几行代码来收集这些数据。
const getPerformance = () => {
if (!window.performance) return
const timing = window.performance.timing
const performance = {
redirect: timing.redirectEnd - timing.redirectStart,
whiteScreen: whiteScreen,
dom: timing.domComplete - timing.domLoading,
load: timing.loadEventEnd - timing.navigationStart,
unload: timing.unloadEventEnd - timing.unloadEventStart,
request: timing.responseEnd - timing.requestStart,
time: new Date().getTime(),
}
return performance
}
const getResources = () => {
if (!window.performance) return
const data = window.performance.getEntriesByType('resource')
const resource = {
xmlhttprequest: [],
css: [],
other: [],
script: [],
img: [],
link: [],
fetch: [],
time: new Date().getTime(),
}
data.forEach(item => {
const arry = resource[item.initiatorType]
arry && arry.push({
name: item.name,
duration: item.duration.toFixed(2),
size: item.transferSize,
protocol: item.nextHopProtocol,
})
})
return resource
}
总结:
通过对性能及资源信息的解读,我们可以判断出页面加载慢有以下几个原因:
- 资源过多、过大
- 网速过慢
- DOM 元素过多
错误数据采集
目前所能捕捉的错误有三种:
- 资源加载错误,通过 addEventListener(‘error’, callback, true) 在捕获阶段捕捉资源加载失败错误。
- js 执行错误,通过 window.onerror 捕捉 js 错误。
- promise 错误,通过 addEventListener(‘unhandledrejection’, callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。
我们可以建一个错误数组变量 errors 在错误发生时,将错误的相关信息添加到数组,然后在某个阶段统一上报,具体如何操作请看下面的代码:
addEventListener('error', e => {
const target = e.target
if (target != window) {
monitor.errors.push({
type: target.localName,
url: target.src || target.href,
msg: (target.src || target.href) + ' is load error',
time: new Date().getTime(),
})
}
}, true)
window.onerror = function(msg, url, row, col, error) {
monitor.errors.push({
type: 'javascript',
row: row,
col: col,
msg: error && error.stack? error.stack : msg,
url: url,
time: new Date().getTime(),
})
}
addEventListener('unhandledrejection', e => {
monitor.errors.push({
type: 'promise',
msg: (e.reason && e.reason.msg) || e.reason || '',
time: new Date().getTime(),
})
})
性能数据上报
性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。
window.onload = () => {
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
})
} else {
setTimeout(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
}, 0)
}
}
错误数据上报
(即时上报):可以避免 “收集完错误,但延时上报还没触发,用户却已经关掉网页导致错误数据丢失” 的问题。
window.onerror = function(msg, url, row, col, error) {
const data = {
type: 'javascript',
row: row,
col: col,
msg: error && error.stack? error.stack : msg,
url: url,
time: new Date().getTime(),
}
axios.post({ url: 'xxx', data, })
}
在拓展一下:
SPA
window.performance API 是有缺点的,在 SPA 切换路由时,window.performance.timing 的数据不会更新。所以我们需要另想办法来统计切换路由到加载完成的时间。拿 Vue 举例,一个可行的办法就是切换路由时,在路由的全局前置守卫 beforeEach 里获取开始时间,在组件的 mounted 钩子里执行 vm.$nextTick 函数来获取组件的渲染完毕时间。
router.beforeEach((to, from, next) => {
store.commit('setPageLoadedStartTime', new Date())
})
mounted() {
this.$nextTick(() => {
this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
})
}
DEMO
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta >
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script>
function monitorInit() {
const monitor = {
url: '',
performance: {},
resources: {},
errors: [],
user: {
screen: screen.width,
height: screen.height,
platform: navigator.platform,
userAgent: navigator.userAgent,
language: navigator.language,
},
addError(error) {
const obj = {}
const { type, msg, url, row, col } = error
if (type) obj.type = type
if (msg) obj.msg = msg
if (url) obj.url = url
if (row) obj.row = row
if (col) obj.col = col
obj.time = new Date().getTime()
monitor.errors.push(obj)
},
reset() {
window.performance && window.performance.clearResourceTimings()
monitor.performance = getPerformance()
monitor.resources = getResources()
monitor.errors = []
},
clearError() {
monitor.errors = []
},
upload() {
},
setURL(url) {
monitor.url = url
},
}
const getPerformance = () => {
if (!window.performance) return
const timing = window.performance.timing
const performance = {
redirect: timing.redirectEnd - timing.redirectStart,
whiteScreen: whiteScreen,
dom: timing.domComplete - timing.domLoading,
load: timing.loadEventEnd - timing.navigationStart,
unload: timing.unloadEventEnd - timing.unloadEventStart,
request: timing.responseEnd - timing.requestStart,
time: new Date().getTime(),
}
return performance
}
const getResources = () => {
if (!window.performance) return
const data = window.performance.getEntriesByType('resource')
const resource = {
xmlhttprequest: [],
css: [],
other: [],
script: [],
img: [],
link: [],
fetch: [],
time: new Date().getTime(),
}
data.forEach(item => {
const arry = resource[item.initiatorType]
arry && arry.push({
name: item.name,
duration: item.duration.toFixed(2),
size: item.transferSize,
protocol: item.nextHopProtocol,
})
})
return resource
}
window.onload = () => {
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
console.log('页面性能信息')
console.log(monitor.performance)
console.log('页面资源信息')
console.log(monitor.resources)
})
} else {
setTimeout(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
console.log('页面性能信息')
console.log(monitor.performance)
console.log('页面资源信息')
console.log(monitor.resources)
}, 0)
}
}
addEventListener('error', e => {
const target = e.target
if (target != window) {
monitor.errors.push({
type: target.localName,
url: target.src || target.href,
msg: (target.src || target.href) + ' is load error',
time: new Date().getTime(),
})
console.log('所有的错误信息')
console.log(monitor.errors)
}
}, true)
window.onerror = function(msg, url, row, col, error) {
monitor.errors.push({
type: 'javascript',
row: row,
col: col,
msg: error && error.stack? error.stack : msg,
url: url,
time: new Date().getTime(),
})
console.log('所有的错误信息')
console.log(monitor.errors)
}
addEventListener('unhandledrejection', e => {
monitor.errors.push({
type: 'promise',
msg: (e.reason && e.reason.msg) || e.reason || '',
time: new Date().getTime(),
})
console.log('所有的错误信息')
console.log(monitor.errors)
})
return monitor
}
const monitor = monitorInit()
</script>
<link rel="stylesheet" href="test.css">
<title>Document</title>
</head>
<body>
<button>错误测试按钮1</button>
<button>错误测试按钮2</button>
<button>错误测试按钮3</button>
<img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">
<img src="test.png" alt="">
<script src="192.168.10.15/test.js"></script>
<script>
document.querySelector('.btn1').onclick = () => {
setTimeout(() => {
console.log(button)
}, 0)
}
document.querySelector('.btn2').onclick = () => {
new Promise((resolve, reject) => {
reject({
msg: 'test.js promise is error'
})
})
}
document.querySelector('.btn3').onclick = () => {
throw ('这是一个手动扔出的错误')
}
</script>
</body>
</html>