前端监控
本文将从前端监控要做的3件事讲起,以及看看github上的web-tracing插件是怎么做的,尽可能展开里面关于用户体验的知识点。主要有以下几点:
- 行为监控
- 错误收集
- 性能监控### 行为监控
行为监控就是页面上加装摄像头,把我们的页面看作是游乐园,把游客访问页面看作是进游乐园游玩。那么只要在游乐园的门口,与各个主题园区加装上一个闸门,就能清楚知道游乐园里面有多少游客,那个主题最受欢迎。然后在游乐园的商店门口加装摄像头,可以监测到特定的店铺吸引哪些用户群体。换过来我们浏览器也相同,我们在进入页面的时候,触发路由事件就是游乐园的门,在页面上触发的鼠标事件就像是摄像头,页面的行为监控大致就是这些。
为什么要做用户的行为情况监控?其实也就是问:采集了用户的行为信息后我们能做什么,答案其实很简单:
- PV、UV量,日同比、周同比等。能清晰的明白流量变化
- 用户热点页面、高访问量TOP10
- 设备、浏览器语言、浏览器、活跃时间段等的用户特征
- 用户的行为追踪:某个用户,进入了网站后的一系列操作或者跳转行为;
- 用户自定义埋点上报用户行为:想做一些自定义事件的监听,比如播放某个视频的行为动作
- 多语种站点,每个语种的用户量
收集页面级的数据
那怎么收集页面的 PV
、UV
呢,这里大概给一个思路,也是我给公司用的自研监控系统中的思路。
一般的路由跳转行为,都是针对于 SPA单页应用
的,因为对于非单页应用来说,url跳转都以页面刷新的形式;
接着往下阅读之前,我们先来了解一下,html5
的 History API
,它所支持的 API
有以下五个
- history.back()
- history.go()
- history.forward()
- history.pushState()
- history.replaceState()
同时在 History API
中还有一个 事件
,该事件为 popstate
;它有着以下特点;
History.back()
、History.forward()
、History.go()
在被调用时,会触发popstate事件
- 但是
History.pushState()
和History.replaceState()
不会触发popstate事件
。
所以我们需要对 replaceState
和 pushState
,去创建新的全局Event事件。然后 window.addEventListener
监听我们加的 Event
即可
可以通过 window.history.pushState
、 popstate
或 hashchange
触发页面级的统计,可以参考以下代码:
window.history.pushState = (data, title, url) => {// ... 这里收集页面级的数据
};
window.history.replaceState = (data, title, url) => {// ... 这里收集页面级的数据
};
// hash变化也会触发popstate事件,而且会先触发popstate事件
// 可以使用popstate来代替hashchange,如果支持History H5 Api
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
window.addEventListener('popstate', () => {if (window.location.hash !== '') {// ... 这里收集页面级的数据}
});
window.addEventListener('hashchange', () => {// ... 这里收集页面级的数据
});
对了,还可以从数据请求入手,用户请求了什么接口都可以监控、收集,但是这个也是有一定的入侵性,因为可能要重写 XMLHttpRequest
或者 Fetch
。 下面也是贴一下主要代码:
// XMLHttpRequest 劫持 open方法
XMLHttpRequest.prototype.open = function (method, url, async) {_config.requestMethod = method;_config.src = url;return open.call(this, method, url, async);
};
// 劫持 send方法
XMLHttpRequest.prototype.send = function(body) {// body 就是post方法携带的参数// readyState发生改变时触发,也就是请求状态改变时// readyState 会依次变为 2,3,4 也就是会触发三次这里this.addEventListener('readystatechange', () => {if (readyState === 4) { // 请求已完成,且响应已就绪if (status === 200 || status === 304) {// ... 这里收集用户请求成功的数据} else if (errorServer) {// ... 这里收集用户请求失败的数据}}});return send.call(this, body);
};
// fetch请求拦截
const nativeFetch = window.fetch;
if (nativeFetch) {window.fetch = function traceFetch(target, options = {}) {const fetchStart = Date.now();const { method = 'GET' } = options;const result = nativeFetch(target, options);result.then((res) => {const { url, status, statusText } = res;if (status === 200 || status === 304) {// ... 这里收集用户请求成功的数据} else if (errorServer) {// ... 这里收集用户请求失败的数据}}, (e) => {// 无法发起请求,连接失败});return result;};
}
收集用户设备交互行为
页面级的收集解决了,接下来看看怎么收集用户的鼠标、键盘事件。有两种思路:
- 第一种:无代码入侵的,全量收集的* 即在全局使用click、mouseenter、dblclick等事件,用户只要在页面内有这些动作,就会被收集
- 第二种:需要在代码里面将需要收集交互的dom标记起来,用户与它交互时收集* 在特定的dom绑定事件,也可以在document绑定,用事件委托的方法触发收集逻辑
这里贴一下代码:
// 第一种思路收集方式
document.addEventListener('click', (e) => { // 点击事件;// ... 这里收集用户交互的数据
}, true);
document.addEventListener('mouseenter', (e) => { // 点击事件;// ... 这里收集用户交互的数据
}, true);
document.addEventListener('dblclick', (e) => { // 点击事件;// ... 这里收集用户交互的数据
}, true);
// 第二种思路收集方式
document.addEventListener('click', function(e) {// 检查事件源e.targe是否为Liif (e.target && e.target.nodeName.toUpperCase == "DIV") {// ...}
}
```### 前端错误收集
`前端错误收集`包括但不限于:怎么捕获前端错误,发生错误的时候应该上报什么信息,还有展示端的设计。
下面是一些捕获错误的途经:
* `window.onerror` window.onerror函数会在页面发生js错误时被调用* `addEventListener('error', callback, true)` 在捕获阶段捕捉资源加载错误信息```
window.addEventListener('error', (e) => {// ... 这里收集页面报错数据// 捕获阶段可以获取资源加载错误,script.onError link.onError img.onError,无法知道具体状态
}, true);
addeventListener('unhandledrejection',callback)
捕获 Promise 错误
window.addEventListener('unhandledrejection', (e) => {// ... 这里收集Promise报错数据
});
vue3
环境下app.config.errorHandler 捕获vue环境下的错误### 性能监控
用户打开我们的页面,需要等待多长时间才有画面出来,什么时候才能和页面交互,这个时间越短,用户的体验越好,所以一般我们会用到一些性能指标。
下面是一些常见的性能指标
FP 白屏(First Paint Time ) 从页面开始加载到浏览器中检测到渲染(任何渲染)时被触发(例如背景改变,样式应用等)
FCP 首屏(first contentful paint ) 从页面开始加载到页面内容的任何部分呈现在屏幕上的时间。关注的焦点是内容,这个度量可以知道用户什么时候收到有用的信息(文本,图像等)
FMP 首次有效绘制(First Meaningful Paint ) 表示页面的“主要内容”,开始出现在屏幕上的时间点,这项指标因页面逻辑而异,因此上不存在任何规范。 (只是记录了加载体验的最开始。如果页面显示的是启动图片或者 loading 动画,这个时刻对用用户而言没有意义)
LCP(Largest Contentful Paint ) LCP 指标代表的是视窗最大可见图片或者文本块的渲染时间。 (可以帮助我们捕获更多的首次渲染之后的加载性能,但这项指标过于复杂,而且很难解释,也经常出错,没办法确定主要内容什么时候加载完。)
长任务(Long Task) 当一个任务执行时间超过 50ms 时消耗到的任务 (50ms 阈值是从 RAIL 模型总结出来的结论,这个是 google 研究用户感知得出的结论,类似用户的感知/耐心的阈值,超过这个阈值的任务,用户会感知到页面的卡顿)
TTI (Time To Internative) 从页面开始到它的主要子资源加载到能够快速地响应用户输入的时间。(没有耗时长任务)
首次输入延时 FID (first Input Delay) 从用户第一次与页面交互到浏览器实际能够开始处理事件的时间。(点击,输入,按键)
CLS(Cumulative Layout Shift) 是所有布局偏移分数的汇总,凡是在页面完整生命周期内预料之外的布局偏移都包括。布局偏移发生在任意时间,当一个可见元素改变了它的位置,从一个渲染帧到下一个
使用Performance API获取上面的指标
Performance
是一个浏览器全局对象,提供了一组 API 用于编程式地获取程序在某些节点的性能数据。它包含一组高精度时间定义,以及配套的相关方法。下面是掘金写作页面的window.performance
其中memory代表内存,navigation代表来源,最重要的是timing,从timing可以得到的信息有很多,包括HTTPS 连接开始的时间
、HTTP 响应全部接收完成的时间(获取到最后一个字节)
、DNS 域名查询开始与结束的时间
、开始解析渲染 DOM 树的时间与渲染结束的时间
、网页内资源加载开始和结束的时间
,得到这些关键的时间点后,通过一些简单的计算,就能得出前端性能指标了
- FP:responseStart - navigationStart
- 重定向耗时:redirectEnd - redirectStart
- DNS 查询耗时:domainLookupEnd - domainLookupStart
- TCP 链接耗时:connectEnd - connectStart
- HTTP 请求耗时:responseEnd - responseStart
- 解析 dom 树耗时:domComplete - domInteractive
- DOM ready 时间:domContentLoadedEventEnd - navigationStart
- onload:loadEventEnd - navigationStart
其中LCP(Largest Contentful Paint )
指标是比较难定义的,可能每个网站的界定都不太一样,因为每个网站的侧重点都不一样,就像淘宝和B站。
FMP
智能获取算法,由于首次有意义绘制比较主观,开发者一般需要自行指定究竟哪些属于有意义的渲染元素,因此可以通过 FMP
的智能获取算法来自定义 FMP 时间。该算法的实现过程大概如下:
- 先定义元素* 体积占比大的* 屏幕内可见比大的* 属于资源加载元素的,例如:img、video、canvse等
- 根据元素对页面视觉的贡献,进行元素的权重划分,例如:img->2,video->3,canvas->3
- 对成个页面进行深度优先遍历搜索,然后对每个元素进行权重计算,具体通过
element.getBoundingClientRect
获取元素的位置和大小 - 最终将页面的所有元素权重总和的平均值,然后过滤出在
平均分之上
的元素集合,就得到了一个FMP
时间。
Performance
强大,但有缺点。就是在单页面应用中,改变 URL 时,使用 Performance
所获取的数据是不会更新的,这时候需要开发者重新设计统计方案。
这里更贴张 W3C第二版的 Navigation Timing
的处理模型(上一篇文章用的图相对比较老一点,新的图特意划分出 Resource Timing
时间,职责划分更加清晰)
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享