1. 卡顿的本质:从用户感知到技术指标,理解60fps与16ms的黄金法则
做前端性能优化这么多年,我经常被问到同一个问题:「为什么我的页面数据加载挺快,用户还是说卡?」
嗯,这个问题其实问到了点子上。加载快不等于流畅。你想想看,一个页面就算1秒就加载完了,但滚动的时候一帧一帧地跳,用户照样会骂。今天我们就从最底层聊起——卡顿到底是个什么东西。
1.1 用户感知的「卡」到底是什么?
先说说我的个人习惯。每次接手一个新项目,我第一件事不是看代码,而是自己先滚一滚页面、点一点按钮。为什么?因为用户的感受是最直接的。
用户说的「卡」,其实就三种表现:
- 掉帧 —— 滚动页面时画面一顿一顿的,像幻灯片
- 延迟响应 —— 点击按钮后,过了好一会儿才有反馈
- 动画不连贯 —— 过渡动画中间有明显的停顿或跳跃
说白了,用户不关心你用了什么框架、什么渲染引擎。他们只关心一件事:操作跟不跟手。
核心观点:卡顿的本质,就是浏览器没能在规定时间内完成一帧的渲染工作。
1.2 60fps 是怎么来的?
你可能听过「60fps是流畅的黄金标准」。但为什么是60,不是30,也不是120?
这跟人眼的视觉暂留效应有关。研究表明,当画面刷新率达到每秒60帧时,人眼会认为运动是连续的。低于这个阈值,就能感知到闪烁或卡顿。
我举个例子。电影是24fps的,为什么你不觉得卡?因为电影每一帧都带了运动模糊,大脑会自动补帧。但网页UI不一样——UI元素边缘锐利、对比度高,任何跳变都会被放大。所以UI必须跑到60fps才够。
| 帧率 | 每帧耗时 | 用户感受 |
|---|---|---|
| 60 fps | 16.67 ms | 非常流畅,跟手 |
| 30 fps | 33.33 ms | 轻微卡顿,能感知到 |
| 15 fps | 66.67 ms | 明显卡顿,无法接受 |
| < 10 fps | > 100 ms | 幻灯片效果,完全不可用 |
一个小技巧:Chrome DevTools 的 Performance 面板里,帧率条如果出现红色标记,就说明那一帧超时了。我排查问题时第一眼就看这个。
1.3 16ms 的黄金法则
60fps 意味着每帧只有 16.67ms。但这不是你全部能用的时间。
浏览器渲染一帧需要经过:JavaScript 执行 → 样式计算 → 布局 → 绘制 → 合成。这五个阶段加起来,必须控制在 16ms 以内。为什么不是 16.67ms?因为浏览器自己也要喘口气,处理一下系统事件、输入事件什么的。
我在项目中遇到过最典型的场景:一个长列表页面,每次滚动都要重新计算所有节点的位置。结果一帧花了 40ms,用户滚一下页面,画面要等两帧才能跟上。这就是典型的「布局抖动」问题。
避坑指南:我曾经接手过一个项目,开发同学在滚动事件里直接改了元素的 offsetTop,导致浏览器每次都要强制回流。一帧里触发了十几次回流,不卡才怪。记住:不要在滚动/动画循环里读写布局属性。
1.4 一帧里到底发生了什么?
我们来看一张流程图,把一帧的生命周期拆开看看。
看到没?这五个阶段是串行的。任何一个阶段超时,整帧就废了。而且更坑的是——有些操作会触发「强制同步布局」,比如你在 JS 里先改了样式,紧接着又去读 offsetHeight,浏览器就得立刻重新布局,把之前攒的优化全废掉。
1.5 怎么衡量一帧是否超时?
光说理论不行,得能动手测。我常用的工具就三个:
- Chrome DevTools Performance 面板 —— 录一段操作,看帧率条和火焰图
- Frame Rendering Stats —— 在 Chrome 地址栏输入
chrome://gpu,查看 GPU 渲染统计 - requestAnimationFrame 打点 —— 自己写个简单的帧率监控
这里给一个我常用的帧率监控代码:
let lastTime = performance.now();
let frames = 0;
function checkFPS(timestamp) {
frames++;
if (timestamp - lastTime >= 1000) {
const fps = frames;
console.log(`当前帧率: ${fps} fps`);
if (fps < 30) {
console.warn('⚠️ 帧率过低,存在卡顿风险');
}
frames = 0;
lastTime = timestamp;
}
requestAnimationFrame(checkFPS);
}
requestAnimationFrame(checkFPS);
我的习惯:在开发环境里把这个监控放在页面角落,用一个小浮层显示实时帧率。一旦掉到 30fps 以下,我就知道该查代码了。
1.6 总结一下这一章的核心
卡顿的本质,说白了就是一帧没干完该干的活。60fps 是目标,16ms 是底线。你写的每一行 JS、每一个 CSS 动画、每一次 DOM 操作,都在跟这个 16ms 赛跑。
记住三句话:
- 用户感知 = 帧率 —— 帧率掉到 30 以下,用户就会觉得卡
- 帧率 = 16ms 预算 —— 超过预算就是掉帧
- 预算分配 = 渲染流水线 —— 任何一个阶段超支,整帧报废