← 返回工具首页

React重渲染了?我总结了7个优化方案

你有没有遇到过这种场景——写了个看起来挺正常的 React 组件,数据也没变,但页面就是卡。一看 DevTools,组件刷刷刷渲染了几十次。老板路过瞄了一眼,说"你这卡啊"。

别慌,今天聊点实在的。我从真实项目里提炼了 7 个经过验证的优化方案,覆盖日常开发中最常见的重渲染问题。看完这篇,你基本上能解决 80% 的 React 性能瓶颈。

一、先说清楚:什么时候算"渲染异常"?

React 的渲染逻辑是:state 变了 → 组件重新渲染。这是设计如此,不是 bug。但问题在于——很多人以为"传进去的 props 没变,子组件就不会重新渲染",实际上并不是。

举个子组件的例子:

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      <Child />
    </div>
  );
}

function Child() {
  console.log('Child 渲染了');
  return <div>子组件</div>;
}

每次点击按钮,Child 都会打印"渲染了"——尽管它自己的 props 和 state 什么都没变。这就是典型的"非必要渲染"。

二、重渲染的常见原因

在动手优化之前,先知道根因在哪。常见原因大概这几类:

1. 父组件重新渲染,强制子组件跟着渲染

上面那个例子就是。父组件的 state 变了,整个组件树重新走一遍 reconciliation。

2. Props 传递的是新对象或新函数

// 每次 Parent 渲染,这俩都是新引用!
function Parent() {
  return <Child onClick={() => console.log('click')} />;
  return <Child onClick={handleClick} />;
}

3. Context 值每次都是新对象

如果你把一个对象放进 Context 的 value,那个对象每次都是新的,整个消费这个 Context 的组件树都会重新渲染。

4. 列表没有稳定的 key

用 index 作为 key,数据变化时 React 分不清谁是谁,干脆全删了重建。

5. 组件本身 state 频繁变化

比如搜索框的实时过滤,每次按键都触发过滤逻辑。

好了,根因知道了。下面逐个击破。

三、7个优化方案

方案1:React.memo——子组件的护身符

React.memo 是最直接的工具。它的作用是:如果 props 没变,就跳过这次渲染

// 优化前
function Child({ name, age }) {
  console.log('Child 渲染了');
  return <div>{name} - {age}</div>;
}

// 优化后:用 React.memo 包裹
const Child = React.memo(function Child({ name, age }) {
  console.log('Child 渲染了');
  return <div>{name} - {age}</div>;
});

效果:父组件 state 变化时,Child 不会重复渲染了——除非 name 或 age 真的变了。

但这里有个坑:React.memo 默认用的是浅比较。如果 props 里有对象:

// Parent 每次渲染,user 都是新对象
<Child user={{ name: 'yock', age: 16 }} />;

// 即使 name 和 age 没变,memo 也会认为 props 变了,因为对象引用不同
// 解决方案:用 useMemo 稳定对象引用(见方案2)

React.memo 还支持第二个参数,做自定义比较:

const Child = React.memo(
  function Child({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true = 不重新渲染(类似 shouldComponentUpdate 的反逻辑)
    return prevProps.user.id === nextProps.user.id;
  }
);

方案2:useMemo——稳定计算结果

useMemo 的作用是:当依赖没变时,复用上一次的计算结果。适合那些计算量大的场景,比如过滤、排序、大数据处理。

function UserList({ users, filter }) {
  // 只有 users 或 filter 变化时,才重新计算 filteredUsers
  const filteredUsers = useMemo(() => {
    console.log('开始过滤...');
    return users.filter(u => u.name.includes(filter));
  }, [users, filter]);

  return (
    <ul>
      {filteredUsers.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

敲黑板:依赖数组一定要写对。漏写了依赖会拿到过期数据,写多了又起不到缓存效果。

另一个常见场景——稳定传给子组件的对象引用:

function Parent() {
  const [count, setCount] = useState(0);

  // 每次都新建对象?Child 每次都重渲染
  // const config = { theme: 'dark', locale: 'zh-CN' };

  // 用 useMemo 稳定引用
  const config = useMemo(() => ({ theme: 'dark', locale: 'zh-CN' }), []);

  return <Child config={config} onClick={() => setCount(c => c + 1)} />;
}

const Child = React.memo(function Child({ config, onClick }) {
  return <button onClick={onClick}>{config.theme}</button>;
});

方案3:useCallback——稳定函数引用

useCallback 是 useMemo 的兄弟,专门用来稳定函数。它和 useMemo 的关系:

// 下面两行是等价的
const fn = useCallback(() => doSomething(a, b), [a, b]);
const fn = useMemo(() => () => doSomething(a, b), [a, b]);

典型场景:把函数作为 props 传给 memo 包裹的子组件。

function Parent() {
  const [count, setCount] = useState(0);

  // 不用 useCallback:每次渲染都是新函数,memo 的子组件会认为 props 变了
  // const handleClick = () => console.log('click');

  // 用 useCallback:count 变化时才创建新函数
  const handleClick = useCallback(() => {
    console.log('click', count);
  }, [count]);

  return <Button onClick={handleClick} />;
}

const Button = React.memo(function Button({ onClick }) {
  return <button onClick={onClick}>点我</button>;
});

注意:不要所有函数都包 useCallback。它本身有开销(闭包、比较),对于不传给子组件的函数,包了反而浪费。优先把 useCallback 用在真正穿透组件层级的 props 函数上。

方案4:Context 优化——别把整个 App 拖下水

Context 是性能重灾区。举个大冤种例子:

const ThemeContext = createContext();

function App() {
  const [user, setUser] = useState({ name: 'yock' });

  return (
    <ThemeContext.Provider value={{ theme: 'dark' }}>
      <Dashboard />  // Dashboard 消费了 ThemeContext
      <Profile />    // Profile 也消费了 ThemeContext
      <Settings />   // Settings 也消费了 ThemeContext
    </ThemeContext.Provider>
  );
}

问题在哪?ThemeContext.Provider 的 value 是个对象字面量,每次 App 渲染都是新对象,导致所有消费这个 Context 的组件全部重新渲染。

解决方案1:拆分成多个小 Context。

// 主题相关的放一个 Context,用户相关的放另一个
const ThemeContext = createContext();
const UserContext = createContext();

// 这样 user 变化不会导致只消费 ThemeContext 的组件重新渲染
function App() {
  const [user, setUser] = useState({ name: 'yock' });
  const theme = useMemo(() => ({ theme: 'dark' }), []);

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Dashboard />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

解决方案2:把 Context 的 value 用 useMemo 包裹。

function App() {
  const [user, setUser] = useState({ name: 'yock' });

  const contextValue = useMemo(() => ({
    user,
    setUser,
    theme: 'dark',
  }), [user]);

  return (
    <Context.Provider value={contextValue}>
      <App />
    </Context.Provider>
  );
}

React 18 的并发特性还给 Context 带来了新的挑战——在 Concurrent 模式下,Context 的 Provider value 变化时,React 可能会多次渲染消费组件。如果你的应用卡顿严重,这个因素也要排查。

方案5:状态扁平化——避免连锁反应

看看这个 state 结构有没有很眼熟:

const [form, setForm] = useState({
  user: {
    profile: {
      name: '',
      avatar: '',
      bio: '',
    },
    settings: {
      notifications: true,
      language: 'zh-CN',
      theme: 'dark',
    }
  }
});

// 改个名字,整条链路都要重新渲染
setForm({
  ...form,
  user: {
    ...form.user,
    profile: {
      ...form.user.profile,
      name: 'yock'
    }
  }
});

这种深层嵌套的状态,任意一处变化都会导致消费这条数据的组件全部重新渲染。而且写起来也痛苦——一条数据变化要展开四五层。

优化思路:按需拆分 state

// 拆成独立的 state,每个只管一件事
const [userName, setUserName] = useState('');
const [userAvatar, setUserAvatar] = useState('');
const [notifications, setNotifications] = useState(true);
const [language, setLanguage] = useState('zh-CN');
const [theme, setTheme] = useState('dark');

// 改名字,只触发 userName 相关的组件重渲染
setUserName('yock');

如果确实需要管理复杂状态,考虑用 Zustand、Jotai 这类原子化状态管理库——React 18 的 useSyncExternalStore 也让这类库在并发模式下的表现更好了。

方案6:虚拟列表——长列表的救星

一个页面渲染 10000 条数据,浏览器直接红牌。这种场景别硬刚 DOM,用虚拟列表。

// 思路:只渲染可视区域内的元素,数据总量可能很大,但 DOM 节点数量固定
// 常见库:react-window、react-virtualized

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

可视区域固定只有几十个 DOM 节点,滚动时动态替换内容,体验和真·渲染一万条完全一样,帧率稳得住。

如果你是表格场景,react-virtualized 的 AutoSizer + ColumnResizer 组合更合适。虚拟列表的关键是估算好 itemSize,动态高度的列表推荐用 variableSize 模式。

方案7:懒加载——减少首屏负担

代码分割,让用户只加载当前页面需要的代码。React.lazy + Suspense 就能搞定:

import { lazy, Suspense } from 'react';

// 路由级别的懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>加载中...</div>}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

组件级别的懒加载:

function HeavyChart() {
  const Chart = useMemo(() => lazy(() => import('./Chart')), []);

  return (
    <Suspense fallback={<Skeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

懒加载不只是首屏优化,配合 preload / prefetch 策略,可以在用户即将访问某页面时提前加载代码,进一步减少等待感。

四、key 优化——你可能一直写错了

React 用 key 来判断列表中每个元素的身份。key 选错了,列表更新时会触发大量不必要的销毁和重建。

错误示范:用 index 作为 key

// 初始数据:[{id:1, name:'a'}, {id:2, name:'b'}]
// 渲染后 key = [0, 1]

// 删除第一项后:[{id:2, name:'b'}]
// 渲染后 key = [0](原本的 index 0 没了,变成新的 index 0)
// React 以为 index 0 的组件更新了,实际上是 item 2 占据了 index 0
// 结果:React 认为是"更新",而不是"删除+重新渲染"
// 代价:所有子组件的 state(输入框内容、展开状态)全部丢失

正确做法:用数据的唯一 ID 作为 key

{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

key 的唯一性只要在同一层级列表内唯一就行,不需要全局唯一。另外,key 不要用随机数——每次渲染都是新值,失去了 key 的意义。

五、用 DevTools Profiler 定位性能瓶颈

优化得对不对,要靠数据说话。React DevTools 的 Profiler 面板就是干这个的。

开启 Profiler

React DevTools 扩展 → Profiler 面板 → 点击 Record → 操作你的应用 → 点击 Stop。

读懂火焰图

火焰图里,每个柱子的高度代表渲染耗时,宽度代表渲染次数。宽度大说明渲染次数多,柱子高说明单次渲染慢。两个都大的就是重点优化对象。

Ranked 视图

按"总共渲染耗时"排序,快速找到哪个组件吃掉了最多的渲染预算。

Component 视图

选中某个组件,可以看到它为什么渲染——是 props 变了?是父组件渲染了?还是 state 变了?直接定位根因。

React 18 的 Suspense 边界

React 18 的 Profiler 对 Suspense 边界有更好的支持,Suspense 展示 loading 状态时,不会触发下游组件的渲染分析。这个改进对于分析懒加载场景特别有用。

建议:先用 Profiler 跑一遍完整的用户操作流程(页面加载 → 交互 → 滚动 → 搜索),记录下渲染次数最多的前 5 个组件,针对性优化。盲目优化不在性能瓶颈路径上的组件,是浪费时间。

六、实战建议:优化要有节奏感

很多人优化 React 性能的方式是——先把所有组件都包上 React.memo,useMemo/useCallback 全部安排上。这是过犹不及。

我的经验是分三步走:

  1. 先 profiling——找到真正的瓶颈在哪。别猜,用数据说话。
  2. 优先处理渲染次数多的——一个渲染了 100 次的组件,优化掉 90 次,价值远大于优化一个只渲染了 2 次的组件。
  3. 避免过度优化——memo/useCallback 本身有开销,组件轻量且渲染不频繁时,不优化反而更划算。

另外,如果你的项目还在用 class 组件,考虑迁移到函数组件 + hooks。函数组件在 memo、useMemo 这些工具的配合下,优化粒度比 class 组件的 shouldComponentUpdate 细得多。

总结

这 7 个方案不是孤立的——经常是组合使用才能达到最佳效果。比如一个高频更新的列表,你可能需要同时用 key 优化 + 虚拟列表 + React.memo + 状态扁平化。但核心原则就一条:让真正需要更新的组件更新,不该更新的组件保持原样

性能优化是个持续的事,不是一次性工程。产品迭代中随时可能出现新的性能问题,保持 profiling 的习惯比记住所有优化技巧更重要。


☘️ 顺手推荐一个我常用的工具站——CloverTools点击访问),收录了前端开发中常用的代码格式化、压缩、转换工具,React 项目里经常需要处理 JS/JSON 格式化,配合使用效率翻倍。

💡 遇到同类问题?用工具快速解决

试试这些配套工具,无需注册,打开即用

Python 代码格式化

常见问题

Q: 如何使用 react重渲染优化7种方案 相关工具?
A: 这类工具一般有明确的输入框和输出框,按提示输入内容,点击对应按钮即可得到结果。建议先用简单示例测试功能是否正常,再处理实际数据。
Q: react重渲染优化7种方案 适合在什么场景使用?
A: 根据具体工具类型决定。格式转换工具适合处理第三方数据,编码工具适合加密传输,压缩工具适合文件上传前处理。多积累工具使用经验,遇到问题时能快速判断用哪个工具解决。
Q: 有没有更好的替代工具?
A: 不同工具有不同侧重,重点是理解原理。可以同时安装多个类似工具,实际使用中对比效果,选择最顺手的一个。随着使用经验增加,你也能判断工具的好坏。