-
Notifications
You must be signed in to change notification settings - Fork 286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前端监控 SDK 的一些技术要点原理分析 #26
Comments
有个疑问:https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/observe 这个方法接受的参数是 entryTypes 而且没有 buffered 参数,而且 entryTypes 中没有 layout-shift 的 value (https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry/entryType),所以这些参数的文档是哪里查的,还是说已经是过时的,文档没有记载? |
太牛了 |
按上面代码的判断方式,滚动到第二屏的时候,第一屏的元素还符合isInScreen。 |
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
// dom 对象是否在屏幕内
function isInScreen(dom) {
const rectInfo = dom.getBoundingClientRect()
if (
rectInfo.left >= 0
&& rectInfo.left < viewportWidth
&& rectInfo.top >= 0
&& rectInfo.top < viewportHeight
) {
return true
}
} 用这个,文章里的不是最新版。 |
学习 |
getBoundingClientRect获取元素几何属性的时候,会导致回流吧? |
这个方法是获取 DOM 属性用的,并不是设置这个 DOM 元素的属性值,所以不会导致回流。 |
一个完整的前端监控平台包括三个部分:数据采集与上报、数据整理和存储、数据展示。
本文要讲的就是其中的第一个环节——数据采集与上报。下图是本文要讲述内容的大纲,大家可以先大致了解一下:
仅看理论知识是比较难以理解的,为此我结合本文要讲的技术要点写了一个简单的监控 SDK,可以用它来写一些简单的 DEMO,帮助加深理解。再结合本文一起阅读,效果更好。
性能数据采集
chrome 开发团队提出了一系列用于检测网页性能的指标:
这四个性能指标都需要通过 PerformanceObserver 来获取(也可以通过
performance.getEntriesByName()
获取,但它不是在事件触发时通知的)。PerformanceObserver 是一个性能监测对象,用于监测性能度量事件。FP
FP(first-paint),从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的。
测量代码如下:
通过以上代码可以得到 FP 的内容:
其中
startTime
就是我们要的绘制时间。FCP
FCP(first-contentful-paint),从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,"内容"指的是文本、图像(包括背景图像)、
<svg>
元素或非白色的<canvas>
元素。为了提供良好的用户体验,FCP 的分数应该控制在 1.8 秒以内。
测量代码:
通过以上代码可以得到 FCP 的内容:
其中
startTime
就是我们要的绘制时间。LCP
LCP(largest-contentful-paint),从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP 指标会根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间。
一个良好的 LCP 分数应该控制在 2.5 秒以内。
测量代码:
通过以上代码可以得到 LCP 的内容:
其中
startTime
就是我们要的绘制时间。element 是指 LCP 绘制的 DOM 元素。FCP 和 LCP 的区别是:FCP 只要任意内容绘制完成就触发,LCP 是最大内容渲染完成时触发。
LCP 考察的元素类型为:
<img>
元素<svg>
元素内的<image>
元素<video>
元素(使用封面图像)url()
函数(而非使用CSS 渐变)加载的带有背景图像的元素CLS
CLS(layout-shift),从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数。
布局偏移分数的计算方式如下:
CLS 就是把所有布局偏移分数加起来的总和。
当一个 DOM 在两个渲染帧之间产生了位移,就会触发 CLS(如图所示)。
上图中的矩形从左上角移动到了右边,这就算是一次布局偏移。同时,在 CLS 中,有一个叫会话窗口的术语:一个或多个快速连续发生的单次布局偏移,每次偏移相隔的时间少于 1 秒,且整个窗口的最大持续时长为 5 秒。
例如上图中的第二个会话窗口,它里面有四次布局偏移,每一次偏移之间的间隔必须少于 1 秒,并且第一个偏移和最后一个偏移之间的时间不能超过 5 秒,这样才能算是一次会话窗口。如果不符合这个条件,就算是一个新的会话窗口。可能有人会问,为什么要这样规定?其实这是 chrome 团队根据大量的实验和研究得出的分析结果 Evolving the CLS metric。
CLS 一共有三种计算方式:
累加
也就是把从页面加载开始的所有布局偏移分数加在一起。但是这种计算方式对生命周期长的页面不友好,页面存留时间越长,CLS 分数越高。
取所有会话窗口的平均数
这种计算方式不是按单个布局偏移为单位,而是以会话窗口为单位。将所有会话窗口的值相加再取平均值。但是这种计算方式也有缺点。
从上图可以看出来,第一个会话窗口产生了比较大的 CLS 分数,第二个会话窗口产生了比较小的 CLS 分数。如果取它们的平均值来当做 CLS 分数,则根本看不出来页面的运行状况。原来页面是早期偏移多,后期偏移少,现在的平均值无法反映出这种情况。
取所有会话窗口中的最大值
这种方式是目前最优的计算方式,每次只取所有会话窗口的最大值,用来反映页面布局偏移的最差情况。详情请看 Evolving the CLS metric。
下面是第三种计算方式的测量代码:
在看完上面的文字描述后,再看代码就好理解了。一次布局偏移的测量内容如下:
代码中的
value
字段就是布局偏移分数。DOMContentLoaded、load 事件
当纯 HTML 被完全加载以及解析时,
DOMContentLoaded
事件会被触发,不用等待 css、img、iframe 加载完。当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发
load
事件。虽然这两个性能指标比较旧了,但是它们仍然能反映页面的一些情况。对于它们进行监听仍然是必要的。
首屏渲染时间
大多数情况下,首屏渲染时间可以通过
load
事件获取。除了一些特殊情况,例如异步加载的图片和 DOM。像这种情况就无法通过
load
事件获取首屏渲染时间了。这时我们需要通过 MutationObserver 来获取首屏渲染时间。MutationObserver 在监听的 DOM 元素属性发生变化时会触发事件。首屏渲染时间计算过程:
requestAnimationFrame()
回调函数中调用performance.now()
获取当前时间,作为它的绘制时间。监听 DOM
上面的代码就是监听 DOM 变化的代码,同时需要过滤掉
style
、script
、link
等标签。判断是否在首屏
一个页面的内容可能非常多,但用户最多只能看见一屏幕的内容。所以在统计首屏渲染时间的时候,需要限定范围,把渲染内容限定在当前屏幕内。
使用
requestAnimationFrame()
获取 DOM 绘制时间当 DOM 变更触发 MutationObserver 事件时,只是代表 DOM 内容可以被读取到,并不代表该 DOM 被绘制到了屏幕上。
从上图可以看出,当触发 MutationObserver 事件时,可以读取到
document.body
上已经有内容了,但实际上左边的屏幕并没有绘制任何内容。所以要调用requestAnimationFrame()
在浏览器绘制成功后再获取当前时间作为 DOM 绘制时间。和首屏内的所有图片加载时间作对比
优化
现在的代码还没优化完,主要有两点注意事项:
第一点,必须要在 DOM 不再变化后再上报渲染时间,一般 load 事件触发后,DOM 就不再变化了。所以我们可以在这个时间点进行上报。
第二点,可以在 LCP 事件触发后再进行上报。不管是同步还是异步加载的 DOM,它都需要进行绘制,所以可以监听 LCP 事件,在该事件触发后才允许进行上报。
将以上两点方案结合在一起,就有了以下代码:
checkDOMChange()
代码每次在触发 MutationObserver 事件时进行调用,需要用防抖函数进行处理。接口请求耗时
接口请求耗时需要对 XMLHttpRequest 和 fetch 进行监听。
监听 XMLHttpRequest
如何判断 XML 请求是否成功?可以根据他的状态码是否在 200~299 之间。如果在,那就是成功,否则失败。
监听 fetch
对于 fetch,可以根据返回数据中的的
ok
字段判断请求是否成功,如果为true
则请求成功,否则失败。注意,监听到的接口请求时间和 chrome devtool 上检测到的时间可能不一样。这是因为 chrome devtool 上检测到的是 HTTP 请求发送和接口整个过程的时间。但是 xhr 和 fetch 是异步请求,接口请求成功后需要调用回调函数。事件触发时会把回调函数放到消息队列,然后浏览器再处理,这中间也有一个等待过程。
资源加载时间、缓存命中率
通过
PerformanceObserver
可以监听resource
和navigation
事件,如果浏览器不支持PerformanceObserver
,还可以通过performance.getEntriesByType(entryType)
来进行降级处理。当
resource
事件触发时,可以获取到对应的资源列表,每个资源对象包含以下一些字段:从这些字段中我们可以提取到一些有用的信息:
判断该资源是否命中缓存
在这些资源对象中有一个
transferSize
字段,它表示获取资源的大小,包括响应头字段和响应数据的大小。如果这个值为 0,说明是从缓存中直接读取的(强制缓存)。如果这个值不为 0,但是encodedBodySize
字段为 0,说明它走的是协商缓存(encodedBodySize
表示请求响应数据 body 的大小)。不符合以上条件的,说明未命中缓存。然后将
所有命中缓存的数据/总数据
就能得出缓存命中率。浏览器往返缓存 BFC(back/forward cache)
bfcache 是一种内存缓存,它会将整个页面保存在内存中。当用户返回时可以马上看到整个页面,而不用再次刷新。据该文章 bfcache 介绍,firfox 和 safari 一直支持 bfc,chrome 只有在高版本的移动端浏览器支持。但我试了一下,只有 safari 浏览器支持,可能我的 firfox 版本不对。
但是 bfc 也是有缺点的,当用户返回并从 bfc 中恢复页面时,原来页面的代码不会再次执行。为此,浏览器提供了一个
pageshow
事件,可以把需要再次执行的代码放在里面。从 bfc 中恢复的页面,我们也需要收集他们的 FP、FCP、LCP 等各种时间。
上面的代码很好理解,在
pageshow
事件触发后,用当前时间减去事件触发时间,这个时间差值就是性能指标的绘制时间。注意,从 bfc 中恢复的页面的这些性能指标,值一般都很小,一般在 10 ms 左右。所以要给它们加个标识字段bfc: true
。这样在做性能统计时可以对它们进行忽略。FPS
利用
requestAnimationFrame()
我们可以计算当前页面的 FPS。代码逻辑如下:
requestAnimationFrame()
时,就将帧数加 1。过去一秒后用帧数/流逝的时间
就能得到当前帧率。当连续三个低于 20 的 FPS 出现时,我们可以断定页面出现了卡顿,详情请看 如何监控网页的卡顿。
Vue 路由变更渲染时间
首屏渲染时间我们已经知道如何计算了,但是如何计算 SPA 应用的页面路由切换导致的页面渲染时间呢?本文用 Vue 作为示例,讲一下我的思路。
代码逻辑如下:
router.beforeEach()
钩子,在该钩子的回调函数里将当前时间记为渲染开始时间。Vue.mixin()
对所有组件的mounted()
注入一个函数。每个函数都执行一个防抖函数。mounted()
触发时,就代表该路由下的所有组件已经挂载完毕。可以在this.$nextTick()
回调函数中获取渲染时间。同时,还要考虑到一个情况。不切换路由时,也会有变更组件的情况,这时不应该在这些组件的
mounted()
里进行渲染时间计算。所以需要添加一个needCalculateRenderTime
字段,当切换路由时将它设为 true,代表可以计算渲染时间了。错误数据采集
资源加载错误
使用
addEventListener()
监听 error 事件,可以捕获到资源加载失败错误。js 错误
使用
window.onerror
可以监听 js 错误。promise 错误
使用
addEventListener()
监听 unhandledrejection 事件,可以捕获到未处理的 promise 错误。sourcemap
一般生产环境的代码都是经过压缩的,并且生产环境不会把 sourcemap 文件上传。所以生产环境上的代码报错信息是很难读的。因此,我们可以利用 source-map 来对这些压缩过的代码报错信息进行还原。
当代码报错时,我们可以获取到对应的文件名、行数、列数:
然后调用下面的代码进行还原:
每次项目打包时,如果开启了 sourcemap,那么每一个 js 文件都会有一个对应的 map 文件。
这时 js 文件放在静态服务器上供用户访问,map 文件存储在服务器,用于还原错误信息。
source-map
库可以根据压缩过的代码报错信息还原出未压缩前的代码报错信息。例如压缩后报错位置为1 行 47 列
,还原后真正的位置可能为4 行 10 列
。除了位置信息,还可以获取到源码原文。上图就是一个代码报错还原后的示例。鉴于这部分内容不属于 SDK 的范围,所以我另开了一个 仓库 来做这个事,有兴趣可以看看。
Vue 错误
利用
window.onerror
是捕获不到 Vue 错误的,它需要使用 Vue 提供的 API 进行监听。行为数据采集
PV、UV
PV(page view) 是页面浏览量,UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次,UV 同一天内多次访问只算一次。
对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 UV。
页面停留时长
用户进入页面记录一个初始时间,用户离开页面时用当前时间减去初始时间,就是用户停留时长。这个计算逻辑可以放在
beforeunload
事件里做。页面访问深度
记录页面访问深度是很有用的,例如不同的活动页面 a 和 b。a 平均访问深度只有 50%,b 平均访问深度有 80%,说明 b 更受用户喜欢,根据这一点可以有针对性的修改 a 活动页面。
除此之外还可以利用访问深度以及停留时长来鉴别电商刷单。例如有人进来页面后一下就把页面拉到底部然后等待一段时间后购买,有人是慢慢的往下滚动页面,最后再购买。虽然他们在页面的停留时间一样,但明显第一个人更像是刷单的。
页面访问深度计算过程稍微复杂一点:
scroll
事件,在回调函数中用第一点得到的数据算出页面访问深度和停留时长。具体代码请看:
用户点击
利用
addEventListener()
监听mousedown
、touchstart
事件,我们可以收集用户每一次点击区域的大小,点击坐标在整个页面中的具体位置,点击元素的内容等信息。页面跳转
利用
addEventListener()
监听popstate
、hashchange
页面跳转事件。需要注意的是调用history.pushState()
或history.replaceState()
不会触发popstate
事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()
或者history.forward()
方法)。同理,hashchange
也一样。Vue 路由变更
Vue 可以利用
router.beforeEach
钩子进行路由变更的监听。数据上报
上报方法
数据上报可以使用以下几种方式:
我写的简易 SDK 采用的是第一、第二种方式相结合的方式进行上报。利用 sendBeacon 来进行上报的优势非常明显。
在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报。一个 HTTP 请求包含发送和接收两个步骤。其实对于上报来说,我们只要确保能发出去就可以了。也就是发送成功了就行,接不接收响应无所谓。为此,我做了个实验,在 beforeunload 用 XMLHttpRequest 传送了 30kb 的数据(一般的待上报数据很少会有这么大),换了不同的浏览器,都可以成功发出去。当然,这和硬件性能、网络状态也是有关联的。
上报时机
上报时机有三种:
requestIdleCallback/setTimeout
延时上报。建议将三种方式结合一起上报:
requestIdleCallback/setTimeout
延时上报。总结
仅看理论知识是比较难以理解的,为此我结合本文所讲的技术要点写了一个简单的监控 SDK,可以用它来写一些简单的 DEMO,帮助加深理解。再结合本文一起阅读,效果更好。
参考资料
性能监控
错误监控
行为监控
The text was updated successfully, but these errors were encountered: