60fps 为什么还是一顿一顿?帧时间曲线实测给你看
同样一段动画,平均 59.9fps,看着还是一顿一顿。
问题不在平均值。10 秒里 P95 帧间隔 16.8ms,最大一帧 30.2ms,那一下就是肉眼看到的顿。
我把每秒 100ms 的卡顿打开,平均帧率还有 54.5fps,最大帧间隔 116.7ms,卡顿指数 6.99。

在线案例与源码: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
登录后直接在对话框里写需求。
码道有三种使用方式: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 我才开始截图。

三、怎么做:两条轨道、一条曲线、六个数字
页面分三块:并排对照、主实验区、统计面板。
并排对照区是两条独立的动画轨道。
左轨道每 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 倍目标的帧标红,一眼就能看到尖峰。

四、实测: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 差不多;只看曲线,问题一目了然。

五、验证与踩坑:三个真实问题
第一个问题出在测量本身。
第一版 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 跑。

六、这个工具验不了什么
- 它测的是 requestAnimationFrame 的回调间隔,不是 GPU 合成和屏幕扫描。
- 它分不清浏览器是没画,还是画了但没显示。
- 120fps 档需要 120Hz 屏幕,60Hz 屏只能测到 60fps。
- 窗口被遮挡或标签页切到后台时,浏览器会降频,数字会变。
- 卡顿注入是把上一帧时间往回拨 100ms,制造一段 116.7ms 的帧间隔,不是真的忙等 100ms。
- 它验证的是统计口径,不是浏览器卡顿的物理成因。
- 单机、单浏览器、单次 10 秒,不能拿来当性能基准。
- 它不测输入延迟,也不测显示器响应时间。
七、直接下载试玩
Demo 的源码、本文档和 5 张截图都在公开仓库里。
- Demo Park 仓库:https://gitcode.com/OPCTeam/demo_park
- 本案例目录:https://gitcode.com/OPCTeam/demo_park/tree/main/codearts-frame-time-lab
本地跑三步。
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,打开就能跑,你也可以拿它测自己的动画和滚动。
- 点赞
- 收藏
- 关注作者
评论(0)