【前端性能优化】指标篇:卡顿率——如何去定义你的页面卡不卡

最近有大量用户反馈,使用我们的平台实在是太卡了。所以于是总监大手一挥,"咱们这个Q必须得做性能优化,如果用户用咱们的平台都卡、用户怎么可能乐意来消费呢?"

我大声反驳"用户用起来卡是因为用户的电脑太差了,换台性能好点的电脑就不卡了"

---------以上是我的幻想

于是乎在组里大佬的带领下、吭哧吭哧搞了一个Q,就有了性能优化一系列的文章;

事先声明、作为一个刚毕业一年多的前端菜狗、这个东西肯定不是我搞出来的、这个得感谢公司里的前辈、做了完善的监控机制,打好了基础架构;才能让我在前端的海洋里不停溺水又浮起来,然后继续溺水。。。。

如何去定义性能指标?

首先、我们的项目是一个普普通通的web端的项目;那怎么去看一个网页它的性能呢?诶这个时候肯定有同学说了,这个我知道lighthouse来一波,常用的性能指标:FP、FCP、CLS等看一看、再看看白屏时间啥的; 在掘金里搜前端性能优化、大部分都是这个方向的内容;

也不能说这些东西是错的、但是有几个问题:

  1. lighthouse在不同性能的电脑上结果不一样,不能当作一个可以锚定的指标。
  2. 用常用的性能指标来作为标准?这么多个指标怎么去平衡这些指标呢?定一个比例?还是就选取几个?
  3. 其实最重要的是老板可能不知道你的性能指标是什么东西,如果+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里的某个指标,除了本文提到的内容,缺失的部分还有很多,比如什么时候执行、什么时候发送,等等...... 这里只是搜了一下掘金发现前端性能优化这块没有找到相关内容,因此把在工作中学到的知识,简单总结了一下,感谢阅读。

相关推荐
冬奇Lab9 小时前
稳定性性能系列之十——卡顿问题分析:从掉帧到流畅体验
android·性能优化
Aliex_git9 小时前
性能优化 - Vue 日常实践优化
前端·javascript·vue.js·笔记·学习·性能优化
飞鹰5110 小时前
CUDA入门:从Hello World到矩阵运算 - Week 1学习总结
c++·人工智能·性能优化·ai编程·gpu算力
sweet丶10 小时前
Swift 方法派发深度解析:从 Swizzling 到派发机制
性能优化
cn_mengbei1 天前
从零到一:基于Qt on HarmonyOS的鸿蒙PC原生应用开发实战与性能优化指南
qt·性能优化·harmonyos
DemonAvenger1 天前
Redis慢查询分析与优化:性能瓶颈排查实战指南
数据库·redis·性能优化
triumph_passion1 天前
Zustand 从入门到精通:我的工程实践笔记
前端·性能优化
shughui1 天前
JMter(六):jmete变量提取常用方式
jmeter·性能优化
dyxal1 天前
Excel情感标注工具性能优化实战:从卡顿到流畅的蜕变
网络·性能优化·excel