React 性能优化技术
性能优化的原则
性能优化的第一条原则是:不要优化还没有测量过的代码。
但这不意味着盲目编写低效代码。理解 React 的渲染机制和常见的性能瓶颈,可以帮助你在编写代码时做出更好的决策,并在性能问题出现时快速定位。
测量工具
React DevTools Profiler
React DevTools 的 Profiler 面板可以记录每次渲染的信息:
// 在开发环境中使用
import { profiler } from 'react';
const ProfileComponent = ({ data }) => {
profiler.log('Rendered with data:', data);
return <div>{data.name}</div>;
};
Performance API
const PerformanceMark = ({ name, children }) => {
useEffect(() => {
performance.mark(`${name}-start`);
return () => {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
};
}, [name]);
return children;
};
User Timing API
function measureRender(componentName, renderCount) {
performance.getEntriesByName(componentName).forEach(entry => {
console.log(`${componentName} took ${entry.duration}ms`);
});
}
记忆化(Memoization)
React.memo
React.memo 防止组件在 props 没变化时重新渲染:
const MemoizedComponent = React.memo(function Component({ data }) {
return <div>{data.name}</div>;
});
// 或者带自定义比较
const MemoizedComponent = React.memo(
Component,
(prevProps, nextProps) => prevProps.id === nextProps.id
);
useMemo
useMemo 缓存计算结果:
function ExpensiveList({ items, filter }) {
// 只有 items 或 filter 变化时才重新计算
const filteredItems = useMemo(() => {
return items
.filter(item => item.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name));
}, [items, filter]);
return <List items={filteredItems} />;
}
useCallback
useCallback 缓存函数引用:
function Parent() {
const [count, setCount] = useState(0);
// 只有空依赖,函数引用稳定
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedChild onClick={handleClick} count={count} />;
}
懒加载(Lazy Loading)
React.lazy
React.lazy 允许你代码分割组件:
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
);
}
命名导出
const LazyComponent = React.lazy(() =>
import('./HeavyComponent').then(module => ({
default: module.HeavyComponent
}))
);
Suspense 边界
function App() {
return (
<div>
<h1>Main App</h1>
<Suspense fallback={<Spinner />}>
<Route path="/dashboard" component={Dashboard} />
<Route path="/settings" component={Settings} />
</Suspense>
</div>
);
}
预加载
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
// 在需要之前预加载
function App() {
const handleHover = () => {
// 用户悬停时预加载
import('./HeavyComponent');
};
return (
<div onMouseEnter={handleHover}>
<Link to="/heavy">Go to Heavy Page</Link>
</div>
);
}
虚拟列表(Virtualization)
当列表项数量非常大时(如 10000+),即使渲染列表项本身不慢,DOM 节点过多也会导致性能问题。虚拟列表的思路是:只渲染可视区域内的列表项。
虚拟列表原理
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(startIndex, endIndex);
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div key={item.id} style={{ height: itemHeight }}>
<ListItem item={item} />
</div>
))}
</div>
</div>
</div>
);
}
第三方库
- react-window:轻量级虚拟列表库
- react-virtualized:功能更丰富的虚拟列表库
- @tanstack/react-virtual(原 react-virtual):现代、轻量级
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(({ index, size, start }) => (
<div
key={items[index].id}
style={{
position: 'absolute',
top: start,
height: size,
}}
>
<ListItem item={items[index]} />
</div>
))}
</div>
</div>
);
}
防抖和节流
防抖(Debounce)
防抖用于「最后一次触发」的场景:如搜索输入,用户停止输入后才执行搜索。
import { useDebouncedCallback } from 'use-debounce';
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const debouncedSearch = useDebouncedCallback((value) => {
fetchResults(value).then(setResults);
}, 300);
return (
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
}}
/>
<SearchResults results={results} />
</div>
);
}
节流(Throttle)
节流用于「固定频率」的场景:如滚动事件监听、拖拽位置更新。
import { useThrottledCallback } from 'use-throttled-callback';
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const handleScroll = useThrottledCallback(() => {
setScrollY(window.scrollY);
}, 100);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
return <div>Scroll: {scrollY}</div>;
}
在 useEffect 中使用
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function SearchComponent({ filter }) {
const debouncedFilter = useDebounce(filter, 300);
const results = useMemo(() => {
return allItems.filter(item =>
item.name.includes(debouncedFilter)
);
}, [debouncedFilter]);
return <List items={results} />;
}
状态管理优化
状态位置
// 不好:状态放在父组件,导致不必要的重新渲染
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<ExpensiveTree /> {/* count 变化导致整个树重新渲染 */}
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
);
}
// 好:状态放在需要它的组件
function Parent() {
return (
<div>
<ExpensiveTree />
<Counter />
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
状态拆分
// 不好:一个 state 对象包含不相关的值
const [state, setState] = useState({
user: null,
theme: 'light',
notifications: [],
isLoading: false
});
// 好:拆分成独立的状态
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);
渲染优化
避免内联对象和函数
// 不好:每次渲染都创建新对象
function Component() {
return <Child style={{ color: 'red' }} />;
}
// 好:对象定义在组件外或使用 useMemo
const RED_STYLE = { color: 'red' };
function Component() {
return <Child style={RED_STYLE} />;
}
使用 CSS 而不是 JavaScript 动画
// 不好:JavaScript 动画会阻塞主线程
function ExpensiveAnimation() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <div style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }} />;
}
// 好:CSS transition 处理动画
function GoodAnimation() {
return <div className="follows-mouse" />;
}
.follows-mouse {
transition: transform 0.1s ease-out;
}
常见性能问题排查
问题一:组件莫名重新渲染
- 使用 React DevTools Profiler 查看是什么触发了渲染
- 检查 state 是否放在了不必要的父组件
- 检查 props 是否每次都创建新引用
问题二:列表渲染卡顿
- 检查是否使用了稳定的 key(不是 index)
- 考虑虚拟列表(如果列表很长)
- 检查是否有不必要的深层嵌套
问题三:输入响应慢
- 检查是否有 useEffect 中的 setState 导致的无限循环
- 考虑使用 debounce 处理频繁的输入事件
- 检查是否有昂贵的计算在渲染中进行
面试中的表达
面试中聊到性能优化,通常是在考察你的工程判断能力:
性能优化的核心是测量优先。不要猜测性能瓶颈,而是用 React DevTools Profiler 找到真正的问题所在。
常见的优化手段包括:记忆化(React.memo/useMemo/useCallback)避免不必要的渲染;懒加载减少初始 bundle 大小;虚拟列表处理大列表;debounce/throttle 处理高频事件。
但更重要的是理解什么时候不需要优化。简单组件的 memo 比较开销可能比渲染本身还大,这就是过早优化。遵循「先测量,再优化」的原则。
延展阅读
- React DevTools Profiler — 使用 DevTools 分析性能
- React Performance Optimization — Kent C. Dodds 的性能优化文章
- Virtual List Implementation — TanStack Virtual 虚拟列表库
- use-debounce — Debounce Hook 库