零散专题36 前端性能监控

关于白屏时间和首屏时间以及FPS监控的学习笔记。

性能监控指标

本文主要从三个角度来进行性监控:

  1. 白屏时间:从浏览器响应用户输入网址地址,到开始显示内容的时间。
  2. 首屏时间:从浏览器响应用户输入网址地址,到首屏内容渲染完成的时间。
  3. 页面渲染帧率FPS:理论不应低于60FPS

Performance API

window.performance对象提供了一组精确的数据,经过简单的计算就能够得出一些网页性能数据。

注意,现在查到的大多数文章使用的API都是performance.timing这个接口,这个接口返回一个PerformanceTiming对象,包含了页面相关的性能信息,但是这个接口已经被废弃,这样它上面定义的大量的属性,例如navigationStart都已经被废弃,应该使用performance.timeOrigin来代替。

为什么会被废弃呢?因为W3C提供了更全面、更强大的性能分析矩阵,其中最重要的工具之一就是High Resuolution Time这个基础的API,可以提供比Date.now()更精准的时间戳。

所有的规范都是基于这个高精度的时间规范。Date对象提取的时间单位都是毫秒,而使用performance.now()(或者其他performance获取到的时间单位)单位也是毫秒,但是返回变量精度达到了小数点后面10位以上,也就是说,performance.now()精度可以达到微妙级别的精度,精度更高。

performance.now()

performance.now()的返回值是一个高精度的时间戳,它与Date.now()的主要区别是:

(1)performance.now()精度使用了浮点数达到微秒级别的精确度,精度更高,用于和页面加载、渲染、性能监测等相关的场景

(2) performance.now()兼容性差(IE10+)

(3)Date.now()返回的是以1970-01-01T00:00:00Z为起点的时间戳,依赖系统时间;而performance.now()不受到系统时间的影响,返回当前网页自从performance.timeOrigin到当前时间之间的毫秒数,它是规律增长的。

performance.timeOrigin

performance.timeOrigin表示性能测试开始的时间点,即所有Performance Timeline起始点的时间,返回的是一个高精度的时间戳起点。

我理解为performance.timeOrigin就是文档开始加载的起点,与之前的performance.timing.navigationStart是相同的(实际上我在demo中测量出的时间,二者还是有一定的区别,不解)。

performance.mark()

performance.mark()一般和performance.measure()以及performance.getEntriesByName()一起使用,可以更高精度的测量某个方法的用时

1
2
3
4
5
6
7
performance.mark('fn_start');
fn();
performance.mark('fn_end');

performance.measure('fn', 'fn_start', 'fn_end');
const fnDuration = performance.getEntriesByName('x').duration;
console.log(fnDuration);

白屏时间

白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素,这里面可能包括了DNS查询、TCP连接、网络请求、加载CSS等步骤。

影响白屏时间的因素:网络、服务端性能、前端页面结构设计。

计算白屏时间

「地址栏输入网址后回车」我们用performance.timeOrigin来标识(或者使用以前的performance.timing.navigationStart

「浏览器出现第一个元素」也就是白屏时间的结束点,通常认为浏览器开始渲染<body>或者解析完<head>的时间就是白屏结束的时间点,具体实现的时候就是在<head>的末尾添加一个<script>脚本,在这个脚本中计算结束时间。

我们可以采取渐进式的方案,如果不支持performance,则使用Date.now()作为降级方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>白屏</title>
<script>
// 不兼容 performance.timing 的浏览器
window.pageStartTime = Date.now()
</script>
<!-- 页面 CSS 资源 -->
<link rel="stylesheet" href="styles/base.css">
<link rel="stylesheet" href="styles/default.css">
<script>
// 白屏结束时间
window.firstPaint = Date.now();

// 白屏时间
const paintDuration = performance ? performance.now() : (firstPaint - pageStartTime);
// performance.now() 就相当于 firstPaint - performance.timeOrigin

console.log(paintDuration);
</script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>

首屏时间

对于页面加载时间,DOM提高的API都不能够用来直接计算首屏时间(onload事件可以得到整个页面资源的加载完成,DOMConentLoad事件可以得到页面DOM树构建完成的时间)

由于浏览器处理图片资源是异步的,并不会阻塞其他DOM节点的渲染,所以对于首屏有图片的情况,应当以耗时最长的图片加载完成的时间作为首屏时间的结束,如果没有图片则可以使用DOMConentLoad打点时间

针对有图片的情况,至少要完成以下几个方面的工作:

  1. 判断屏幕尺寸
  2. 遍历所有图片,获得图片位置信息,筛选出处于首屏内图片
  3. 有图片的话,监听图片的onload事件,获取耗时最长的图片的加载时间
  4. 没有图片,监听DOMConentLoad事件获取事件

也可以通过MutationObserver来监听页面变化,当首屏DOM节点稳定后,认为首屏加载完成,具体可以参考这两篇文章的实现(《精确并自动化地获取页面首屏时间》《关于首屏时间采集自动化的解决方案》)。这样的方法有一定误差,并且实现起来也很繁琐。

但是在浏览器没有给出更多便捷的接口之前,可能也只能采取上面的某一种方式来实现了。

FPS监控

(1)开发环境

开发环境下FPS的监控可以使用开发者工具中,点击More tools中的rendering,勾选FPS meter

然后在屏幕的左上角就可以看到实时的FPS数据了:

(2)生产环境

可以通过requestAnimationFrame API来执行一个函数,如果浏览器卡顿,那么函数执行的间隔变长,我们在函数中是可以感知到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let lastTime = performance.now()
let frame = 0
let lastFameTime = performance.now()
const loop = time => {
let now = performance.now()
let fs = (now - lastFameTime)
lastFameTime = now
let fps = Math.round(1000 / fs)
frame++
if (now > 1000 + lastTime) {
fps = Math.round(( frame * 1000 ) / ( now - lastTime ))
frame = 0
lastTime = now
}
window.requestAnimationFrame(loop)
}

通过上面的代码,我们就可以收集页面的近似FPS数据,结合自定义的卡顿标准(比如连续3个低于20FPS)就可以近似判断页面卡顿情况,进行上报

(3)Node环境

可以使用puppeteer来访问页面,通过tracing接口获得的trace.json数据分析页面的FPS。

这个思路是在前公司,让我去分析页面的“跟手性”时实验过一点点的方向,当时是通过模拟CPU的卡顿,来比对页面被滑动时的时间差。

因为Puppeteer就是无头浏览器,浏览器中的接口一般都可以在其中找到,既然浏览器可以实时监控FPS数据,那么在Puppeteer中就可以获得对应的数据。

有机会再具体研究吧,不知道是否可行,但是是一个可以尝试的思路。

参考