60fps 为什么还是一顿一顿?帧时间曲线实测给你看

举报
deli007 发表于 2026/11/11 08:30:41 2026/11/11
【摘要】 60fps 为什么还是一顿一顿?帧时间曲线实测给你看

同样一段动画,平均 59.9fps,看着还是一顿一顿。

问题不在平均值。10 秒里 P95 帧间隔 16.8ms,最大一帧 30.2ms,那一下就是肉眼看到的顿。

我把每秒 100ms 的卡顿打开,平均帧率还有 54.5fps,最大帧间隔 116.7ms,卡顿指数 6.99。

图 1:开启每秒 100ms 卡顿后,帧时间曲线出现 6 根尖峰,最高 116.7ms,平均帧率仍有 54.5fps

在线案例与源码:https://gitcode.com/OPCTeam/demo_park/tree/main/codearts-frame-time-lab
成品只有一个 index.html,零依赖、无外链,断网可用。下面的数字都是我在页面上跑出来的。

一、为什么值得看:帧率是平均数,卡顿是最大值

60fps 的意思是每秒 60 帧,换算成帧间隔是 16.7ms。

只要有一帧拖到 30ms,这一秒的平均帧率还是接近 60,眼睛却已经看到停顿。

播放器、游戏、滚动页面都一样,平均值好看不代表顺滑。

真正决定手感的是每一帧的间隔,尤其是最慢的那几帧。

目标帧率 理论帧间隔 一帧的预算
30fps 33.3ms 33.3ms
60fps 16.7ms 16.7ms
120fps 8.3ms 8.3ms

这张表里最容易被忽略的是 120fps。

屏幕只有 60Hz 时,浏览器每秒最多给你 60 次绘制机会,8.3ms 的预算根本用不上。

后面 120fps 档实测只有 60.0fps,不是代码慢,是屏幕到顶了。

二、准备环境:进入码道 Web

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwlb&sourcead=dmzntgwlb1&invitecode=INV-IXhxk5bwWx_qKSP1FcgSuiOEf6ruDXtjvLFiCH9nMs5dZGLapaSSylwt-cKRj5LoT3MlxikWxy8VhqplfvtQjefQOq31Tbp3R3wYP3K535Cn4XTaUzqqExKPYTWvvw4LijV14IS1torXdTGp7oyXb6SR5VxgYTg50jM

登录后直接在对话框里写需求。

码道有三种使用方式:WebUI 浏览器对话、TUI 终端命令行和桌面 IDE 插件。本文用 WebUI 版演示。

这一轮我把需求写成一条,重点只有四个:单文件、无外链、真实测帧、失败路径。

做一个单文件网页应用「帧时间实验室」,纯 HTML + CSS + JavaScript,不依赖任何外部库,不发起任何网络请求,打开 index.html 就能用。

页面要回答一个问题:为什么平均 60fps 的动画还是一顿一顿?全部用真实测量,不做示意图。

功能要求:
1. 顶部并排对照区:左轨道按 30fps 刷新,右轨道按 60fps 刷新,两个方块做同样的水平往返运动,各自带运动拖影,方便肉眼比较顺滑度。
2. 控制条:目标帧率(30 / 60 / 120 / 自定义 5~240)、动画时长(3~30 秒)、动画类型(方块往返 / 滚动文字 / 旋转指针)、开始、停止、重置;再加一个「每秒注入 100ms 卡顿」开关,用来制造平均帧率不低但明显卡顿的场景。
3. 实时帧时间曲线:横轴时间,纵轴帧间隔 ms,画 16.7ms 与 33.3ms 两条参考线,超过目标帧间隔 1.5 倍的帧标红。
4. 统计面板:平均 FPS、平均帧间隔、P95 帧间隔、最大帧间隔、掉帧数、卡顿指数(最大帧间隔 / 中位帧间隔),每次运行结束给出结果。
5. 一张对照表:30 / 60 / 120fps 的理论帧间隔,以及本次实测的平均帧间隔、P95 和掉帧数,方便直接对比。
6. 页面底部放自检面板,加载后自动跑至少 12 条断言并把 PASS / FAIL 显示出来。断言至少覆盖:帧间隔与目标帧率的换算、P95 计算、掉帧判定(大于 1.5 倍目标)、卡顿指数计算、帧率输入边界 5~240、时长边界 3~30、对照表三档数值。
7. 失败路径:帧率输入 0、5 以下、240 以上,或时长小于 3 秒时,输入框变红并显示中文提示「帧率需在 5~240 之间,已保留上一次有效值」或「时长需在 3~30 秒之间,已保留上一次有效值」,同时继续使用上一次有效值,不能崩溃。
8. 页面自检和运行结果都要在界面上看得见,不要只输出到控制台。

完成后打开预览,让我直接看到运行效果。

生成用了 GLM-5.2-ArkTS-SPARK,约 17 分钟,产出 1220 行、34KB 的 index.html。

页面加载后自己跑 20 条断言,全部 PASS 我才开始截图。

图 2:打开页面后的初始界面,左边 30fps、右边 60fps 两条对照轨道,底部是控制条和实时曲线

三、怎么做:两条轨道、一条曲线、六个数字

页面分三块:并排对照、主实验区、统计面板。

并排对照区是两条独立的动画轨道。

左轨道每 33.3ms 更新一次,右轨道每 16.7ms 更新一次,方块做同样的往返运动。

两条轨道都带拖影,慢的那条会留下更长的残影。

主实验区的循环只做一件事:记录两次绘制之间的间隔。

if (elapsed >= this.targetInterval - 2) {
  this.frameIntervals.push(elapsed);
  this._update(elapsed);
  this._render();
}

-2 是 2ms 容差,后面会讲它为什么必须加。

统计面板给六个数字:平均 FPS、平均帧间隔、P95 帧间隔、最大帧间隔、掉帧数、卡顿指数。

掉帧的判定是帧间隔大于目标间隔的 1.5 倍。

卡顿指数是最大帧间隔除以中位帧间隔,1 表示很稳,超过 3 就该查了。

实时曲线把每一帧画成一条竖线,16.7ms 和 33.3ms 两条参考线固定在背景里。

超过 1.5 倍目标的帧标红,一眼就能看到尖峰。

图 3:60fps 档运行到第 5 秒,主实验区的方块正在移动,曲线已记录 303 帧

四、实测:30、60、120 三档,外加一次卡顿注入

我先跑 30fps 和 60fps,再跑 120fps,最后打开卡顿注入再跑一次 60fps。

每次 10 秒,窗口 1600×1000,同一台机器、同一个浏览器。

目标 理论帧间隔 实测平均 实测 P95 最大帧间隔 掉帧数
30fps 33.3ms 33.4ms 33.4ms 47.3ms 0
60fps 16.7ms 16.7ms 16.8ms 30.2ms 1
120fps 8.3ms 16.7ms 16.7ms 16.8ms 600
60fps 加卡顿 16.7ms 18.4ms 16.8ms 116.7ms 11

30fps 档跑出 30.0fps,平均间隔 33.4ms,10 秒里没有一帧超过 50ms。

60fps 档跑出 59.9fps,平均间隔 16.7ms,但最大一帧 30.2ms,掉了 1 帧。

这一帧就是开头说的那下顿。

120fps 档跑出 60.0fps,掉帧数 600。

600 不是 600 次事故,是 120fps 的 1.5 倍阈值只有 12.45ms,60Hz 屏幕每帧 16.7ms 都超过它。

卡顿注入那一组最有意思。

平均帧率 54.5fps,看着只掉了 5 帧。

最大帧间隔 116.7ms,卡顿指数 6.99,10 秒里出现 11 次明显停顿。

只看平均 FPS,这组和正常 60fps 差不多;只看曲线,问题一目了然。

图 4:统计面板与对照表,平均 59.9fps、P95 16.8ms、最大 30.2ms、掉帧 1、卡顿指数 1.81

五、验证与踩坑:三个真实问题

第一个问题出在测量本身。

第一版 60fps 档跑出 39.8fps,平均间隔 25.1ms,10 秒掉了 202 帧。

我盯着代码看了一遍,问题在循环的入口判断。

if (elapsed >= this.targetInterval) {

60fps 的目标间隔是 16.667ms,浏览器实际给的是 16.6ms 左右。

16.6 小于 16.667,这一帧被跳过,下一帧记成 33.3ms。

一帧跳过一帧,平均值正好落在 25ms 附近。

把判断改成 elapsed >= this.targetInterval - 2,留 2ms 容差,60fps 档回到 59.9fps。

这不是界面问题,是测量口径错了。

第二个问题在自检里。

有一条断言写的是 16.7ms 目标下,25ms 应该算掉帧。

可 16.7 × 1.5 = 25.05,严格大于才算掉帧,25 差 0.05ms。

页面显示 FAIL,我把用例改成 26ms,20 条断言全过。

这条 FAIL 说明自检确实在跑,不是摆设。

第三个问题是失败路径。

第一版输入 500 后,代码立刻把输入框改回上一次有效值,再触发 blur,红框和提示马上被清掉。

用户几乎看不到错误。

我改成保留非法输入,只把运行逻辑回退到上一次有效值。

现在输入 500,输入框变红,提示停在下面,点开始仍然按上一次有效的 60fps 跑。

图 5:自定义帧率输入 500,输入框变红,提示「帧率需在 5~240 之间,已保留上一次有效值」

六、这个工具验不了什么

  • 它测的是 requestAnimationFrame 的回调间隔,不是 GPU 合成和屏幕扫描。
  • 它分不清浏览器是没画,还是画了但没显示。
  • 120fps 档需要 120Hz 屏幕,60Hz 屏只能测到 60fps。
  • 窗口被遮挡或标签页切到后台时,浏览器会降频,数字会变。
  • 卡顿注入是把上一帧时间往回拨 100ms,制造一段 116.7ms 的帧间隔,不是真的忙等 100ms。
  • 它验证的是统计口径,不是浏览器卡顿的物理成因。
  • 单机、单浏览器、单次 10 秒,不能拿来当性能基准。
  • 它不测输入延迟,也不测显示器响应时间。

七、直接下载试玩

Demo 的源码、本文档和 5 张截图都在公开仓库里。

本地跑三步。

git clone https://gitcode.com/OPCTeam/demo_park.git
cd demo_park\codearts-frame-time-lab
python -m http.server 8080

浏览器打开 http://localhost:8080/index.html。

先点「开始」看 60fps 曲线,再打开「每秒注入 100ms 卡顿」看尖峰,最后把自定义帧率改成 500 看红框。

八、使用码道体会

  • 把验收条件写进需求,比写完再补测试省事。
  • 自检要覆盖主逻辑的边界,不然测试全绿也可能测错。
  • 真实数据一跑出来就能发现口径问题,39.8fps 这种数藏不住。
  • 单文件、无外链的约束,让复现成本降到一次下载。
  • 边界要写进页面,比如 60Hz 屏跑不了 120fps。
  • 真实 bug 比设计取舍更值得写,读者能照着复现。

九、总结

60fps 是一个平均数,顺滑是每一帧的事。

把帧间隔画出来,平均帧率骗不了人。

这个页面只有 34KB,打开就能跑,你也可以拿它测自己的动画和滚动。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。