name: react-performance description: React 网站应用的性能优化。在优化渲染、实现虚拟化、记忆组件或调试性能问题时使用。
React 性能(Web)
问题陈述
React 性能问题通常源于不必要的重新渲染、未优化的列表和主线程上的昂贵计算。理解 React 的渲染行为是构建高性能应用的关键。
模式:记忆化
useMemo - 昂贵计算
// ✅ 正确:记忆化昂贵计算
const sortedAndFilteredItems = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => b.score - a.score)
.slice(0, 100);
}, [items]);
// ❌ 错误:每次渲染重新计算
const sortedAndFilteredItems = items
.filter(item => item.active)
.sort((a, b) => b.score - a.score);
// ❌ 错误:记忆化简单访问(开销 > 好处)
const userName = useMemo(() => user.name, [user.name]);
何时使用 useMemo:
- 数组转换(filter, sort, map 链)
- 传递给记忆化子组件的对象创建
- O(n) 或更高复杂度的计算
useCallback - 稳定的函数引用
// ✅ 正确:子组件属性的稳定回调
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
// 传递给记忆化子组件
<MemoizedItem onClick={handleClick} />
// ❌ 错误:useCallback 与不稳定依赖
const handleClick = useCallback((id: string) => {
doSomething(unstableObject); // unstableObject 每次渲染都变化
}, [unstableObject]); // 适得其反
何时使用 useCallback:
- 传递给记忆化子组件的回调
- 依赖数组中的回调
- 会导致子组件重新渲染的事件处理程序
模式:React.memo
// 包装接收稳定属性的组件
const ItemCard = memo(function ItemCard({
item,
onSelect
}: Props) {
return (
<div onClick={() => onSelect(item.id)}>
<h3>{item.name}</h3>
<p>{item.price}</p>
</div>
);
});
// 复杂属性的自定义比较
const ItemCard = memo(
function ItemCard({ item, onSelect }: Props) {
// ...
},
(prevProps, nextProps) => {
// 返回 true 如果属性相等(跳过重新渲染)
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.price === nextProps.item.price
);
}
);
何时使用 React.memo:
- 列表项组件
- 接收稳定原始属性的组件
- 频繁渲染但很少变化的组件
何时不使用:
- 总是接收新属性的组件
- 简单组件(开销 > 好处)
- 根级页面
模式:列表虚拟化
对于长列表,使用 react-window 或 react-virtualized 只渲染可见项。
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<ItemCard item={items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={80}
>
{Row}
</FixedSizeList>
);
}
// 可变高度项
import { VariableSizeList } from 'react-window';
function VariableList({ items }: { items: Item[] }) {
const getItemSize = (index: number) => {
return items[index].expanded ? 200 : 80;
};
return (
<VariableSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
何时虚拟化:
- 有 100+ 项的列表
- 复杂的项组件
- 有许多子项的可滚动容器
模式:Zustand 选择器优化
问题: 选择整个存储会导致任何状态变化时重新渲染。
// ❌ 错误:在任何存储变化时重新渲染
const store = useAppStore();
// 或
const { items, loading, filters, ... } = useAppStore();
// ✅ 正确:仅在选定值变化时重新渲染
const items = useAppStore((s) => s.items);
const loading = useAppStore((s) => s.loading);
// ✅ 正确:多个值与浅比较
import { useShallow } from 'zustand/react/shallow';
const { items, loading } = useAppStore(
useShallow((s) => ({
items: s.items,
loading: s.loading
}))
);
模式:避免重新渲染
对象/数组稳定性
// ❌ 错误:每次渲染新对象
<ChildComponent style={{ padding: 10 }} />
<ChildComponent config={{ enabled: true }} />
// ✅ 正确:稳定引用
const style = useMemo(() => ({ padding: 10 }), []);
const config = useMemo(() => ({ enabled: true }), []);
<ChildComponent style={style} />
<ChildComponent config={config} />
// ✅ 正确:或在组件外定义
const style = { padding: 10 };
function Parent() {
return <ChildComponent style={style} />;
}
子组件稳定性
// ❌ 错误:内联函数每次渲染创建新元素
<Parent>
{() => <Child />}
</Parent>
// ✅ 正确:稳定元素
const child = useMemo(() => <Child />, [deps]);
<Parent>{child}</Parent>
模式:代码拆分
import { lazy, Suspense } from 'react';
// 懒加载组件
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// 命名导出
const Dashboard = lazy(() =>
import('./pages/Dashboard').then(module => ({
default: module.Dashboard
}))
);
模式:防抖和节流
import { useMemo } from 'react';
import { debounce, throttle } from 'lodash-es';
// 防抖 - 等待用户停止输入
function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
const debouncedSearch = useMemo(
() => debounce(onSearch, 300),
[onSearch]
);
return (
<input
type="text"
onChange={(e) => debouncedSearch(e.target.value)}
/>
);
}
// 节流 - 限制函数运行频率
function InfiniteScroll({ onLoadMore }: { onLoadMore: () => void }) {
const throttledLoad = useMemo(
() => throttle(onLoadMore, 1000),
[onLoadMore]
);
useEffect(() => {
const handleScroll = () => {
if (nearBottom()) {
throttledLoad();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [throttledLoad]);
return <div>...</div>;
}
模式:图片优化
// 懒加载图片
<img
src={imageUrl}
loading="lazy"
alt="Description"
/>
// 使用 intersection observer 获得更多控制
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" />
)}
</div>
);
}
// 如果使用 Next.js 的 Next.js 图像组件
import Image from 'next/image';
<Image
src={imageUrl}
alt="Description"
width={400}
height={300}
placeholder="blur"
blurDataURL={blurHash}
/>
模式:Web Workers 用于繁重计算
// worker.ts
self.onmessage = (e: MessageEvent<{ data: number[] }>) => {
const result = heavyComputation(e.data.data);
self.postMessage(result);
};
// 组件
function DataProcessor({ data }: { data: number[] }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.onmessage = (e) => {
setResult(e.data);
};
worker.postMessage({ data });
return () => worker.terminate();
}, [data]);
return result ? <Results data={result} /> : <Loading />;
}
模式:检测重新渲染
React DevTools Profiler
- 打开 React DevTools
- 转到 Profiler 标签
- 点击记录,交互,停止
- 查看 “Flamegraph” 以获取渲染时间
- 查找不必要渲染的组件
why-did-you-render
// 开发环境设置
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// 标记特定组件进行跟踪
ItemCard.whyDidYouRender = true;
控制台日志
// 快速检查重新渲染
function ItemCard({ item }: Props) {
console.log('ItemCard render:', item.id);
// ...
}
性能清单
发布前:
- [ ] 长列表已虚拟化
- [ ] 列表项已使用
React.memo记忆化 - [ ] 传递给项的回调使用
useCallback - [ ] Zustand 选择器是特定的(不是整个存储)
- [ ] 图片使用懒加载
- [ ] 重路由已代码拆分
- [ ] 没有内联对象/函数属性传递给记忆化子组件
- [ ] Profiler 显示没有不必要的重新渲染
常见问题
| 问题 | 解决方案 |
|---|---|
| 列表滚动延迟 | 虚拟化列表,记忆化项 |
| 组件重新渲染太频繁 | 检查选择器特定性,记忆化属性 |
| 初始渲染缓慢 | 代码拆分,减少包大小 |
| 内存增长 | 检查事件侦听器清理,状态累积 |
| 交互时 UI 冻结 | 将计算移动到 Web Worker 或延迟 |
与其他技能的关系
- react-zustand-patterns: 选择器优化模式
- react-async-patterns: 适当的异步处理防止重新渲染循环