React性能优化:从“卡成狗”到“丝般顺滑”的5个秘诀
前言
React已经很快了,但如果你不注意细节,它会做很多“无用功”:组件没必要的重渲染、大列表全量渲染、状态更新导致整个页面刷新……这些问题累积起来,再好的电脑也扛不住。
今天我们不聊虚拟DOM原理,直接上代码、上工具,告诉你哪些写法是“性能杀手”,哪些是“救星”。优化完,你的React应用会快得让用户怀疑是不是装了外挂。
一、第1招:用React.memo避免“父动子也动”
React默认:父组件更新,所有子组件都会重新渲染(即使props没变)。这会导致大量浪费。
差代码:
const Child = ({ name }) => {
console.log('Child渲染了');
return <div>{name}</div>;
};
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c+1)}>点击 {count}</button>
<Child name="张三" />
</div>
);
};
每次点击按钮,Child都会重新渲染,但它的name根本没变。
好代码:用React.memo包装子组件。
const Child = React.memo(({ name }) => {
console.log('Child渲染了');
return <div>{name}</div>;
});
现在只有name变化时,Child才会重新渲染。
注意:如果Child接收的props包含函数或对象,需要配合useCallback、useMemo(见第2招)。
二、第2招:用useCallback和useMemo缓存函数和值
在React组件里,每次渲染都会重新创建所有内联函数和对象。即使内容相同,引用也不同,导致React.memo失效。
差代码:
const Parent = () => {
const handleClick = () => console.log('clicked'); // 每次渲染都是新函数
return <Child onClick={handleClick} />;
};
好代码:用useCallback缓存函数。
const Parent = () => {
const handleClick = useCallback(() => console.log('clicked'), []); // 依赖为空,永远不变
return <Child onClick={handleClick} />;
};
对于复杂计算的值(比如过滤大列表),用useMemo缓存结果:
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
三、第3招:虚拟列表,渲染一万条也不怕
直接渲染长列表(比如聊天记录、商品列表)会导致浏览器创建上万个DOM节点,内存爆炸,滚动卡顿。虚拟列表只渲染可视区域内的几条,滚动时动态替换。
不用自己造轮子,用现成库:
-
react-window(轻量) -
react-virtualized(功能全)
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>行 {index}</div>
);
<List
height={400}
itemCount={10000}
itemSize={35}
width={300}
>
{Row}
</List>
一秒渲染一万条,滚动丝滑。
四、第4招:代码分割 + 懒加载,别一次加载所有组件
你的用户访问首页,结果你把后台管理、用户设置、订单详情所有页面的代码都下载了。浪费流量,也拖慢首屏。
用React.lazy + Suspense:
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
每个路由的代码单独打包,只有访问时才加载。
五、第5招:避免内联对象和函数传递
即使不用memo,内联对象也会导致子组件每次接收新对象,触发重渲染。
差代码:
<Child style={{ color: 'red' }} /> // 每次渲染都是新对象
好代码:把对象提取到组件外部或使用useMemo。
const childStyle = { color: 'red' }; // 外部定义,引用不变
<Child style={childStyle} />
六、额外绝招:使用useTransition标记非紧急更新
React 18引入了useTransition,可以把某些更新标记为“低优先级”,让高优先级交互(如输入框打字)更流畅。
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [list, setList] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 紧急更新:更新输入框
startTransition(() => {
// 非紧急更新:过滤大列表
const filtered = hugeList.filter(item => item.includes(value));
setList(filtered);
});
};
这样打字不会卡,列表过滤稍后完成。
七、工具检测:React DevTools Profiler
安装React DevTools,打开Profiler标签,录制一段操作,可以看到每个组件渲染耗时。颜色越黄/红,越需要优化。
八、总结:优化口诀
- 子组件纯展示,包上
React.memo。 - 函数和对象,用
useCallback、useMemo缓存。 - 长列表用虚拟滚动。
- 路由懒加载,按需取。
- 内联对象移出去,引用不变。
- 紧急更新与非紧急分开,用
useTransition。
优化完,你的React应用会快得飞起。用户会惊叹:“这网站怎么比原生App还流畅?”