前端
2024年6月15日
20 分钟阅读
React 性能优化实战:从 Profiler 到代码分割的完整指南
React 性能优化实战:从 Profiler 到代码分割的完整指南
性能优化不是猜测,而是基于数据的决策。本文将带你从性能测量开始,深入探讨 React 应用的性能优化策略和实战技巧。
性能测量:React DevTools Profiler
使用 Profiler API
React 提供了 Profiler 组件来测量渲染性能:
import { Profiler } from 'react';
function onRenderCallback(
id, // 被测量的组件树 ID
phase, // "mount" 或 "update"
actualDuration, // 本次渲染花费的时间(ms)
baseDuration, // 不使用 memo 时估算的渲染时间
startTime, // React 开始渲染的时间戳
commitTime // React 提交更新的时间戳
) {
console.log('Render:', {
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<ExpensiveComponent />
</Profiler>
);
}生产环境性能监控
// 创建性能监控 Hook
function usePerformanceMonitor(componentName: string) {
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'measure') {
// 发送到监控平台
analytics.track('render_performance', {
component: componentName,
duration: entry.duration,
timestamp: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['measure'] });
return () => observer.disconnect();
}
}, [componentName]);
}
// 使用示例
function MyComponent() {
usePerformanceMonitor('MyComponent');
performance.mark('MyComponent-start');
// 组件逻辑...
useEffect(() => {
performance.mark('MyComponent-end');
performance.measure('MyComponent', 'MyComponent-start', 'MyComponent-end');
});
}组件渲染优化
React.memo 的正确使用
// ❌ 错误使用:props 总是变化
const ExpensiveComponent = React.memo(({ items }) => {
return items.map(item => <Item key={item.id} item={item} />);
});
function Parent() {
const [count, setCount] = useState(0);
const items = [{ id: 1, name: 'Item 1' }];
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* items 每次都是新对象,memo 无效 */}
<ExpensiveComponent items={items} />
</>
);
}
// ✅ 正确使用:稳定 props
const ExpensiveComponent = React.memo(({ items }) => {
return items.map(item => <Item key={item.id} item={item} />);
}, (prevProps, nextProps) => {
// 自定义比较函数
if (prevProps.items.length !== nextProps.items.length) {
return false; // props 不同,需要重新渲染
}
return prevProps.items.every((item, index) =>
item.id === nextProps.items[index].id
);
});
function Parent() {
const [count, setCount] = useState(0);
const items = useMemo(() => [{ id: 1, name: 'Item 1' }], []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveComponent items={items} />
</>
);
}子组件提取策略
// ❌ 问题:每次渲染都创建新的子组件
function Parent({ data }) {
const [filter, setFilter] = useState('');
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{data.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onClick={() => handleClick(item.id)} // 新函数
/>
))}
</div>
);
}
// ✅ 优化:提取子组件,稳定引用
const ExpensiveItem = React.memo(({ item, onClick }) => {
return <div onClick={onClick}>{item.name}</div>;
});
function Parent({ data }) {
const [filter, setFilter] = useState('');
// 使用 useCallback 稳定函数引用
const handleClick = useCallback((id: string) => {
// 处理点击
}, []);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{data.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onClick={handleClick}
/>
))}
</div>
);
}
// ✅ 进一步优化:分离列表组件
const ItemList = React.memo(({ items, onItemClick }) => {
return (
<>
{items.map(item => (
<ExpensiveItem
key={item.id}
item={item}
onClick={onItemClick}
/>
))}
</>
);
});
function Parent({ data }) {
const [filter, setFilter] = useState('');
const filteredData = useMemo(
() => data.filter(item => item.name.includes(filter)),
[data, filter]
);
const handleClick = useCallback((id: string) => {
// 处理点击
}, []);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<ItemList items={filteredData} onItemClick={handleClick} />
</div>
);
}虚拟列表优化
对于长列表,虚拟滚动是必需的:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // 估算每个项目高度
overscan: 5, // 渲染额外的项目以平滑滚动
});
return (
<div
ref={parentRef}
style={{ height: '400px', overflow: 'auto' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}代码分割与懒加载
路由级别的代码分割
// 使用 React.lazy 进行代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Blog = lazy(() => import('./pages/Blog'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/blog" element={<Blog />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}组件级别的懒加载
// 条件加载重型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>显示图表</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
// 使用 Intersection Observer 实现视图内懒加载
function LazyImage({ src, alt }) {
const imgRef = useRef<HTMLImageElement>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isInView ? src : undefined}
alt={alt}
loading="lazy"
/>
);
}动态导入优化
// 预加载组件(鼠标悬停时)
function LinkWithPreload({ to, children }) {
const preloadComponent = () => {
// 预加载但不同步执行
import(`./pages/${to}`);
};
return (
<Link
to={to}
onMouseEnter={preloadComponent}
>
{children}
</Link>
);
}
// 使用动态导入加载第三方库
async function loadHeavyLibrary() {
const module = await import('heavy-library');
return module.default;
}
function Component() {
const [Library, setLibrary] = useState(null);
useEffect(() => {
loadHeavyLibrary().then(setLibrary);
}, []);
if (!Library) return <Loading />;
return <Library />;
}状态管理优化
状态拆分策略
// ❌ 问题:所有状态放在一起
function Component() {
const [state, setState] = useState({
user: null,
posts: [],
settings: {},
ui: { modal: false, sidebar: true }
});
// 更新 UI 状态会导致所有组件重新渲染
const toggleModal = () => setState(s => ({
...s,
ui: { ...s.ui, modal: !s.ui.modal }
}));
}
// ✅ 优化:按更新频率拆分状态
function Component() {
// 不常变化的数据
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// 频繁变化的 UI 状态
const [modal, setModal] = useState(false);
const [sidebar, setSidebar] = useState(true);
const toggleModal = () => setModal(v => !v);
}Context 性能优化
// ❌ 问题:所有状态在一个 Context
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [locale, setLocale] = useState('zh');
// 任何状态变化都会导致所有消费者重新渲染
return (
<AppContext.Provider value={{ user, theme, locale, setUser, setTheme, setLocale }}>
{children}
</AppContext.Provider>
);
}
// ✅ 优化:拆分 Context
const UserContext = createContext();
const ThemeContext = createContext();
const LocaleContext = createContext();
// 或者使用选择器模式
function createSelectorContext<T>() {
const Context = createContext<T | null>(null);
const Provider = ({ value, children }) => (
<Context.Provider value={value}>{children}</Context.Provider>
);
const useSelector = <R,>(selector: (value: T) => R) => {
const value = useContext(Context);
if (value === null) throw new Error('Provider missing');
// 只在选择的值变化时更新
return useMemo(() => selector(value), [value, selector]);
};
return { Provider, useSelector };
}渲染性能优化技巧
避免不必要的重新渲染
// 使用 React.memo 和 useMemo 组合
const Item = React.memo(({ id, name, onClick }) => {
console.log(`Rendering item ${id}`);
return <div onClick={onClick}>{name}</div>;
});
function List({ items }) {
const memoizedItems = useMemo(
() => items.map(item => ({
id: item.id,
name: item.name,
})),
[items]
);
const handleClick = useCallback((id: string) => {
// 处理点击
}, []);
return (
<>
{memoizedItems.map(item => (
<Item
key={item.id}
{...item}
onClick={() => handleClick(item.id)}
/>
))}
</>
);
}使用 useDeferredValue 延迟更新
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ query }) {
// 延迟更新,保持 UI 响应
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
return expensiveSearch(deferredQuery);
}, [deferredQuery]);
// query 变化时立即显示旧结果,后台计算新结果
return <ResultsList results={results} />;
}批量更新优化
// React 18 自动批量更新(包括异步操作)
function Component() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// React 18: 这两个更新会被批量处理,只渲染一次
fetch('/api').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
}
}
// 手动批量更新
import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
setCount(c => c + 1);
setFlag(f => !f);
setName('New Name');
});内存泄漏预防
// 清理订阅和定时器
function Component() {
useEffect(() => {
const subscription = subscribe();
const timer = setInterval(() => {
// 定时任务
}, 1000);
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, []);
// 清理事件监听器
useEffect(() => {
const handleResize = () => {
// 处理窗口大小变化
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
}
// 避免在已卸载的组件上设置状态
function useSafeState<T>(initialValue: T) {
const [state, setState] = useState(initialValue);
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const safeSetState = useCallback((value: T | ((prev: T) => T)) => {
if (isMountedRef.current) {
setState(value);
}
}, []);
return [state, safeSetState] as const;
}性能优化检查清单
- ✅ 测量性能:使用 React DevTools Profiler 识别瓶颈
- ✅ 组件优化:合理使用 React.memo、useMemo、useCallback
- ✅ 代码分割:路由和组件级别的懒加载
- ✅ 虚拟列表:长列表使用虚拟滚动
- ✅ 状态管理:拆分状态,优化 Context
- ✅ 图片优化:懒加载、WebP 格式、响应式图片
- ✅ Bundle 分析:使用 webpack-bundle-analyzer 分析打包大小
- ✅ 生产构建:启用代码压缩、Tree Shaking
记住:过早优化是万恶之源。先测量,再优化,持续监控。