你刚写了个酷炫的滚动动画,结果一滑,帧率掉到10帧,电脑风扇狂转,用户直接关掉网页。今天我们不聊首屏,专治“运行时卡顿”——滚动、动画、输入框打字都能卡成狗。5招下去,让你的页面像吃了德芙,纵享丝滑。
前言
你有没有这种体验:滑动页面,感觉像在拖拽一块湿水泥;鼠标滚轮滚一下,页面半秒后才动;输入框打字,字母一个一个蹦出来。这就是运行时性能差——不是加载慢,而是交互不流畅。
原因通常是:重排重绘太频繁、JS执行时间太长、动画没用GPU。今天我们就来逐一击破,让你写出60帧满跑的页面。
一、帧率是怎么回事?60帧才是丝滑
浏览器理想刷新率是60fps,也就是每16.6毫秒要渲染一帧。如果JS任务或渲染任务超过这个时间,就会丢帧,用户就感觉“卡”。
Chrome DevTools → Performance 录制,看帧率条,红色就是掉帧了。我们的目标:每帧任务控制在10ms以内,留出余量。
二、第1招:用transform和opacity做动画,别动left/top
最经典的性能优化。修改left、top、width、margin会触发重排(Layout),修改颜色、背景会触发重绘(Paint),而修改transform和opacity只触发合成(Composite),直接走GPU,完全不卡。
差代码:
.box {
transition: left 0.3s;
left: 0;
}
.box.active {
left: 100px;
}
好代码:
.box {
transition: transform 0.3s;
transform: translateX(0);
}
.box.active {
transform: translateX(100px);
}
记住:能用transform绝不用left,能用opacity绝不用visibility。
三、第2招:滚动事件用passive和requestAnimationFrame
滚动时触发scroll事件,如果你在里面做复杂操作,浏览器会等你的代码执行完才滚动,导致卡顿。
解决方案1:passive: true
告诉浏览器:我不会调用preventDefault(),你可以直接滚动。
window.addEventListener('scroll', handler, { passive: true });
解决方案2:用requestAnimationFrame节流
滚动事件触发频率很高,不需要每一帧都处理。用requestAnimationFrame保证只在浏览器要渲染时才执行。
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
// 做滚动相关操作
ticking = false;
});
ticking = true;
}
});
四、第3招:输入框防抖,别每敲一个字都发请求
搜索框实时搜索,用户每打一个字母就发请求,不仅卡,还把服务器打爆。
防抖(debounce):用户停止输入300ms后才执行。
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((keyword) => {
fetch('/search?q=' + keyword);
}, 300);
input.addEventListener('input', (e) => search(e.target.value));
五、第4招:虚拟列表,一万条数据也不怕
渲染长列表(比如聊天记录、商品列表)时,一次性生成所有DOM节点会导致页面卡死。虚拟列表只渲染可视区域内的几条,滚动时动态替换。
实现思路:监听滚动,计算当前显示哪些索引,只创建这些DOM。推荐直接用库:react-window、vue-virtual-scroller。
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => <div style={style}>行 {index}</div>;
<List height={400} itemCount={10000} itemSize={35}>
{Row}
</List>
瞬间渲染一万条,丝滑。
六、第5招:Web Worker,把重活丢到后台
复杂计算(比如数据加密、图像处理、大量数据排序)会阻塞主线程,导致页面无法交互。用Web Worker在后台线程执行,完事通知主线程。
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
console.log('计算结果', e.data);
};
注意:Worker里不能操作DOM,只能做计算。
七、额外绝招:避免强制同步布局
当你在JS里读取布局属性(offsetTop、clientWidth等),又紧接着修改样式,浏览器会强制同步重排,非常卡。
// 坏
boxes.forEach(box => {
box.style.width = box.offsetWidth + 'px'; // 读,触发重排
});
// 好
const widths = boxes.map(box => box.offsetWidth); // 先全读
boxes.forEach((box, i) => {
box.style.width = widths[i] + 'px'; // 再全写
});
读写分离,批量操作。
八、工具检测:Performance面板使用技巧
- 录制一段操作,看火焰图里长任务(Long Task)——超过50ms的任务会标记为红色。
- 勾选“Screenshots”能看到卡顿时的画面。
- 看“Summary”标签,如果“Layout”或“Paint”占比高,说明需要减少重排重绘。
九、总结:运行时优化口诀
- 动画用
transform,不用left/top。 - 滚动加
passive,事件用rAF。 - 输入做防抖,长列表用虚拟。
- 重计算丢Worker,读写要分离。
优化完,你再滑动页面,就像摸到丝绸一样顺滑。用户会惊讶:“这网站怎么这么快?”
如果你觉得今天的“丝滑课”够流畅,点个赞让更多人看到。明天我们聊聊前端工程化——从脚手架到自动化部署,让你一键发布,告别手动上传FTP。我们明天见!