一次真实项目中的前端性能优化实战经验分享
先看效果,再看代码
上周上线一个数据看板页,用户反馈“点按钮卡顿”“下拉半天没反应”。我打开 Performance 面板一录,主线程直接飙到 120ms 帧耗——不是动画卡,是点击后 JS 处理逻辑拖垮了整个交互。查了一圈,罪魁祸首居然是 useEffect 里反复调用的 setState + 没加防抖的 window.addEventListener('resize'),再加上一个没 memo 的 map 渲染组件。不是框架不行,是我写得太糙。
今天就直接甩出我在项目里**正在用、已上线、没翻车**的几招性能优化实操,不讲大道理,只说“怎么改、为什么这么改、改完啥效果”。
这个场景最好用:列表渲染卡顿?别急着换虚拟滚动
很多同学一看到长列表就想着上 react-window,但其实 90% 的情况,只是忘了加 React.memo 和 useCallback。我们有个设备监控页,展示 300+ 行实时状态,原始写法:
function DeviceItem({ device }) {
return (
<div className="device-row">
<span>{device.id}</span>
<span>{device.status}</span>
<button onClick={() => updateStatus(device.id)}>更新</button>
</div>
);
}
function DeviceList({ devices }) {
return (
<div className="device-list">
{devices.map(device => (
<DeviceItem key={device.id} device={device} />
))}
</div>
);
}
问题在哪?每次父组件重渲染(比如顶部搜索框输入),哪怕 devices 数组没变,DeviceItem 也会全部重 render —— 因为 onClick 是内联函数,每次都是新引用,React.memo 直接失效。
亲测有效改法:
const DeviceItem = React.memo(function DeviceItem({ device, onUpdate }) {
return (
<div className="device-row">
<span>{device.id}</span>
<span>{device.status}</span>
<button onClick={() => onUpdate(device.id)}>更新</button>
</div>
);
});
function DeviceList({ devices }) {
const handleUpdate = useCallback((id) => {
// 实际业务逻辑
updateStatus(id);
}, []);
return (
<div className="device-list">
{devices.map(device => (
<DeviceItem
key={device.id}
device={device}
onUpdate={handleUpdate}
/>
))}
</div>
);
}
就两处改动:① 把 onClick 提成 onUpdate props;② 用 useCallback 包一层。实测首屏渲染时间从 86ms 降到 22ms,滚动也顺了。注意:useCallback 的依赖数组必须写全,漏掉 updateStatus 就会闭包旧值——这里我踩过坑,折腾半天发现状态没更新,最后 console.log 闭包才发现。
踩坑提醒:这三点一定注意
- useMemo 不是万能的,别滥用:我之前在表格列配置里对整个 columns 数组
useMemo,结果发现每次 filter 变化都触发重计算,反而更慢。后来改成只 memo 单个 column renderer 函数,效果立竿见影。 - resize 事件不节流,页面直接变幻灯片:某个仪表盘页用了
useEffect(() => { window.addEventListener('resize', handler) }),但没加防抖。用户缩放浏览器窗口时,handler被调用上百次,CPU 占用瞬间 95%。现在统一用这个 hook:
function useDebouncedResize(callback, delay = 150) {
useEffect(() => {
const handleResize = () => {
clearTimeout(handleResize.timer);
handleResize.timer = setTimeout(callback, delay);
};
handleResize.timer = null;
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(handleResize.timer);
};
}, [callback, delay]);
}
然后在组件里:useDebouncedResize(() => updateLayout(), 200) —— 简单粗暴,亲测有效。
- fetch 请求别堆在 useEffect 里裸奔:有个页面同时发了 5 个
fetch('https://jztheme.com/api/xxx'),全是独立 useEffect,没做并发控制也没 loading 状态管理,网络差的时候 UI 完全无响应。后来改用 Promise.all + 自定义 loading 状态,再加个 abortController 防止组件卸载后还更新 state(又一个我踩过好几次坑的地方):
function useBatchData() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
Promise.all([
fetch('https://jztheme.com/api/status', { signal: controller.signal }),
fetch('https://jztheme.com/api/metrics', { signal: controller.signal }),
fetch('https://jztheme.com/api/alerts', { signal: controller.signal })
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([status, metrics, alerts]) => {
setData({ status, metrics, alerts });
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('Batch fetch failed:', err);
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, []);
return { data, loading };
}
高级技巧:用 CSS containment 偷懒优化渲染
这个可能很多人没听过,但它真香。我们有个复杂表单页,里面嵌了 4 个独立子模块(每个都有自己的状态和动画),但它们之间完全不通信。以前每次改其中一个字段,整个表单树都 re-render。后来给每个子模块外层加了:
.module-container {
contain: layout style paint;
}
效果?Chrome DevTools 的 Rendering 面板里,“Paint” 区域明显缩小,滚动时 GPU 占用下降 30%。原理很简单:contain: layout style paint 告诉浏览器“这块区域的内容变化不会影响外部布局”,于是浏览器可以跳过很多计算。不是所有场景都适用(比如模块里有 position: fixed 元素就可能失效),但只要你的模块确实是“独立盒子”,建议直接用这种方式。
最后说句实在话
性能优化没有银弹。我上线的那个看板页,加了 memo、节流、containment 后,Lighthouse 性能分从 42 升到 78,但还有两个小问题:① 切换 Tab 时首次加载稍慢(懒加载 chunk 还没预取);② 某个图表库内部 setState 过频,暂时没动它——因为改它的成本远高于收益,目前用户没投诉,我就先放着。
技术选型也好、优化策略也罢,最终要回归到“用户感知”和“投入产出比”。有些优化写起来爽,但真实世界里根本测不出差别;有些 bug 看似严重,其实用户根本没注意到。
这个技巧的拓展用法还有很多,后续会继续分享这类博客。比如:如何用自定义 Hook 统一管理多个异步请求的状态边界;怎么在 SSR 场景下避免 hydration mismatch 导致的重复渲染;还有——怎么说服产品经理别在首页加自动轮播的 10 张高清 Banner 图(别笑,真有)。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论