React篇 | 性能优化
前言
“你在 React 中做过哪些性能优化?“——这是面试中出现率极高的开放题。很多人的回答只是列举 API:React.memo、useMemo、useCallback、React.lazy……
但面试官想听的不是 API 列表,而是:
- 你是怎么发现性能问题的?用了什么工具?
- 你是怎么分析问题根因的?是渲染次数多还是单次渲染慢?
- 你是怎么选择优化方案的?为什么用这个而不是那个?
- 你知道哪些”看起来是优化但实际上是反模式”的做法吗?
本章不只是教你用哪些 API,更要帮你建立一套**“定位问题 → 分析原因 → 选择方案”**的完整思维框架。
诊断自测
Q1:下面的代码有性能问题吗?如果有,问题在哪?
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveList />
</div>
);
}
function ExpensiveList() {
// 假设这个组件渲染很慢(几千个列表项)
return <ul>{heavyData.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
点击查看答案
有性能问题。每次点击按钮修改 count,App 组件重渲染,ExpensiveList 也会跟着重渲染——即使它的 props 和内部状态没有任何变化。
优化方案:
- 用
React.memo包裹ExpensiveList,让它在 props 不变时跳过重渲染 - 或者把
count相关的逻辑提取到一个子组件中(状态下沉),让ExpensiveList不受count变化影响
Q2:useMemo 和 useCallback 是不是用得越多越好?
点击查看答案
不是。useMemo 和 useCallback 本身有开销——每次渲染都要比较依赖项、维护缓存。如果被缓存的计算很轻量(比如简单的字符串拼接),或者组件本身没有性能问题,加 memo 反而增加了不必要的开销和代码复杂度。
正确的做法是:先用 Profiler 等工具确认存在性能问题,再有针对性地使用 memo。“不要过早优化”这条原则在 React 中同样适用。
一、React.memo / useMemo / useCallback 的正确使用
1.1 React.memo:跳过不必要的组件重渲染
React.memo 是一个高阶组件,它会在 props 没有变化时跳过组件的重渲染:
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
function App() {
const [count, setCount] = useState(0);
const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{/* items 引用没变,ExpensiveList 不会重渲染 */}
<ExpensiveList items={items} />
</div>
);
}
但上面的代码有个问题——items 虽然内容没变,但它是在 App 的函数体中定义的,每次渲染都会创建一个新的数组引用。React.memo 用的是浅比较(===),新引用 !== 旧引用,所以 ExpensiveList 还是会重渲染。
修复方法之一是用 useMemo:
function App() {
const [count, setCount] = useState(0);
// 用 useMemo 缓存 items,避免每次渲染创建新引用
const items = useMemo(
() => [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
[] // 依赖为空,只计算一次
);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveList items={items} />
</div>
);
}
另一种更简单的方法是把 items 提到组件外面:
// 如果 items 是静态的,直接提到组件外部
const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveList items={items} />
</div>
);
}
1.2 useMemo:缓存计算结果
useMemo 缓存一个值,只有依赖项变化时才重新计算:
function SearchResults({ query, items }) {
// 只有 query 或 items 变化时才重新过滤
const filteredItems = useMemo(
() => items.filter(item => item.name.includes(query)),
[query, items]
);
return (
<ul>
{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
什么时候该用 useMemo?
- 计算确实很耗时(如大数据量的 filter/sort/map、复杂的格式化)
- 返回值是对象或数组,需要保持引用稳定(配合 React.memo 使用)
- 不该用的场景:简单计算(如
a + b、字符串拼接)、不需要引用稳定的基本类型
1.3 useCallback:缓存函数引用
useCallback 是 useMemo 的语法糖,专门用于缓存函数:
// 这两种写法完全等价
const handleClick = useCallback(() => { /* ... */ }, [dep]);
const handleClick = useMemo(() => () => { /* ... */ }, [dep]);
useCallback 最常见的使用场景是配合 React.memo:
const MemoChild = React.memo(function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// 不用 useCallback:每次渲染创建新函数,MemoChild 还是会重渲染
// const handleClick = () => console.log('clicked');
// 用 useCallback:函数引用稳定,MemoChild 跳过重渲染
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<MemoChild onClick={handleClick} />
</div>
);
}
关键理解:useCallback 单独使用没有意义。 如果子组件没有用 React.memo 包裹,传不传稳定的函数引用都一样——子组件照样重渲染。useCallback 必须配合 React.memo(或 shouldComponentUpdate)才有效果。
1.4 React 19 的编译器优化
React 19 引入了 React Compiler(之前叫 React Forget),它可以在编译阶段自动插入 memo。在未来,你可能不再需要手动写 useMemo、useCallback、React.memo——编译器会帮你做。
但在编译器普及之前,手动 memo 仍然是必要的。
二、避免不必要的重渲染
比起 memo 缓存,更根本的优化思路是:从源头减少不必要的重渲染。
2.1 状态下沉(Lift Content Up / Push State Down)
把频繁变化的状态下沉到需要它的最小子组件中,避免影响不相关的兄弟组件:
// ❌ 糟糕:color 变化导致整个 App 重渲染,包括 ExpensiveTree
function App() {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
// ✅ 方案一:状态下沉——把 color 相关的逻辑提取到子组件
function App() {
return (
<div>
<ColorPicker />
<ExpensiveTree /> {/* 不再因 color 变化重渲染 */}
</div>
);
}
function ColorPicker() {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
<p>Hello, world!</p>
</div>
);
}
// ✅ 方案二:内容提升(children pattern)
function App() {
return (
<ColorWrapper>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorWrapper>
);
}
function ColorWrapper({ children }) {
const [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={e => setColor(e.target.value)} />
{children} {/* children 的引用没变,不会重渲染 */}
</div>
);
}
方案二的原理:children 是在 App 中创建的 React 元素,它的引用在 ColorWrapper 重渲染时不会变化(因为 App 没有重渲染),所以 ExpensiveTree 不会重渲染。
2.2 组件拆分
把大组件拆分成小组件,让每个组件只依赖它需要的状态:
// ❌ 一个大组件,任何状态变化都导致全部重渲染
function Dashboard() {
const [search, setSearch] = useState('');
const [sort, setSort] = useState('name');
const [page, setPage] = useState(1);
return (
<div>
<SearchBar value={search} onChange={setSearch} />
<SortControls value={sort} onChange={setSort} />
<Pagination page={page} onChange={setPage} />
<DataTable search={search} sort={sort} page={page} />
</div>
);
}
// ✅ 如果 SearchBar 只需要 search 状态,可以让它管理自己的状态
// 只在确认搜索时才更新父组件
function SearchBar({ onSearch }) {
const [localSearch, setLocalSearch] = useState('');
return (
<input
value={localSearch}
onChange={e => setLocalSearch(e.target.value)}
onKeyDown={e => e.key === 'Enter' && onSearch(localSearch)}
/>
);
}
三、列表渲染优化:虚拟列表
3.1 问题:渲染大量列表项
// ❌ 渲染 10000 个列表项
function BigList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
即使每个 <li> 渲染只需要 0.1ms,10000 项也要 1 秒。而用户在屏幕上同时看到的可能只有 20 项。
3.2 虚拟列表的原理
虚拟列表(Virtual List / Windowing)的核心思想:只渲染可视区域内的元素。
┌─────────────────────┐
│ (不渲染) │ ← 滚动到上方的元素,不在 DOM 中
│ ... │
├─────────────────────┤
│ Item 45 │ ← 可视区域:只渲染这些
│ Item 46 │
│ Item 47 │
│ ... │
│ Item 65 │
├─────────────────────┤
│ (不渲染) │ ← 滚动到下方的元素,不在 DOM 中
│ ... │
└─────────────────────┘
通过计算滚动位置,动态决定哪些元素在可视区域内,只渲染这些元素。DOM 中始终只有几十个节点,而不是上万个。
3.3 使用 react-window
react-window 是最流行的 React 虚拟列表库:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={600} // 可视区域高度
width="100%"
itemCount={items.length}
itemSize={50} // 每项高度
>
{Row}
</FixedSizeList>
);
}
对于高度不固定的列表,使用 VariableSizeList:
import { VariableSizeList } from 'react-window';
function VirtualList({ items }) {
const getItemSize = (index) => items[index].expanded ? 200 : 50;
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<VariableSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
3.4 什么时候需要虚拟列表?
- 列表项 > 100-200 项时考虑
- 如果列表项包含复杂组件(嵌套、图片等),阈值更低
- 如果列表是静态的且项数不多,不需要虚拟列表
四、代码分割:React.lazy + Suspense
4.1 问题:bundle 太大
默认情况下,Webpack/Vite 会把所有代码打包成一个(或少数几个)bundle。如果应用很大,首屏需要下载的 JS 量就很大,导致加载慢。
4.2 按路由分割
最常见的代码分割点是路由:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// 懒加载路由组件
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
用户访问首页时只下载 Home 的代码。导航到 Dashboard 时才下载 Dashboard 的代码。
4.3 按组件分割
对于体积大但不立即需要的组件,也可以做懒加载:
const HeavyEditor = lazy(() => import('./components/HeavyEditor'));
function ArticlePage() {
const [editing, setEditing] = useState(false);
return (
<div>
<article>{/* ... */}</article>
<button onClick={() => setEditing(true)}>Edit</button>
{editing && (
<Suspense fallback={<p>Loading editor...</p>}>
<HeavyEditor />
</Suspense>
)}
</div>
);
}
编辑器代码只有在用户点击”编辑”后才加载。
4.4 预加载
可以在用户可能需要之前提前加载:
const Dashboard = lazy(() => import('./pages/Dashboard'));
// 鼠标悬停在链接上时预加载
function NavLink() {
const preload = () => {
import('./pages/Dashboard'); // 触发预加载
};
return (
<Link to="/dashboard" onMouseEnter={preload}>
Dashboard
</Link>
);
}
五、React Profiler 的使用
5.1 React DevTools Profiler
React DevTools 的 Profiler 面板是分析性能问题的最佳工具。
使用步骤:
- 打开 React DevTools → Profiler 面板
- 点击”录制”按钮
- 在页面上执行你想分析的操作
- 点击”停止录制”
- 分析结果
关键指标:
- 每次 commit 的耗时:整个更新(从 setState 到 DOM 更新完成)花了多久
- 每个组件的渲染耗时:哪个组件最慢
- 为什么重渲染:Profiler 可以显示组件重渲染的原因(props 变了 / state 变了 / 父组件重渲染)
5.2 Profiler 组件 API
你也可以在代码中使用 <Profiler> 组件来收集性能数据:
import { Profiler } from 'react';
function onRender(id, phase, actualDuration) {
console.log({
id, // "MyList"
phase, // "mount" 或 "update"
actualDuration, // 本次渲染耗时(ms)
});
}
function App() {
return (
<Profiler id="MyList" onRender={onRender}>
<HeavyList />
</Profiler>
);
}
5.3 “高亮更新”功能
在 React DevTools 的设置中启用 “Highlight updates when components render”,可以在页面上直接看到哪些组件被重渲染了——被更新的组件会闪烁高亮。这是快速发现不必要重渲染的最直观方式。
六、常见性能反模式
6.1 反模式:在渲染中创建新对象/数组/函数
// ❌ 每次渲染都创建新的 style 对象
function Avatar({ url }) {
return <img src={url} style={{ borderRadius: '50%', width: 40 }} />;
}
// ✅ 提到组件外部(如果是静态的)
const avatarStyle = { borderRadius: '50%', width: 40 };
function Avatar({ url }) {
return <img src={url} style={avatarStyle} />;
}
但要注意:这只在组件被 React.memo 包裹时才重要。如果没有 memo,新旧引用是否相同不影响渲染行为。
6.2 反模式:在 Context 中传递经常变化的值
// ❌ 每次渲染都创建新的 value 对象,所有 Consumer 都会重渲染
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// ✅ 用 useMemo 稳定 value 引用
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
6.3 反模式:不必要的 state
// ❌ fullName 可以从 firstName 和 lastName 计算出来,不需要单独的 state
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName); // 导致额外的重渲染
}, [firstName, lastName]);
return <span>{fullName}</span>;
}
// ✅ 直接计算,不要用 state + effect 来"同步"派生值
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; // 直接计算
return <span>{fullName}</span>;
}
核心原则:如果一个值可以从现有 state/props 计算出来,就不要把它存成 state。 用 state + useEffect 来”同步”派生值是 React 中最常见的反模式之一。
6.4 反模式:给所有东西加 memo
// ❌ 过度 memo
function App() {
const greeting = useMemo(() => 'Hello ' + name, [name]); // 字符串拼接不需要 memo
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 如果没有传给 memo 组件,没有意义
return <button onClick={handleClick}>{greeting}</button>;
}
memo 有成本:依赖比较、缓存维护、代码复杂度。只有在确认存在性能问题时才使用。
常见误区
误区一:“React.memo 会缓存组件的渲染结果”
不准确。React.memo 不缓存 “渲染结果”(虚拟 DOM),而是在 props 没变时跳过渲染函数的调用。如果 props 变了,组件照常完整执行渲染函数。它做的是”跳过”而非”缓存”。
误区二:“useCallback 能提升性能”
单独使用 useCallback 不能提升性能。它只保持函数引用稳定,本身有额外开销。只有当这个稳定的引用被下游的 React.memo 组件利用来跳过重渲染时,才有性能收益。如果接收这个函数的子组件没有做任何优化,useCallback 只是增加了代码复杂度。
误区三:“虚拟列表解决所有列表性能问题”
虚拟列表解决的是”DOM 节点太多”的问题。如果你的性能问题是”单个列表项渲染太慢”(比如每个 item 内部有复杂计算),虚拟列表不会有帮助——你需要优化单个 item 的渲染逻辑。两者经常需要配合使用。
误区四:“代码分割越多越好”
过度的代码分割会导致大量小 chunk,每个 chunk 都需要一次 HTTP 请求。过多的请求反而会降低性能(尤其是 HTTP/1.1 环境下)。合理的分割点通常是路由级别和大型第三方库。
小结
本章我们从实际问题出发,系统讲解了 React 性能优化的核心策略和常见反模式。
核心要点
- React.memo:跳过 props 不变时的重渲染,需要配合稳定的引用(useMemo / useCallback)
- useMemo / useCallback:缓存值/函数引用,只在确认有性能问题时使用
- 状态下沉:把频繁变化的状态下沉到最小的子组件,避免波及不相关的组件
- 虚拟列表:只渲染可视区域的元素,适用于长列表(> 100-200 项)
- 代码分割:React.lazy + Suspense,按路由或按组件懒加载
- React Profiler:先定位问题再优化,不要盲目加 memo
- 常见反模式:不必要的 state、渲染中创建新引用、过度 memo、用 state + effect 同步派生值
优化决策流程
发现页面卡顿
→ 用 Profiler 定位:是哪个组件慢?
→ 渲染次数太多?
→ 状态下沉 / 组件拆分
→ React.memo + useMemo/useCallback
→ 单次渲染太慢?
→ 计算量大?→ useMemo 缓存 / Web Worker
→ DOM 节点太多?→ 虚拟列表
→ 代码量大?→ React.lazy 代码分割
本章思维导图
- 避免不必要的重渲染
- React.memo:props 不变时跳过
- useMemo:缓存计算结果 / 稳定引用
- useCallback:缓存函数引用(配合 memo 才有意义)
- 状态下沉:频繁变化的状态下沉到子组件
- 组件拆分:大组件拆小,减少影响范围
- children pattern:内容提升避免重渲染
- 大列表优化
- 虚拟列表:只渲染可视区域(react-window)
- FixedSizeList / VariableSizeList
- 代码分割
- React.lazy + Suspense
- 按路由分割
- 按组件分割
- 预加载(onMouseEnter + import())
- 性能分析工具
- React DevTools Profiler
- Highlight updates
- Profiler 组件 API
- 常见反模式
- 渲染中创建新引用(对象/数组/函数)
- 不必要的 state(派生值应直接计算)
- state + useEffect 同步派生值
- Context 传递不稳定引用
- 过度 memo
- React 19 Compiler
- 自动 memo(未来方向)
练习挑战
第一题 ⭐(基础):找出性能问题并修复
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const options = { roomId, serverUrl: 'https://chat.example.com' };
useEffect(() => {
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
return () => connection.disconnect();
}, [options]); // ← 问题在这里
return <MessageList messages={messages} />;
}
点击查看答案与解析
问题: options 在每次渲染时都是一个新的对象引用。useEffect 的依赖项比较用的是 ===,新引用 !== 旧引用,所以每次渲染都会重新执行 effect——不断地断开/重连 WebSocket。
修复方案一:把 options 移到 effect 内部
useEffect(() => {
const options = { roomId, serverUrl: 'https://chat.example.com' };
const connection = createConnection(options);
// ...
return () => connection.disconnect();
}, [roomId]); // 只依赖 roomId,不依赖 options
修复方案二:用 useMemo 缓存 options
const options = useMemo(
() => ({ roomId, serverUrl: 'https://chat.example.com' }),
[roomId]
);
方案一更好——把不需要在 effect 外部使用的变量移到 effect 内部,是 React 推荐的做法。
第二题 ⭐⭐(进阶):优化这个组件
下面的 Dashboard 组件在输入搜索词时非常卡。请用至少两种方式优化它。
function Dashboard() {
const [search, setSearch] = useState('');
const [data] = useState(generateHugeDataset()); // 10000 条数据
const filteredData = data.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
<Stats data={filteredData} />
<Chart data={filteredData} />
<Table data={filteredData} />
</div>
);
}
点击查看答案与解析
优化一:useMemo 缓存过滤结果
const filteredData = useMemo(
() => data.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
),
[data, search]
);
优化二:useTransition 让搜索不阻塞输入
const [inputValue, setInputValue] = useState('');
const [search, setSearch] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setInputValue(e.target.value); // 紧急
startTransition(() => {
setSearch(e.target.value); // 非紧急
});
};
优化三:虚拟列表(如果 Table 是长列表)
<VirtualTable data={filteredData} /> // 用 react-window 渲染
优化四:React.memo 包裹子组件
const MemoStats = React.memo(Stats);
const MemoChart = React.memo(Chart);
// filteredData 用 useMemo 保持引用稳定
优化五:防抖
const debouncedSearch = useDeferredValue(inputValue);
// 用 debouncedSearch 做过滤
实际中通常组合使用这些方案。最关键的是先用 Profiler 确认瓶颈在哪。
第三题 ⭐⭐⭐(综合):设计一个高性能的无限滚动列表
要求:
- 支持无限滚动加载(滚动到底部自动加载更多)
- 使用虚拟列表(只渲染可视区域)
- 加载状态和错误处理
- 描述实现思路即可,不需要完整代码
点击查看参考思路
核心架构:
function InfiniteVirtualList() {
// 1. 数据管理
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
// 2. 加载更多
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
const newItems = await fetchPage(items.length);
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setLoading(false);
}, [items.length, loading, hasMore]);
// 3. 虚拟列表 + 滚动检测
return (
<FixedSizeList
height={600}
itemCount={hasMore ? items.length + 1 : items.length}
itemSize={50}
onItemsRendered={({ visibleStopIndex }) => {
// 当滚动到接近底部时加载更多
if (visibleStopIndex >= items.length - 5) {
loadMore();
}
}}
>
{({ index, style }) => {
if (index >= items.length) {
return <div style={style}>Loading...</div>;
}
return <div style={style}>{items[index].name}</div>;
}}
</FixedSizeList>
);
}
关键设计决策:
- 预加载阈值:不等到最后一项才加载,提前 5 项开始加载(
items.length - 5),避免用户看到 loading - 防止重复请求:用
loading标志位防止并发请求 - 虚拟列表 + 无限滚动结合:
itemCount设为items.length + 1(多一个 loading 项),利用虚拟列表的onItemsRendered回调触发加载 - 内存考虑:如果列表可能无限增长,考虑限制缓存的数据量(比如只保留最近的 N 页)
- 实际项目中推荐用 React Query 的
useInfiniteQuery管理分页数据
自我检测
读完本章后,对照下面的清单检验一下自己的掌握程度。
- 能解释 React.memo、useMemo、useCallback 各自的作用和使用场景
- 知道 useCallback 单独使用没有意义,需要配合 React.memo
- 能用至少两种方式减少不必要的重渲染(状态下沉、children pattern、组件拆分)
- 知道虚拟列表的原理和适用场景,以及如何使用 react-window
- 能使用 React.lazy + Suspense 实现路由级和组件级代码分割
- 能使用 React DevTools Profiler 定位性能瓶颈
- 能说出至少三个常见的性能反模式,并给出正确的做法
- 遵循”先定位再优化”的原则,不盲目加 memo
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90