刚开始我也以为React性能优化是个玄学,不就是加个React.memo、useMemo、useCallback三件套吗?结果第一个正式项目上线,用户反馈页面卡得跟PPT似的,我才发现这玩意儿真不是表面那么简单。今天就从我踩过的三个大坑说起,聊聊React优化里最实用的3个技巧,保证你读完能立刻用上。
先来点真凭实据:我们优化的目标在哪?
别跟我扯“提升用户体验”这种废话,直接上数据:
- 优化前:组件渲染耗时从 120ms 飙到 450ms 时,用户开始察觉卡顿(据团队内部测试)
- 优化后:把关键路径耗时从 3.2秒 砍到 0.8秒,用户反馈满意度提升40%(基于内部问卷调研)
我做的项目中,有一个仪表盘页面,里面有20多个动态图表和实时数据流。刚开始所有组件都用一个Context传状态,结果每次数据更新,整个页面都重渲染。画面是这样的:鼠标挪一下,所有图表闪一下,CPU飙到90%,风扇狂转。
这个设计真的反人类,但当时我还在想“React不是虚拟DOM吗?应该没事吧”——天真!虚拟DOM比对只是减少了真实DOM操作,但组件函数本身的执行、Hooks的重新计算,这些开销一点都没少。
技巧一:把「屎山」Context拆成「瑞士军刀」模式
来看代码。这是最典型的坑爹写法,官方文档其实有提,但文档不够清晰:
“`jsx
import { createContext, useState, useContext } from ‘react’;
// ❌ 反模式:一个Context塞所有状态
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState(‘light’);
const [notifications, setNotifications] = useState([]);
const [dashboardData, setDashboardData] = useState({});
return (
{children}
);
}
// 使用时,哪怕只改theme,所有Consumer都得重渲染
function UserProfile() {
const { user } = useContext(AppContext);
// 这个组件会被theme、notifications的变化影响
return
;
}
“`
为什么要这么写?因为一个Context的value变化时,所有use了这个Context的组件都会重新渲染。你把所有状态绑在一起,就像把所有水管接在一个龙头上,拧一下全屋喷水。
优化方案:拆成多个独立的Context:
“`jsx
import { createContext, useState } from ‘react’;
// ✅ 推荐:每个Context只管自己的事
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationsContext = createContext();
const DashboardContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState(‘light’);
const [notifications, setNotifications] = useState([]);
const [dashboardData, setDashboardData] = useState({});
return (
{children}
);
}
“`
这样改完,改theme只影响用到ThemeContext的组件,UserProfile这种只依赖UserContext的组件完全不受影响。我那个仪表盘页面,改完之后,滚动和交互直接从 卡成狗 变成 丝般顺滑——当然,这是相对说的,具体数据是:渲染次数从每次更新触发40+次减少到只影响相关组件,CPU占用从90%降到30%。
技巧二:useMemo和useCallback不是银弹,乱用反而更慢
另一个坑:我见过有人把所有函数都包上useCallback,把所有计算结果都包上useMemo。官方文档要是能说话,估计会骂:“你们在搞毛啊?”
以下为错误示例,请勿模仿:
“`jsx
// ❌ 过度优化:每个函数都包useCallback,且依赖不稳定导致失效
function ExpensiveList({ items, onItemClick }) {
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]); // 注意:若父组件未用useCallback包裹onItemClick,此依赖仍会每次变化,导致handleClick每次都重建,useCallback白用
const filteredItems = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
return (
- {filteredItems.map(item => (
- handleClick(item.id)}>
{item.name}
))}
);
}
“`
为什么不要这么写?因为useMemo和useCallback本身有内存分配和依赖比对的开销。如果你的计算本来就很简单,比如items.filter(...)几百毫秒就搞定,加上useMemo的依赖比对可能还要几十毫秒,净亏!而且内存引用一直不变,反而导致垃圾回收的压力。
还有个技巧:真正该用useMemo的场景是计算量大的东西,比如处理几千条数据的排序、过滤;而useCallback只在把函数作为props传给子组件时用,并且子组件用了React.memo才有意义。否则就是脱裤子放屁。
看这个正确的用法:
“`jsx
// ✅ 正确:只在需要的地方用,且父组件需配合useCallback或React.memo
function Dashboard() {
const { data } = useContext(DashboardContext);
// 这个计算量很大,所以要缓存
const aggregatedData = useMemo(() => {
return data.reduce((acc, item) => {
// 复杂计算,比如nested loops
return acc;
}, []);
}, [data])