1、卡顿的本质:从用户感知到帧率分析
页面切换卡顿,说白了就是用户觉得「不跟手」。我做了这么多年前端性能优化,发现很多人一上来就扎进代码细节,却忽略了最根本的问题——我们到底在优化什么?
今天咱们就从用户感知出发,一路挖到帧率分析。把这个根儿捋顺了,后面的优化才有方向。
1.1 用户感知:卡顿的「体感」从哪来?
用户说「卡」,其实是在说两件事:
- 响应慢:点了个按钮,页面半天没反应
- 画面不流畅:切换动画一顿一顿的,像幻灯片
我记得有一次做移动端 H5 项目,用户反馈说「页面像在游泳」。后来一查,是路由切换时主线程被 JS 计算堵死了。用户感知到的,就是那几百毫秒的「空白期」。
核心结论:用户感知到的卡顿,本质上是视觉反馈与用户操作之间的时间差。这个时间差超过 100ms,人就会觉得「不对劲」;超过 300ms,基本就判定为「卡」了。
1.2 帧率:衡量流畅度的硬指标
为什么是 60fps?你想想看,人眼对画面变化的感知有个临界点。每秒 60 帧,每帧约 16.67ms,这个节奏下画面看起来是连续的。
我习惯把帧率比作「心跳」。正常心跳是 60 次/分钟,页面正常心跳就是 60fps。一旦掉到 30fps,用户立刻就能感觉到「心律不齐」。
| 帧率范围 | 用户感知 | 典型场景 |
|---|---|---|
| 60fps(16.67ms/帧) | 流畅,无感知 | 原生应用、优秀 Web 应用 |
| 30-60fps | 偶尔轻微卡顿 | 复杂列表滚动、动画密集页面 |
| 15-30fps | 明显卡顿,体验差 | 低端设备、未优化的大页面 |
| <15fps | 几乎不可用 | 内存泄漏、主线程长时间阻塞 |
一个小技巧:Chrome DevTools 的 Performance 面板里,把帧率显示打开。看到红色长条就说明掉帧了。我每次做性能评审,第一件事就是看帧率曲线。
1.3 页面切换为什么会卡?
页面切换不是简单的「隐藏 A 显示 B」。浏览器要干一堆活:
- 卸载旧页面:触发 beforeunload、清理定时器、解绑事件
- 加载新页面:请求数据、解析 HTML/CSS/JS
- 渲染新页面:构建 DOM 树、计算样式、布局、绘制、合成
- 执行动画:过渡效果、路由动画
这些步骤里,任何一个环节超过 16.67ms,就会造成掉帧。我遇到过最典型的情况——某个页面在 componentWillUnmount 里做了大量数据清理,直接把下一帧给拖没了。
避坑指南:我曾经在一个 SPA 项目里,发现路由切换时有个诡异的 200ms 空白。查了半天,是某个组件在卸载时同步执行了 localStorage 的批量写入。嗯,这个操作在主线程上阻塞了渲染。后来改成异步 + requestIdleCallback,问题就解决了。
1.4 帧的生命周期:一帧里发生了什么?
要理解卡顿,就得知道一帧里浏览器在忙什么。我画了张图,你看完就明白了。
你看,一帧里要做这么多事。其中任何一个环节超时,都会导致浏览器无法在 16.67ms 内完成渲染,下一帧就被跳过了。这就是掉帧。
1.5 卡顿的三大元凶
根据我的经验,页面切换卡顿 90% 的情况可以归到这三类:
- 主线程阻塞:JS 执行时间过长,比如大量计算、同步网络请求、复杂的 DOM 操作。我见过有人把 10 万条数据的排序放在路由切换时做,直接卡死 2 秒。
- 强制回流:在布局完成后又去读/写 DOM 的几何属性(比如
offsetHeight、getBoundingClientRect),导致浏览器被迫重新布局。嗯,这个坑我踩过不止一次。 - 渲染管线过长:页面 DOM 层级太深、CSS 选择器太复杂、图层过多。尤其是移动端低端设备,GPU 性能有限,合成阶段很容易超时。
记住这个公式:
卡顿 = 主线程忙碌 + 渲染管线超时 + 帧率下降 → 用户感知延迟
1.6 如何量化卡顿?
光说「卡」不够,得用数据说话。我常用的几个指标:
| 指标 | 含义 | 可接受阈值 |
|---|---|---|
| FPS | 每秒帧数 | ≥50fps 为良好,≥30fps 为可接受 |
| Long Task | 主线程上超过 50ms 的任务 | 每 10 秒内不超过 3 个 |
| TTI (Time to Interactive) | 页面可交互时间 | ≤3.5s(移动端) |
| FCP (First Contentful Paint) | 首次内容绘制 | ≤1.8s |
我个人习惯在开发环境打开 Chrome 的「Rendering」面板,勾选「FPS Meter」和「Layer Borders」。这样能实时看到帧率和图层情况。发现问题后,再用 Performance 面板录制一段操作,逐帧分析。
一个小工具:用 requestAnimationFrame 写个简单的帧率监控,打印到控制台。我经常在项目里加这么一段,上线前再删掉。代码很简单:
let lastTime = performance.now();
let frames = 0;
function checkFPS(time) {
frames++;
if (time - lastTime >= 1000) {
console.log(`当前帧率: ${frames} fps`);
frames = 0;
lastTime = time;
}
requestAnimationFrame(checkFPS);
}
requestAnimationFrame(checkFPS);
1.7 本章小结
卡顿的本质,说白了就是帧率低于用户感知阈值。而页面切换卡顿,是因为切换过程中触发了大量计算和渲染工作,导致主线程超负荷运转。
记住几个关键数字:
- 16.67ms —— 一帧的生死线
- 50ms —— Long Task 的警戒线
- 100ms —— 用户感知的临界点
后面我们会一步步拆解,怎么把这几个数字压下来。嗯,先从「帧率分析」这个基本功开始。