跳到主要内容
预计阅读 53 分钟

React篇 | 性能优化

前言

“你在 React 中做过哪些性能优化?“——这是面试中出现率极高的开放题。很多人的回答只是列举 API:React.memouseMemouseCallbackReact.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>;
}
点击查看答案

有性能问题。每次点击按钮修改 countApp 组件重渲染,ExpensiveList 也会跟着重渲染——即使它的 props 和内部状态没有任何变化。

优化方案:

  1. React.memo 包裹 ExpensiveList,让它在 props 不变时跳过重渲染
  2. 或者把 count 相关的逻辑提取到一个子组件中(状态下沉),让 ExpensiveList 不受 count 变化影响

Q2:useMemouseCallback 是不是用得越多越好?

点击查看答案

不是。useMemouseCallback 本身有开销——每次渲染都要比较依赖项、维护缓存。如果被缓存的计算很轻量(比如简单的字符串拼接),或者组件本身没有性能问题,加 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:缓存函数引用

useCallbackuseMemo 的语法糖,专门用于缓存函数:

// 这两种写法完全等价
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。在未来,你可能不再需要手动写 useMemouseCallbackReact.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 面板是分析性能问题的最佳工具。

使用步骤:

  1. 打开 React DevTools → Profiler 面板
  2. 点击”录制”按钮
  3. 在页面上执行你想分析的操作
  4. 点击”停止录制”
  5. 分析结果

关键指标:

  • 每次 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 性能优化的核心策略和常见反模式。

核心要点

  1. React.memo:跳过 props 不变时的重渲染,需要配合稳定的引用(useMemo / useCallback)
  2. useMemo / useCallback:缓存值/函数引用,只在确认有性能问题时使用
  3. 状态下沉:把频繁变化的状态下沉到最小的子组件,避免波及不相关的组件
  4. 虚拟列表:只渲染可视区域的元素,适用于长列表(> 100-200 项)
  5. 代码分割:React.lazy + Suspense,按路由或按组件懒加载
  6. React Profiler:先定位问题再优化,不要盲目加 memo
  7. 常见反模式:不必要的 state、渲染中创建新引用、过度 memo、用 state + effect 同步派生值

优化决策流程

发现页面卡顿
  → 用 Profiler 定位:是哪个组件慢?
    → 渲染次数太多?
      → 状态下沉 / 组件拆分
      → React.memo + useMemo/useCallback
    → 单次渲染太慢?
      → 计算量大?→ useMemo 缓存 / Web Worker
      → DOM 节点太多?→ 虚拟列表
      → 代码量大?→ React.lazy 代码分割

本章思维导图

React:性能优化
  • 避免不必要的重渲染
    • 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 确认瓶颈在哪。

第三题 ⭐⭐⭐(综合):设计一个高性能的无限滚动列表

要求:

  1. 支持无限滚动加载(滚动到底部自动加载更多)
  2. 使用虚拟列表(只渲染可视区域)
  3. 加载状态和错误处理
  4. 描述实现思路即可,不需要完整代码
点击查看参考思路

核心架构:

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>
  );
}

关键设计决策:

  1. 预加载阈值:不等到最后一项才加载,提前 5 项开始加载(items.length - 5),避免用户看到 loading
  2. 防止重复请求:用 loading 标志位防止并发请求
  3. 虚拟列表 + 无限滚动结合itemCount 设为 items.length + 1(多一个 loading 项),利用虚拟列表的 onItemsRendered 回调触发加载
  4. 内存考虑:如果列表可能无限增长,考虑限制缓存的数据量(比如只保留最近的 N 页)
  5. 实际项目中推荐用 React Query 的 useInfiniteQuery 管理分页数据

自我检测

读完本章后,对照下面的清单检验一下自己的掌握程度。

  • 能解释 React.memo、useMemo、useCallback 各自的作用和使用场景
  • 知道 useCallback 单独使用没有意义,需要配合 React.memo
  • 能用至少两种方式减少不必要的重渲染(状态下沉、children pattern、组件拆分)
  • 知道虚拟列表的原理和适用场景,以及如何使用 react-window
  • 能使用 React.lazy + Suspense 实现路由级和组件级代码分割
  • 能使用 React DevTools Profiler 定位性能瓶颈
  • 能说出至少三个常见的性能反模式,并给出正确的做法
  • 遵循”先定位再优化”的原则,不盲目加 memo

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90