技术
把一帧压进 16.7 毫秒
3 分钟
60fps = 每帧 16.7ms。浏览器自己的合成、样式计算要吃掉 3–5ms,留给你的实际只有 10ms 左右。
预算这么紧,就得像做穷游一样精打细算。
#一、先分清三条流水线
JS → Style → Layout → Paint → Composite
动画只走最后一步(Composite)时,成本几乎为零,因为它在 GPU 上跑,甚至不占主线程。
- 动
transform/opacity→ 只 Composite ✅ - 动
width/left→ 从 Layout 重跑 ❌ - 动
background-color→ 从 Paint 重跑 ⚠️
#二、WebGL 的三个省法
本站背景是一个实时光线步进的流体,但它在低端机上也不掉帧,靠的是:
1. 降低渲染分辨率
const scale = 0.55;
canvas.width = innerWidth * devicePixelRatio * scale;
canvas.height = innerHeight * devicePixelRatio * scale;
分辨率降 45%,像素着色器的调用次数降到 30%。而背景本来就是模糊的,肉眼无损。
2. 自适应降级
统计最近 40 帧的平均耗时,超过 24ms 就把 scale 再降一档,同时减少步进次数。永远不要假设用户的设备和你的一样好。
3. 不可见就不算
document.addEventListener('visibilitychange', () => {
running = !document.hidden;
});
标签页切走还在满速渲染的网站,是在偷用户的电池。
#三、DOM 侧的三个省法
- 批量读写分离:先集中读所有
getBoundingClientRect,再集中写样式。混着来会触发强制同步布局(layout thrashing)。 - IntersectionObserver 代替 scroll 监听:滚动事件一秒能打上百次,观察器由浏览器调度,几乎免费。
content-visibility: auto:长列表视口外的元素跳过渲染,一行 CSS 换几十毫秒。
#四、真正的性能感知
用户感知到的"快",有一半不是真快,是响应及时。
100ms 内有反馈 = 即时;1s 内完成 = 流畅;超过 1s 必须有进度指示。
所以哪怕数据还没回来,按钮也要立刻有按下的形变。先给反馈,再给结果。