React 性能反模式
为什么需要了解反模式
理解什么是「反模式」比知道什么是「正确做法」更重要。性能优化的第一条原则是:不要优化还没有测量过的代码。
但这不意味着你可以随意写低效代码。了解常见的性能陷阱,可以帮助你:
- 在代码审查中发现潜在问题
- 在调试性能问题时快速定位原因
- 在编写代码时避免常见的坑
本文不是要你变成强迫症式的优化狂,而是帮助你建立对 React 性能的正确直觉。
内联函数作为 Props
问题所在
在 JSX 中直接定义函数是最常见的性能问题:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* 每次渲染都创建新的 onClick 函数 */}
<Child onClick={() => console.log('clicked')} />
{/* 每次渲染都创建新的 handleChange 函数 */}
<Input onChange={e => setValue(e.target.value)} />
{/* 每次渲染都创建新的 renderItem 函数 */}
<List items={items} renderItem={item => <ListItem key={item.id} item={item} />} />
</div>
);
}
为什么这是问题?
即使组件被 React.memo 包装,每次父组件渲染,子组件的 onClick prop 都是一个新的函数引用。React.memo 的浅比较会发现这个变化,导致子组件重新渲染。
解决方案
方案一:useCallback
function Parent() {
const [count, setCount] = useState(0);
const [value, setValue] = useState('');
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
const handleChange = useCallback((e) => {
setValue(e.target.value);
}, []);
const renderItem = useCallback((item) => (
<ListItem key={item.id} item={item} />
), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child onClick={handleClick} />
<Input onChange={handleChange} />
<List items={items} renderItem={renderItem} />
</div>
);
}
方案二:重构组件接收 children
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* children 作为函数时可以接受稳定的引用 */}
<Child>
{() => <ExpensiveComponent />}
</Child>
</div>
);
}
function Child({ children }) {
// children 是稳定的函数引用
return <div>{children()}</div>;
}
在渲染中创建新对象和数组
问题所在
function UserCard({ user }) {
const [liked, setLiked] = useState(false);
return (
<div>
<h1>{user.name}</h1>
{/* 每次渲染都创建新对象 */}
<ProfileCard
user={user}
style={{ color: 'red' }}
className="user-card"
/>
{/* 每次渲染都创建新数组 */}
<Tags tags={['React', 'JavaScript', 'CSS']} />
{/* 每次渲染都创建新函数 */}
<Button onClick={() => setLiked(!liked)}>Like</Button>
</div>
);
}
解决方案
方案一:useMemo
function UserCard({ user }) {
const [liked, setLiked] = useState(false);
const cardStyle = useMemo(() => ({
color: 'red',
}), []);
const cardClassName = useMemo(() => 'user-card', []);
const tags = useMemo(() => ['React', 'JavaScript', 'CSS'], []);
return (
<div>
<h1>{user.name}</h1>
<ProfileCard user={user} style={cardStyle} className={cardClassName} />
<Tags tags={tags} />
<Button onClick={() => setLiked(!liked)}>Like</Button>
</div>
);
}
方案二:将常量移到组件外
// 常量不需要在每次渲染时创建
const DEFAULT_STYLE = { color: 'red' };
const DEFAULT_CLASS = 'user-card';
const DEFAULT_TAGS = ['React', 'JavaScript', 'CSS'];
function UserCard({ user }) {
return (
<div>
<ProfileCard user={user} style={DEFAULT_STYLE} className={DEFAULT_CLASS} />
<Tags tags={DEFAULT_TAGS} />
</div>
);
}
方案三:优化数据结构
function UserCard({ user }) {
return (
<div>
<ProfileCard user={user} />
</div>
);
}
// ProfileCard 内部自己定义样式
function ProfileCard({ user }) {
const style = useMemo(() => ({
color: 'red',
}), []);
return <div style={style}>{user.name}</div>;
}
依赖数组不完整
问题所在
function SearchResults({ query, items }) {
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, []); // 错误!缺少 query 和 items
return <div>{filteredItems.map(item => item.name)}</div>;
}
function UserProfile({ userId }) {
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // 错误!缺少 userId,可能使用过期的 userId
}
解决方案
function SearchResults({ query, items }) {
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [query, items]); // 完整依赖
return <div>{filteredItems.map(item => item.name)}</div>;
}
function UserProfile({ userId }) {
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, [userId]); // 完整依赖
}
无效的 React.memo 使用
问题一:memo 了一个本身就频繁更新的组件
const MemoizedCounter = React.memo(function Counter({ count, onIncrement }) {
console.log('Counter rendered'); // 每次 count 变化都会渲染
return <button onClick={onIncrement}>{count}</button>;
});
function App() {
const [count, setCount] = useState(0);
// count 变化时,这个组件的 memo 毫无意义
return <Counter count={count} onIncrement={() => setCount(c => c + 1)} />;
}
问题二:memo 了一个渲染很快的组件
const MemoizedDiv = React.memo(function Div({ text }) {
return <div>{text}</div>; // 渲染时间 < 0.1ms
});
function App() {
const [text, setText] = useState('Hello');
// memo 的比较开销可能比渲染本身还大
return <MemoizedDiv text={text} />;
}
问题三:在 memo 的比较函数中做了昂贵的计算
const ExpensiveMemo = React.memo(
function ExpensiveComponent({ data }) {
return <div>{data.name}</div>;
},
(prevProps, nextProps) => {
// 深度比较,计算成本很高
return isDeepEqual(prevProps.data, nextProps.data);
}
);
Context 过度使用
问题:单一 Context 包含太多状态
const AppContext = createContext({
user: null,
theme: 'light',
language: 'en',
notifications: [],
cart: [],
// ... 更多状态
});
function App() {
const [state, setState] = useReducer(appReducer, initialState);
// 任何状态变化都会导致所有消费者重新渲染
return (
<AppContext.Provider value={state}>
<App />
</AppContext.Provider>
);
}
解决方案:拆分 Context
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const CartContext = createContext([]);
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<CartContext.Provider value={{ cart, dispatch }}>
<App />
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
在 useEffect 中直接更新状态
问题:setState 导致无限循环
function Component({ items }) {
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
// 每次 filteredItems 变化都会触发这个 effect
// 而 setFilteredItems 又会导致 filteredItems 变化
setFilteredItems(items.filter(item => item.active));
}, [items, filteredItems]); // 错误:filteredItems 在依赖数组中
}
解决方案
function Component({ items }) {
const [filter, setFilter] = useState('');
// 使用 useMemo 而不是 useEffect + setState
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return <List items={filteredItems} />;
}
过度依赖 ref 作为状态容器
问题:ref 的变化不会触发重新渲染
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
// 错误:intervalRef.current 变化不会更新 UI
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// count 更新了,但 intervalRef.current 可能和显示不一致
return <div>Count: {count}</div>;
}
解决方案
function Timer() {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(true);
const intervalRef = useRef(null);
useEffect(() => {
if (!isRunning) {
clearInterval(intervalRef.current);
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, [isRunning]);
return (
<div>
Count: {count}
<button onClick={() => setIsRunning(r => !r)}>
{isRunning ? 'Pause' : 'Resume'}
</button>
</div>
);
}
面试中的表达
面试中聊到性能反模式,通常是在考察你对 React 渲染机制的理解深度:
最常见的性能问题是「过度创建新的函数和对象引用」。每次父组件渲染,如果 props 是新创建的对象或函数,即使内容完全相同,React.memo 的浅比较也会失败。
关于 useEffect 中的 setState,我见过有人写出无限循环的代码。关键是理解 useEffect 的依赖数组——如果你在 effect 中更新了依赖数组中的状态,就会触发无限循环。
Context 过度使用也是个问题。把太多不相关的状态放一个 Context,任何一个变化都会导致所有消费者重新渲染。拆分 Context 是常见的解决方案。
最后,我想强调的是:性能优化要有的放矢。先用 React DevTools Profiler 测量,找到真正的瓶颈,再针对性地优化。过度优化反而增加代码复杂度。
延展阅读
- React Docs: Keeping Components Pure — 组件纯度与性能
- React DevTools: Performance Debugging — 使用 DevTools 调试性能
- Kent C. Dodds: Common React performance mistakes — 常见 React 性能错误
- Overreacted: Before you memo — Dan Abramov 讲解 memo 的使用时机