最近有大量用户反馈,使用我们的平台实在是太卡了。所以于是总监大手一挥,"咱们这个Q必须得做性能优化,如果用户用咱们的平台都卡、用户怎么可能乐意来消费呢?"
我大声反驳"用户用起来卡是因为用户的电脑太差了,换台性能好点的电脑就不卡了"
---------以上是我的幻想

于是乎在组里大佬的带领下、吭哧吭哧搞了一个Q,就有了性能优化一系列的文章;
事先声明、作为一个刚毕业一年多的前端菜狗、这个东西肯定不是我搞出来的、这个得感谢公司里的前辈、做了完善的监控机制,打好了基础架构;才能让我在前端的海洋里不停溺水又浮起来,然后继续溺水。。。。
如何去定义性能指标?
首先、我们的项目是一个普普通通的web端的项目;那怎么去看一个网页它的性能呢?诶这个时候肯定有同学说了,这个我知道lighthouse来一波,常用的性能指标:FP、FCP、CLS等看一看、再看看白屏时间啥的; 在掘金里搜前端性能优化、大部分都是这个方向的内容;
也不能说这些东西是错的、但是有几个问题:
- lighthouse在不同性能的电脑上结果不一样,不能当作一个可以锚定的指标。
- 用常用的性能指标来作为标准?这么多个指标怎么去平衡这些指标呢?定一个比例?还是就选取几个?
- 其实最重要的是老板可能不知道你的性能指标是什么东西,如果+1或者+2是rd/qa/pm升上去的,你对着老板说我们使用FCP首次内容绘制来定义性能;老板:"啥?你在说什么玩意?" 咱打工人 到最后都是为了满足那个okr嘛,老板都不理解你在搞什么东西,这绩效还要吗。。。
所以有没有办法去整一个指标,既能让非前端人员简单易懂,又能比较能表达性能呢?
有的,兄弟,有的。

卡顿率
什么是卡顿率?
先来一段文绉绉的定义:卡顿率 用于衡量用户在使用页面过程中,画面无法以正常帧率持续渲染的时间占比,是一个反映页面整体流畅性体验的百分比指标。
一句话来概括就是:卡顿率表示用户在可感知使用页面的过程中,有多大比例的时间画面处于"卡住"的状态。
这下其他岗位的同学也能听懂了,就是我在使用某个页面的时间里,有多长时间的占比卡了嘛;
怎么统计卡顿率?
卡顿率的简单分类
目前卡顿率分为三种类型:
- 时间卡顿率:卡顿时间 / 总观察时间;
- 卡顿率 = Long Task 总耗时 / 总时长
- 交互卡顿率:交互的卡顿次数 / 交互总次数;
- 100 次点击,其中 12 次发生 Long Task,就认为卡顿率 = 12%
- Task卡顿率
- Long Task 次数 / 总 Task 次数
目前我们使用的是时间卡顿率、也就是第一种;
卡顿时间的优化
W3C 标准里提出,任何在主线程上执行时间超过 50 毫秒 (ms) 的任务都被定义为 Long Task。 也就是说:单次 task > 50ms 就算长任务,就应该算入卡顿时间。
对吗???
从前端的角度来看,对的对的。但是从用户的感受来看,卡50ms,好像也感觉不出来?
现在屏幕的理想刷新率一般是60帧每秒,1000/60=16.7ms; 50/16.7约等于3帧;于是我们加上了一个前提条件:连续三帧都有长任务并且被完全阻塞,才会算做卡顿。
(一帧能用,两帧流畅,三帧电竞。。。。)
注意点: 这里的有长任务不是说我只要碰到0.1ms都算,而是完全占满才算;
todo:补一张图,拿nano banana跑了几次出来的图都不可用,还得自己画。。 大概长这样:
Frame 0: [0 ----- 16.7] (ms)
Frame 1: [16.7 -- 33.4]
Frame 2: [33.4 -- 50.1]
Frame 3: [50.1 -- 66.8]
Long Task [10ms ------------------------- 61ms] 长任务耗时51ms,可能从frame0的中间开始,frmae3的中间结束
因为我们部分占用并不代表着会影响帧渲染,当然也不一定能保证说剩下的时间就一定够渲染;比如占用6ms,剩下的10ms不能确定能不能完成渲染;目前也没有办法去统计是非完成渲染,所以我们只统计一定被占满的帧,去减少噪音;
| 情况 | 渲染统计 |
|---|---|
| 帧完全落在 Long Task 内 | 100% 无法渲染 |
| 帧部分落在 Long Task 内 | 不确定,忽略不计 |
| 帧未落在 Long Task 内 | 不影响 |
总观察时间
为什么叫做总观察时间而不是总时间,这个也好理解;
如果页面处于前台、用户操作,那必须得记录;
如果用户失焦了、点到了别的标签,这里后台可能会开始执行一些动作比如上报埋点数据、这个时候因为用户看不到,所以就算发生了长任务也不计入;
又或者是进入了下一个界面,当前界面冻结了,过了一会点了回退;中间这段时间就得排除,因为实际上在这段时间里,没有做任何的交互,不做排除会导致分母变大,数据失真;
伪代码部分
那么其实我们要做的就很明显了:
卡顿率 = 页面处于前台、用户可感知期间,画面被连续 ≥3 帧阻塞的时间 / 可感知总时间
核心常量 & 状态
js
const FRAME_TIME = 16.7; // 一帧时间
const STALL_FRAMES = 3; // 连续三帧算卡顿
let longTasks = [];
// 统计页面的可感知时间
let observing = true;
let lastActiveTime = performance.now();
let activeTime = 0;
// 卡顿时间
let stallTime = 0;
// 是否结束统计
let finalized = false;
Long Task的监听
js
const longTaskObserver = new PerformanceObserver(list => {
if (!observing) return;
list.getEntries().forEach(entry => {
longTasks.push({
start: entry.startTime,
duration: entry.duration
});
});
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
用Long Task计算出完整占用帧数的时间
js
function getBlockedFrameCount(task) {
const startFrame = Math.floor(task.start / FRAME_TIME);
const endFrame = Math.floor(
(task.start + task.duration) / FRAME_TIME
);
// 只计算完整被占用的帧
return Math.max(0, endFrame - startFrame - 1);
}
用visibilitychange和pagehide监听,排除标签切走/最小化/页面冻结的时间
js
document.addEventListener('visibilitychange', () => {
const now = performance.now();
if (document.hidden) {
if (observing) {
activeTime += now - lastActiveTime;
observing = false;
}
} else {
lastActiveTime = now;
observing = true;
}
});
window.addEventListener('pagehide', event => {
if (event.persisted) {
return;
}
finalize();
});
window.addEventListener('pageshow', event => {
if (event.persisted) {
lastActiveTime = performance.now();
observing = true;
}
});
结束统计
js
function finalize() {
if (finalized) return;
finalized = true;
const now = performance.now();
if (observing) {
activeTime += now - lastActiveTime;
}
longTasks.forEach(task => {
const blockedFrames = getBlockedFrameCount(task);
if (blockedFrames >= STALL_FRAMES) {
stallTime += blockedFrames * FRAME_TIME;
}
});
const stallRate = activeTime > 0 ? (stallTime / activeTime) * 100 : 0;
report({
activeTime,
stallTime,
stallRate,
stallCount: longTasks.filter(
t => getBlockedFrameCount(t) >= STALL_FRAMES
).length
});
longTaskObserver.disconnect();
}
结语
其实如果要把卡顿率做一个完整商用的sdk里的某个指标,除了本文提到的内容,缺失的部分还有很多,比如什么时候执行、什么时候发送,等等...... 这里只是搜了一下掘金发现前端性能优化这块没有找到相关内容,因此把在工作中学到的知识,简单总结了一下,感谢阅读。
