如何提高组件的渲染效率?如何避免不必要的 render?
一、为什么需要优化
React 的默认行为是:当组件状态变化时,该组件及其所有子组件都会重新执行 render。对于大多数应用来说这不会有问题,但当组件树很深或 render 计算量较大时,不必要的重新渲染会导致界面卡顿。
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数:{count}</button>
{/* 每次 count 变化,ExpensiveList 都会重新渲染 */}
<ExpensiveList items={staticItems} />
</div>
);
}
优化的核心原则:让没有变化的组件跳过 render,让必须 render 的组件尽快完成。
二、React.memo — 函数组件记忆化
React.memo 是一个高阶组件,它对 props 进行浅比较,如果 props 没有变化则跳过重新渲染:
const ExpensiveList = React.memo(function ExpensiveList({
items,
}: {
items: Item[];
}) {
console.log('ExpensiveList render');
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
也可以传入自定义比较函数,对特定字段进行精确比较。
memo 的失效场景
React.memo 进行的是浅比较,以下情况会导致它失效:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* 每次渲染都创建新对象,memo 浅比较总是不等 */}
<MemoChild style={{ color: 'red' }} />
{/* 每次渲染都创建新函数引用 */}
<MemoChild onClick={() => console.log('click')} />
{/* 每次渲染都创建新数组 */}
<MemoChild items={[1, 2, 3]} />
</div>
);
}
解决方案是配合 useMemo 和 useCallback 稳定引用。
三、useMemo 与 useCallback — 稳定 Props 引用
useCallback 稳定函数引用
function Parent() {
const [count, setCount] = useState(0);
const [query, setQuery] = useState('');
const handleSearch = useCallback((term: string) => {
fetchResults(term);
}, []);
const handleItemClick = useCallback((id: string) => {
console.log('点击了', id);
}, []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数:{count}</button>
<SearchBar onSearch={handleSearch} />
<ItemList items={items} onItemClick={handleItemClick} />
</div>
);
}
const SearchBar = React.memo(function SearchBar({
onSearch,
}: {
onSearch: (term: string) => void;
}) {
const [term, setTerm] = useState('');
return (
<input
value={term}
onChange={e => setTerm(e.target.value)}
onKeyDown={e => e.key === 'Enter' && onSearch(term)}
/>
);
});
useMemo 稳定对象/计算结果
function Dashboard({ data }: { data: RawData[] }) {
const [filter, setFilter] = useState('all');
const processedData = useMemo(() => {
return data
.filter(item => filter === 'all' || item.category === filter)
.map(item => ({ ...item, score: computeScore(item) }))
.sort((a, b) => b.score - a.score);
}, [data, filter]);
const chartConfig = useMemo(() => ({
type: 'bar' as const,
colors: ['#3b82f6', '#ef4444', '#22c55e'],
animate: true,
}), []);
return (
<div>
<FilterBar value={filter} onChange={setFilter} />
<DataTable data={processedData} />
<Chart config={chartConfig} data={processedData} />
</div>
);
}
四、shouldComponentUpdate 与 PureComponent
在类组件中,可以通过 shouldComponentUpdate 手动控制是否需要重新渲染:
class HeavyList extends React.Component<Props, State> {
shouldComponentUpdate(nextProps: Props, nextState: State) {
return (
nextProps.items !== this.props.items ||
nextState.selectedId !== this.state.selectedId
);
}
render() {
return (
<ul>
{this.props.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
}
React.PureComponent 内置了浅比较的 shouldComponentUpdate,等价于对所有 props 和 state 进行浅层相等检查:
class OptimizedCard extends React.PureComponent<{ title: string; count: number }> {
render() {
return (
<div>
<h2>{this.props.title}</h2>
<span>{this.props.count}</span>
</div>
);
}
}
五、状态下放(State Colocation)
将状态尽可能放在需要它的组件附近,而非提升到不必要的高层级。这样状态变化时只有相关组件重新渲染:
// 优化前:所有子组件都因 inputValue 变化而重新渲染
function Page() {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<ExpensiveChart />
<ExpensiveTable />
</div>
);
}
// 优化后:将输入状态下放到独立组件
function Page() {
return (
<div>
<SearchInput />
<ExpensiveChart />
<ExpensiveTable />
</div>
);
}
function SearchInput() {
const [inputValue, setInputValue] = useState('');
return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
}
六、Children as Props 模式
利用 children 或 render props 将不变的组件树作为 props 传入,使其在父组件 state 变化时不被重新创建:
function ScrollTracker({ children }: { children: React.ReactNode }) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<div>
<div className="scroll-indicator">滚动位置:{scrollY}px</div>
{children}
</div>
);
}
// 使用时 children 不会因 scrollY 变化而重新渲染
function App() {
return (
<ScrollTracker>
<ExpensiveContent />
</ScrollTracker>
);
}
原理:children 是在 App 渲染时创建的 JSX,ScrollTracker 的 state 变化不会导致 App 重新渲染,因此 children 引用保持不变。
七、Context 拆分
当一个 Context 包含多个值时,其中任意值变化会导致所有消费者重新渲染。将不同更新频率的数据拆分到不同的 Context 中:
// 优化前:theme 和 user 放在一起,更新任一都导致所有消费者渲染
const AppContext = createContext({ theme: 'light', user: null });
// 优化后:拆分为独立 Context
const ThemeContext = createContext<'light' | 'dark'>('light');
const UserContext = createContext<User | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [user, setUser] = useState<User | null>(null);
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
八、lazy 与 Suspense — 代码分割
通过 React.lazy 按需加载组件,减少初始包体积,加快首屏渲染:
import { lazy, Suspense } from 'react';
const AdminPanel = lazy(() => import('./AdminPanel'));
const UserProfile = lazy(() => import('./UserProfile'));
const Analytics = lazy(() => import('./Analytics'));
function App() {
return (
<Suspense fallback={<LoadingSkeleton />}>
<Routes>
<Route path="/admin" element={<AdminPanel />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
Suspense 还支持嵌套使用,为不同区域提供独立的加载状态和加载粒度控制。
九、React Compiler(自动优化)
React 19 引入的 React Compiler(原 React Forget)可以在编译时自动插入 memo/useMemo/useCallback 等优化,让开发者无需手动管理引用稳定性:
// 编写时无需手动 memo
function TodoList({ todos, onToggle }) {
const visibleTodos = todos.filter(t => !t.hidden);
return (
<ul>
{visibleTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
))}
</ul>
);
}
// React Compiler 编译后自动等效于:
// - visibleTodos 被 useMemo 包裹
// - onToggle 引用自动追踪
// - TodoItem 渲染自动优化
React Compiler 通过静态分析组件代码,自动识别哪些值需要记忆化,从根本上解决了手动优化的负担和遗漏问题。
十、总结
优化的首要原则是先度量再优化。使用 React DevTools Profiler 定位实际的性能瓶颈,避免过早优化带来的代码复杂性。