React面试宝典【Part5:性能优化】

2026-08-03 React,面试

1. 减少不必要的重渲染(最核心)

(1)React.memo 缓存组件

对函数组件做 props 浅比较,不变就不重渲染。

const Child = React.memo(function Child({ data }) {
  return <div>{data}</div>;
});

(2)useCallback 缓存函数引用

传给 memo 子组件的函数必须缓存,否则子组件必重渲染。

const handleClick = useCallback(() => {
  setCount(c => c + 1);
}, []);

(3)useMemo 缓存复杂计算结果

避免每次渲染都重新计算大量数据。

const expensiveResult = useMemo(() => {
  return hugeList.filter(...).map(...);
}, [hugeList]);

(4)不要在 JSX 里直接写对象/函数

下面这种每次渲染都是新引用,会击穿 memo:

// ❌ bad
<Child onClick={() => {}} data={{}} />

// ✅ good
const handleClick = useCallback(() => {}, []);
const data = useMemo(() => ({}), []);
<Child onClick={handleClick} data={data} />

(5)合理拆分组件

频繁更新的部分和不怎么变的部分拆开,缩小渲染范围。


2. 列表渲染优化

(1)使用稳定 key

不要用 index,尤其是可删除、可排序的列表。

// ❌ bad
list.map((item, index) => <Item key={index} />)

// ✅ good
list.map(item => <Item key={item.id} />)

(2)大数据列表使用虚拟滚动

只渲染视口内的节点,大幅减少 DOM 数量。
库:react-windowreact-virtualizedantd-virtual-list


3. 代码加载优化

(1)React.lazy + Suspense 懒加载组件

const BigComponent = React.lazy(() => import('./BigComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <BigComponent />
    </Suspense>
  );
}

(2)路由代码分割

每个路由单独打包,访问时才加载。

(3)Tree shaking

  • 使用 ES6 import/export
  • 避免全量导入大库(如 lodash)
  • 工具:Webpack / Rollup 自动剔除无用代码

4. 渲染计算优化

(1)避免在 render 做昂贵计算

不要在组件内部直接做大量循环、过滤、排序。

// ❌ bad
function App() {
  const result = hugeArray.filter(...).sort(...) // 每次渲染都跑
}

// ✅ good
const result = useMemo(() => hugeArray.filter(...), [hugeArray])

(2)避免滥用 Context

一个大 Context 里放一堆状态 → 一个变全量重渲染。
优化:

  • 拆成多个小 Context
  • 配合 useMemo 缓存 value
  • 复杂状态用 Zustand/Jotai/Redux 替代

(3)图片懒加载

  • loading="lazy"
  • 滚动到可视区域再加载

5. React 18 并发优化

  • useTransition:低优先级渲染,不阻塞输入
  • useDeferredValue:延迟值更新,避免界面卡顿
  • 自动批处理:所有场景 setState 合并,减少渲染次数

6. 调试与定位

  • React DevTools Profiler:看哪些组件在“浪费渲染”
  • Performance 面板:看长任务、JS 执行耗时
  • 原则:先定位瓶颈,再优化,不要盲目提前优化

总结

React 性能优化主要从四块入手:

  1. 减少重渲染:memo、useCallback、useMemo、拆分组件
  2. 列表优化:稳定 key + 虚拟滚动
  3. 加载优化:懒加载、路由分割、tree-shaking
  4. 渲染优化:避免昂贵计算、合理拆分 Context、并发特性

一些示例

先演示不优化 → 疯狂重渲染,再一步步加上 React.memo + useCallback + useMemo 优化,可以清晰看到前后区别。

1. 【未优化版本】——子组件无脑重渲染

import { useState } from 'react';

// 子组件(没优化)
function Child({ data, onChange }) {
  console.log('Child 重渲染了'); // 每次父更新都会打印
  return (
    <div>
      <p>{data.name}</p>
      <button onClick={onChange}>点我</button>
    </div>
  );
}

// 父组件
export default function App() {
  const [count, setCount] = useState(0);

  console.log('App 重渲染了');

  return (
    <div>
      <h1>计数:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>

      {/* 问题点:直接传对象、直接传函数 */}
      <Child
        data={{ name: '测试' }}
        onChange={() => console.log('click')}
      />
    </div>
  );
}

现象

  • 点击父组件 +1
  • App 重渲染了
  • Child 也一定会重渲染
    明明 Child 的 props 逻辑上没变,却每次都跟着渲染。

原因

  1. data={{ name: '测试' }} 每次都是新对象
  2. onChange={() => {}} 每次都是新函数
  3. 引用变了 → Child 认为 props 变了 → 必重渲染

2. 优化版本 1:给子组件包 React.memo

import { useState } from 'react';
import React from 'react';

// 包一层 memo:props 浅比较,不变就不渲染
const Child = React.memo(function Child({ data, onChange }) {
  console.log('Child 重渲染了');
  return (
    <div>
      <p>{data.name}</p>
      <button onClick={onChange}>点我</button>
    </div>
  );
});

export default function App() {
  const [count, setCount] = useState(0);

  console.log('App 重渲染了');

  return (
    <div>
      <h1>计数:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>

      <Child
        data={{ name: '测试' }}
        onChange={() => console.log('click')}
      />
    </div>
  );
}

现象
依然无效!Child 还是每次都渲染。
因为 memo 是浅比较,而传递的对象/函数每次都是新引用,浅比较直接判为不等。

3. 最终优化版:memo + useCallback + useMemo

import { useState, useCallback, useMemo } from 'react';
import React from 'react';

// 子组件 memo 缓存
const Child = React.memo(function Child({ data, onChange }) {
  console.log('Child 重渲染了');
  return (
    <div>
      <p>{data.name}</p>
      <button onClick={onChange}>点我</button>
    </div>
  );
});

export default function App() {
  const [count, setCount] = useState(0);

  console.log('App 重渲染了');

  // 缓存函数
  const onChange = useCallback(() => {
    console.log('click');
  }, []);

  // 缓存对象
  const data = useMemo(() => ({
    name: '测试'
  }), []);

  return (
    <div>
      <h1>计数:{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>

      {/* 现在传的是稳定引用 */}
      <Child data={data} onChange={onChange} />
    </div>
  );
}

最终效果

  • 点击父组件 +1
  • App 重渲染
  • Child 只在首次渲染一次,之后再也不渲染
    优化成功!

4. 再加一个【昂贵计算】的 useMemo 演示

import { useState, useMemo } from 'react';
import React from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  // 模拟昂贵计算
  function heavyCompute() {
    console.log('执行昂贵计算');
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) sum += i;
    return sum;
  }

  // 不用 useMemo:每次渲染都执行
  // const total = heavyCompute();

  // 用 useMemo:依赖不变只执行一次
  const total = useMemo(() => heavyCompute(), []);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <p>总和:{total}</p>
    </div>
  );
}

对比

  • 不用 useMemo:每次点 +1 都会卡一下
  • 用 useMemo:只有第一次执行,后续丝滑

总结

  1. React.memo:缓存组件,避免 props 不变时重渲染
  2. useCallback:缓存函数引用,配合 memo 生效
  3. useMemo:缓存昂贵计算结果 & 缓存对象引用
  4. 不要在 JSX 里直接写 {}(),一定会击穿 memo

Context 滥用导致全组件重渲染 + 拆分优化

  • Context 滥用:一个 Provider 里放多状态 → 任一状态变,所有消费组件都会被 Context 强制重渲染
  • 拆分 Context:按功能拆成多个 Provider → 只消费对应 Context 的组件才重渲染,无关组件不受 Context 影响
  • 重要边界:拆分不能阻止“父组件 setState 导致子组件函数执行”,只能阻止“Context 跨状态广播渲染”。

1. 错误写法:单 Context 滥用(渲染风暴)

usertheme 放同一个 Context value → 改 theme,UserInfo 也会因 Context 强制重渲染。

import { createContext, useContext, useState } from 'react';

const AppContext = createContext(null);

// 只用到 user
const UserInfo = () => {
  console.log('🔵 UserInfo 渲染');
  const { user } = useContext(AppContext);
  return <div>用户:{user.name}</div>;
};

// 只用到 theme
const ThemePanel = () => {
  console.log('🟢 ThemePanel 渲染');
  const { theme } = useContext(AppContext);
  return <div>主题:{theme}</div>;
};

export default function App() {
  const [user, setUser] = useState({ name: '张三' });
  const [theme, setTheme] = useState('light');
  console.log('🟡 App 渲染');

  return (
    <AppContext.Provider value={{ user, theme }}>
      <UserInfo />
      <ThemePanel>
      </ThemePanel>
      <button onClick={() => setTheme('dark')}>切换主题</button>
    </AppContext.Provider>
  );
}

点击「切换主题」结果

🟡 App 渲染
🔵 UserInfo 渲染   ❌ 完全没必要,被 Context 强行拖下水
🟢 ThemePanel 渲染 ✅

真实原因

  • value={{ user, theme }} 每次都是新引用
  • 所有消费该 Context 的组件一律被标记更新
  • 即使组件只用到其中一个状态,照样重渲染

2. 正确写法:拆分多个 Context

优化点

  • 拆为 UserContext + ThemeContext
  • 改 theme → 只触发 ThemePanel
  • 改 user → 只触发 UserInfo
import { createContext, useContext, useState } from 'react';

const UserContext = createContext(null);
const ThemeContext = createContext(null);

const UserInfo = () => {
  console.log('🔵 UserInfo 渲染');
  const user = useContext(UserContext);
  return <div>用户:{user.name}</div>;
};

const ThemePanel = () => {
  console.log('🟢 ThemePanel 渲染');
  const theme = useContext(ThemeContext);
  return <div>主题:{theme}</div>;
};

export default function App() {
  const [user, setUser] = useState({ name: '张三' });
  const [theme, setTheme] = useState('light');
  console.log('🟡 App 渲染');

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <UserInfo />
        <ThemePanel />
        <button onClick={() => setTheme('dark')}>切换主题</button>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

点击「切换主题」结果

🟡 App 渲染
🔵 UserInfo 渲染   ⚠️ 函数执行(父渲染导致)
🟢 ThemePanel 渲染 ✅

优化逻辑

  • UserInfo 确实执行了函数(打印 log)
    这是父组件 App 重渲染的正常行为,和 Context 无关
  • 但 UserInfo 没有因 Context 变化而产生真实更新
    不会走 diff、不会更新 DOM、不会执行副作用,开销极低
  • 对比#1版:
    • 滥用:UserInfo 被 Context 强制重渲染
    • 拆分:UserInfo 只执行函数,不做更新

这就是 Context 拆分的真实优化效果

3. 最佳实践:拆分 + memo

import { createContext, useContext, useState, memo } from 'react';

const UserContext = createContext(null);
const ThemeContext = createContext(null);

// 包 memo
const UserInfo = memo(() => {
  console.log('🔵 UserInfo 渲染');
  const user = useContext(UserContext);
  return <div>用户:{user.name}</div>;
});

const ThemePanel = memo(() => {
  console.log('🟢 ThemePanel 渲染');
  const theme = useContext(ThemeContext);
  return <div>主题:{theme}</div>;
});

export default function App() {
  const [user, setUser] = useState({ name: '张三' });
  const [theme, setTheme] = useState('light');
  console.log('🟡 App 渲染');

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <UserInfo />
        <ThemePanel />
        <button onClick={() => setTheme('dark')}>切换主题</button>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

点击「切换主题」最终结果

🟡 App 渲染
// 🔵 UserInfo 不再打印 ✅
🟢 ThemePanel 渲染 ✅

4. 所有渲染规则

场景 子组件函数执行 子组件因 Context 强制更新 优化级别
单 Context 滥用 是(所有消费组件) ❌ 最差
拆分 Context 是(父渲染导致) 否(仅对应消费) ✅ 优化
拆分 + memo 否(仅对应消费) ✅✅ 最佳

5. 总结

  1. Context 滥用问题
    把多个无关状态放入同一个 Context,导致一个状态更新,所有消费组件被强制重渲染,产生大量无效更新。

  2. 拆分优化原理
    按业务/功能拆分为多个独立 Context,实现状态更新隔离,只有消费对应 Context 的组件才会更新,避免广播式渲染。

  3. 重要边界
    拆分 Context 不能阻止父组件 setState 带来的子组件函数执行,只能消除 Context 自身的广播渲染。
    想要彻底阻止子组件执行,必须搭配 React.memo

  4. 最佳实践

    • 小状态、局部状态不用 Context
    • 全局状态按功能拆分多个 Context
    • 消费组件使用 memo 避免父渲染扩散
    • 复杂全局状态优先使用状态库(Zustand/Jotai)实现细粒度订阅