创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0
性能优化的思维方式
性能优化有一条铁律:先测量,再优化。
很多开发者凭直觉去优化代码,结果往往是:花了大量时间把一些「看起来慢」的地方优化好,但真正的瓶颈根本没碰。
React 的性能问题通常出在三个地方:
- 不必要的重新渲染------组件被重复渲染,但 props/state 根本没变
- 昂贵的计算------每次渲染都在重复执行开销很大的计算
- 资源体积过大------JS 包太大、图片未优化、字体未压缩
优化之前,问问自己:这真的是瓶颈吗?开发模式下的 React 比生产模式慢 10 倍------你看到的卡顿可能只是开发模式的特有现象。
React.memo:阻止不必要的渲染
React 的默认行为是:父组件重新渲染时,所有子组件也重新渲染。这通常不是问题,但当组件树很大时,一次小的状态更新可能导致几十上百个组件重新渲染。
jsx
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Yardon');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* HeavyComponent 的 props 没变,但每次 count 变它都重新渲染 */}
<HeavyComponent name={name} />
</div>
);
}
React.memo 就像一个过滤器:如果 props 没变,就跳过渲染。
jsx
const HeavyComponent = React.memo(function HeavyComponent({ name }) {
// 昂贵的渲染逻辑...
console.log('渲染了');
return <div>{name}</div>;
});
现在 count 无论如何变化,HeavyComponent 都不会重新渲染------因为它的 name prop 没变。
自定义比较函数
默认情况下 React.memo 做浅比较(===)。对于复杂对象,可以传自定义比较函数:
jsx
const areEqual = (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name;
};
const UserCard = React.memo(function UserCard({ user }) {
// 只有 user 的关键字段变化才渲染
}, areEqual);
useMemo 和 useCallback
这两个 Hook 经常被滥用。在 80% 的情况下,你不需要它们。
useMemo:记忆计算结果
jsx
function Dashboard({ transactions }) {
// ❌ 每次渲染都重新计算
const totals = {
income: transactions.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0),
expense: transactions.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0),
};
return <Charts totals={totals} />;
}
function Dashboard({ transactions }) {
// ✅ 只在 transactions 变化时重新计算
const totals = useMemo(() => ({
income: transactions.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0),
expense: transactions.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0),
}), [transactions]);
return <Charts totals={totals} />;
}
useCallback:保持函数引用的稳定性
jsx
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染创建一个新函数
const handleClick = () => {
doSomething(count);
};
// ✅ 只在 count 变化时创建新函数
const handleClick = useCallback(() => {
doSomething(count);
}, [count]);
return <Child onClick={handleClick} />;
}
只有当你把函数传给 React.memo 包装的子组件,或者把函数放进 useEffect 的依赖数组时,useCallback 才有意义。单独使用它不会带来性能提升------它本身也有开销。
列表优化:key 与虚拟滚动
Key 的正确使用
jsx
// ❌ 用 index 做 key(列表元素重排时出问题)
{items.map((item, index) => <li key={index}>{item.name}</li>)}
// ✅ 用唯一标识做 key
{items.map(item => <li key={item.id}>{item.name}</li>)}
虚拟滚动
当列表有成千上万条数据时,一次性渲染所有 DOM 节点会让浏览器崩溃。虚拟滚动只渲染可视区域内的元素。
jsx
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
react-window 只有约 3KB,但能把 10000 条数据的渲染时间从数秒降到毫秒级。
代码分割与 Tree Shaking
动态 import
jsx
// 路由级分割
const Dashboard = lazy(() => import('./pages/Dashboard'));
// 组件级分割
const HeavyChart = lazy(() => import('./components/HeavyChart'));
Tree Shaking
Vite/Webpack 的 Tree Shaking 依赖于 ESM 的静态分析------它能在打包时移除未使用的代码。
jsx
// ❌ lodash 的 CJS 方式不支持 tree-shaking
const _ = require('lodash');
_.sortBy(data, 'name');
// ✅ 按需导入
import sortBy from 'lodash/sortBy';
sortBy(data, 'name');
图片与资源优化
jsx
// srcset 根据屏幕宽度加载不同尺寸的图片
<img
src="/hero-1200w.webp"
srcSet="/hero-400w.webp 400w, /hero-800w.webp 800w, /hero-1200w.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
loading="lazy" // 懒加载
alt="描述"
/>
Profiler:找到真正的瓶颈
jsx
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navbar />
</Profiler>
);
}
| 指标 | 含义 |
|---|---|
| actualDuration | 组件本次渲染的实际耗时 |
| phase | mount(首次)或 update(更新) |
本章小结
| 技术 | 使用场景 |
|---|---|
| React.memo | props 不变的组件免渲染 |
| useMemo | 昂贵的计算、防止子组件不必要的渲染 |
| useCallback | 传递给 memo 组件的函数保持引用稳定 |
| lazy + Suspense | 按需加载页面,减小首屏体积 |
| react-window | 长列表虚拟滚动 |
| Profiler | 定位真正的性能瓶颈 |
下一章是本系列的最后一章------我们聊聊测试与部署,让你的应用从开发环境走向线上。
📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top
📖 本章是「React 从入门到生产 」系列的第 7 章。下一章:测试与部署
🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!