
最近折腾 React 性能优化,踩了几个坑,这篇把问题说清楚。
React 性能优化这个话题,坊间流传着太多迷思。开发者动不动就给所有组件套 React.memo,给所有函数包 useCallback,结果应用还是卡顿、内存还是爆炸。
过早优化反而会让性能变差、代码变乱。要构建真正快的 React 应用,得针对实际的性能瓶颈来下手:无谓的重渲染、状态位置不当、包体积过大、主线程阻塞。下面分享 5 个在生产环境实测有效的 React 性能优化技巧。
动手加 useMemo 或 React.memo 之前,先审视一下你的状态放置位置。如果状态挂在组件树的太高层级,任何一次状态更新都会强制整个子树重新渲染。
// 修改 `color` 会导致 <HeavyChartComponent/> 和 <ComplexTable/> 全部重新渲染!
export default function App() { const [color, setColor] = useState('#6366f1'); return ( <div> <input type="color" value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Sample Text</p> <HeavyChartComponent/> <ComplexTable/> </div> );
}
✅ 解决方案:组件隔离
把独立的状态及其控制逻辑下移到专属的子组件中:
function ColorPicker() { const [color, setColor] = useState('#6366f1'); return ( <div> <input type="color" value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Sample Text</p> </div> );
} export default function App() { return ( <div> <ColorPicker/> {/* 这些组件不再受 color 状态变更影响 */} <HeavyChartComponent/> <ComplexTable/> </div> );
}
有时候状态必须留在父组件,但你又不想让子组件因为父组件状态更新而重新渲染。这时可以用 children props 隔离渲染。
// ScrollContainer 管理滚动位置状态,但 `HeavyContent` 不会因为滚动而重新渲染!
function ScrollContainer({ children }) { const [scrollPosition, setScrollPosition] = useState(0); return ( <div onScroll={(e) => setScrollPosition(e.currentTarget.scrollTop)}> <p>Position: {scrollPosition}px</p> {children} </div> );
} export default function App() { return ( <ScrollContainer> <HeavyContent/> </ScrollContainer> );
}
因为 <HeavyContent/> 是从 App 作为 props 传递进来的,React 知道它的 props 没有变化,所以在 ScrollContainer 更新状态时会跳过重新渲染它。
一次性渲染成千上万个 DOM 节点会把主线程卡死,内存也会爆炸,不管你的 React 代码写得多好都没用。
与其渲染 5000 行表格数据或列表卡片,不如用虚拟化技术只渲染当前视口内可见的条目。
npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual'; function VirtualizedList({ items }) { const parentRef = React.useRef(null); const rowVirtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // 每个条目的预估高度(像素) }); return ( <div ref={parentRef} className="h-[400px] overflow-auto border rounded-md"> <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: '100%', position: 'relative', }} > {rowVirtualizer.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)`, }} className="p-3 border-b flex items-center" > {items[virtualRow.index]} </div> ))} </div> </div> );
}
如果用户打开的是登录页面,就不应该让他们一开始就下载数据分析看板、用户设置页和订单生成器的 JavaScript 包。
用动态导入和 React.lazy 把臃肿的单一包拆成多个轻量级 chunk:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner'; // 动态路由导入
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics')); export default function AppRoutes() { return ( <BrowserRouter> <Suspense fallback="{<LoadingSpinner"/>}> <Routes> <Route element="{<Dashboard" path="/dashboard"/>} /> <Route element="{<Settings" path="/settings"/>} /> <Route element="{<Analytics" path="/analytics"/>} /> </Routes> </Suspense> <//BrowserRouter> );
}
在复杂的用户界面(比如实时搜索过滤或数据看板输入框)里,如果在输入时 React 正在同步重渲染成百上千个 UI 组件,用户的每一次按键都会感觉卡顿。
用 React 的 useTransition 把重量级渲染标记为非紧急,保持输入框的响应:
import { useState, useTransition } from 'react'; export function SearchFilter({ dataset }) { const [query, setQuery] = useState(''); const [filteredResults, setFilteredResults] = useState(dataset); const [isPending, startTransition] = useTransition(); const handleSearch = (e) => { const value = e.target.value; // 紧急更新:立即更新输入框状态,让打字感觉即时响应 setQuery(value); // 非紧急更新:在后台计算和渲染重量级的过滤列表 startTransition(() => { const results = dataset.filter((item) => item.name.toLowerCase().includes(value.toLowerCase()) ); setFilteredResults(results); }); }; return ( <div> <input type="text" value={query} onChange={handleSearch} placeholder="Search records..." /> {isPending && <p className="text-sm text-gray-500">Updating results...</p>} <ResultsList data="{filteredResults}"/> </div> );
}
架构先行:在到处撒 React.memo 之前,先优化状态放置、善用组件组合。
列表虚拟化:永远不要一次渲染成千上万个 DOM 节点,用 @tanstack/react-virtual 这类窗口化库。
分割包:用 React.lazy 和 Suspense 做路由级代码分割,削减首屏加载时间。
延迟重量计算:用 React 18+ 的并发 hooks(如 useTransition)防止密集渲染时界面冻结。