【译】【React 】深度解析:为什么你的组件总是重新渲染?

80 阅读21分钟

🔗 原文链接:Why React Re-Renders

👨‍💻 原作者:Josh W. Comeau
📅 发布时间:2022年8月16日,最后更新:2025年5月9日

📢 译者说明

⚠️ 关于本译文
本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明:文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。

📖 建议结合英文原版阅读,获得最完整的学习体验!

🚀 开篇:为什么 React 重新渲染让人困惑?

说实话,我在 React 江湖上混了好几年,才真正搞明白这个"重新渲染"到底是个什么鬼!😅 就像学开车,一开始只知道踩油门刹车,后来才明白发动机是怎么工作的。

我想这对很多 React 开发者来说都是事实。我们理解得足够多,能够应付工作,但如果你问一群 React 开发者一个问题,比如"什么会触发 React 中的重新渲染?",你可能会得到一堆五花八门的答案,就像问"为什么天会下雨"一样,答案从"云朵哭了"到"大气循环"都有!

关于这个话题有很多误解,这可能导致很多不确定性。如果我们不理解 React 的渲染周期,我们怎么能理解如何使用 React.memo,或者什么时候应该用 useCallback 包装我们的函数呢??

在这个教程中,我们将一起揭开 React 重新渲染的神秘面纱,构建一个清晰的心理模型。我们还将学习如何用 React devtools 这个"侦探工具"来找出为什么特定组件重新渲染。

目标受众

本教程是为帮助初级/中级 React 开发者更舒适地使用 React 而写的。如果你刚开始学习 React,你可能希望先收藏这个,几周后再回来!就像学武功,先练基本功,再学绝招!


🧠 核心概念:React 的"心脏"是如何跳动的?

所以,让我们从一个基本事实开始:React 中的每个重新渲染都始于状态变化。 这是 React 中组件重新渲染的唯一"触发器",就像心脏跳动需要血液流动一样!*在过去,有一个"forceUpdate()"方法也会触发重新渲染,但现在不存在了,就像过时的武功秘籍一样被淘汰了

现在,这听起来可能不对...毕竟,当组件的 props 改变时,组件不会重新渲染吗?那 context 呢??别急,让我们慢慢来,就像侦探破案一样,一步步揭开真相!

事情是这样的:当组件重新渲染时,它也会重新渲染所有它的后代。 就像多米诺骨牌,一旦第一块倒下,后面的都会跟着倒下!

让我们看一个例子:

import React from 'react';

function App() {
  return (
    <>
      <Counter />
      <footer>
        <p>Copyright 2022 Big Count Inc.</p>
      </footer>
    </>
  );
}

function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <main>
      <BigCountNumber count={count} />
      <button
        onClick={() => setCount(count + 1)}
      >
        Increment
      </button>
    </main>
  );
}

function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">
        Count:
      </span>
      {count}
    </p>
  );
}

export default App;

image.png

在这个例子中,我们有 3 个组件:顶部的 App,它渲染 Counter,而 Counter 渲染 BigCountNumber

在 React 中,每个状态变量都附加到特定的组件实例。在这个例子中,我们有一个状态片段,count,它与 Counter 组件相关联。

每当这个状态改变时,Counter 就会重新渲染。因为 BigCountNumberCounter 渲染,它也会重新渲染。

这里有一个交互式图表,展示了这个机制在行动中的样子。点击"Increment"按钮来触发状态变化:

钉钉录屏_2025-08-13 200000.gif

(绿色闪烁表示组件正在重新渲染。)

好的,让我们清除大误解 #1每当状态变量改变时,整个应用程序都会重新渲染。

我知道一些开发者相信 React 中的每个状态变化都会强制进行应用程序范围的渲染,但这不是真的!这就像以为打喷嚏会传染整个城市一样夸张。重新渲染只影响拥有状态的组件 + 它的后代(如果有的话)。在这个例子中,App 组件在 count 状态变量改变时不必重新渲染,就像楼下邻居家装修不会影响你家一样。

但是,与其把这个作为规则来记忆,让我们退后一步,看看我们是否能弄清楚为什么它这样工作。

React 的主要工作是让应用程序 UI 与 React 状态保持同步,就像一个尽职的管家,时刻关注主人的需求变化。重新渲染的目的是弄清楚需要改变什么

让我们考虑上面的Counter例子。当应用程序首次挂载时,React 就像一位画家,渲染我们所有的组件,并得出以下关于 DOM 应该是什么样子的草图:

<main>
  <p>
    <span class="prefix">Count:</span>
    0
  </p>
  <button>
    Increment
  </button>
</main>
<footer>
  <p>Copyright 2022 Big Count Inc.</p>
</footer>

当用户点击按钮时,count 状态变量从 0 翻转到 1。这如何影响 UI?好吧,这就是我们希望通过做另一个渲染来学习的!

React 重新运行 CounterBigCountNumber 组件的代码,我们生成一个新的关于我们想要的 DOM 的草图:

<main>
  <p>
    <span class="prefix">Count:</span>
   
  </p>
  <button>
    Increment
  </button>
</main>
<footer>
  <p>Copyright 2022 Big Count Inc.</p>
</footer>

每次渲染都是一个快照,就像相机拍摄的照片,显示了基于当前应用程序状态 UI 应该是什么样子。

React 玩一个"找不同"的游戏来弄清楚这两个快照之间有什么变化,类似于玩"大家来找茬"一样!在这种情况下,它看到我们的段落有一个文本节点从 0 变成了 1,所以它编辑文本节点以匹配快照。满意于工作完成,React 就像完成任务的员工一样,安定下来等待下一个状态变化。

这就是 React 的核心循环。 就像心脏的跳动,有规律、有节奏,永不停息!

有了这个框架,让我们再次看看我们的渲染图: 钉钉录屏_2025-08-13 200000.gif

我们的 count 状态与 Counter 组件相关联。因为数据不能在 React 应用程序中"向上"流动,我们知道这个状态变化不可能影响 <App />。所以我们不需要重新渲染那个组件。

但我们确实需要重新渲染 Counter 的子组件,BigCountNumber。这是实际显示 count 状态的组件。如果我们渲染它,我们就不会知道我们段落的文本节点应该从 0 变成 1。我们需要在我们的草图中包含这个组件。

重新渲染的目的是弄清楚状态变化应该如何影响用户界面。所以我们需要重新渲染所有可能受影响的组件,以获得准确的快照。

💡 画外音
这里有个关键理解:React 的重新渲染不是惩罚,而是必要的计算过程。就像解数学题,你需要重新计算才能得到新答案,而不是简单地在旧答案上涂改。这就像做饭,你不能把昨天的剩菜热一下就说是新菜,需要重新烹饪!


🎭 不是关于 props 的问题:又一个"美丽的误会"!

好的,让我们谈谈大误解 #2组件会因为它的 props 改变而重新渲染。

这个误解就像以为"下雨是因为云朵哭了"一样可爱,但事实并非如此!让我们用一个更新的例子来探索,就像侦探一样一步步揭开真相。

在下面的代码中,我们的"Counter"应用程序被赋予了一个全新的组件,Decoration,就像给房子加了个装饰品:

// App.js
import React from 'react';

import Counter from './Counter';

function App() {
  return (
    <>
      <Counter />
      <footer>
        <p>Copyright 2022 Big Count Inc.</p>
      </footer>
    </>
  );
}

export default App;

// Counter.jsx
import React from 'react';

import Decoration from './Decoration';
import BigCountNumber from './BigCountNumber';

function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <main>
      <BigCountNumber count={count} />
      <button
        onClick={() => setCount(count + 1)}
      >
        Increment
      </button>

      {/* 👇 This fella is new 👇 */}
      <Decoration />
    </main>
  );
}

export default Counter;

// Decoration.jsx
function Decoration() {
  return (
    <div className="decoration">
      ⛵️
    </div>
  );
}

export default Decoration;

// BigCountNumber.jsx
function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">Count:</span>
      {count}
    </p>
  );
}

export default BigCountNumber;

image.png (因为在一个大文件中放置所有组件变得有点拥挤,所以我重新组织了。但整体组件结构是相同的,除了新的 Decoration 组件。)

我们的计数器现在在角落有一个可爱的小帆船,由 Decoration 组件渲染,它不依赖于 count,所以当 count 改变时它可能不会重新渲染,对吧?

好吧,呃,不完全是这样。这就像你以为"只要我不动,别人就不会注意到我"一样天真!

钉钉录屏_2025-08-13 201937.gif

当组件重新渲染时,它试图重新渲染所有后代,无论它们是否通过 props 传递特定的状态变量。

现在,这似乎违反直觉。为什么 Decoration 需要重新渲染?它甚至不知道 count 是什么!这就像为什么你感冒了,你室友也要跟着戴口罩一样奇怪!

答案是这样的:React 很难 100% 确定 <Decoration> 是否直接或间接依赖于 count 状态变量。

🧠 画外音
这里有个重要的概念:React 的重新渲染是自上而下的瀑布流。就像多米诺骨牌,一旦父组件倒下,所有子组件都会跟着倒下。也像瀑布一样,水从高处流下,下面的水池都会受到影响!

React 需要重新渲染 Decoration 的原因是:它不知道 Decoration 是否依赖于 count

想象一下,如果 Decoration 组件内部有这样的代码:

function Decoration() {
 // 这个组件实际上 DOES 依赖于 count!
 const count = React.useContext(CountContext);
 
 return (
   <div className="decoration">
     {count > 5 ? '🎈' : '🚢'}
   </div>
 );
}

如果 React 跳过重新渲染 Decoration,我们就不会看到装饰从帆船变成气球!这就像错过了魔术表演最精彩的部分一样遗憾!

所以 React 采取安全的方法:重新渲染所有后代,以防万一。 就像"宁可错杀一千,不可放过一个"的谨慎策略。

这也是为什么 React 被称为"声明式"而不是"命令式"的原因。我们告诉 React "UI 应该是什么样子",而不是"如何改变 UI"。就像点菜时告诉服务员"我要一份宫保鸡丁",而不是"请先切鸡丁,再放花生米,最后勾芡"一样!

在理想世界中,React 组件应该总是"纯"的。纯组件是指当给定相同的 props 时,总是产生相同 UI 的组件。

但在现实世界中,我们的很多组件都是不纯的。创建一个不纯的组件出奇地容易:

function CurrentTime() {
  const now = new Date();
  return (
    <p>It is currently {now.toString()}</p>
  );
}

这个组件每次渲染时都会显示不同的值,因为它依赖于当前时间!就像时钟一样,每分每秒都在变化。

这个问题的更隐蔽版本与 refs 有关。如果我们传递一个 ref 作为 prop,React 就无法判断我们是否在上次渲染后修改了它。所以它选择重新渲染,以确保安全。

React 的首要目标是确保用户看到的 UI 与应用程序状态保持"同步"。因此,React 宁愿渲染过多,也不愿冒险向用户显示过时的 UI。这就像宁愿多跑几趟确认,也不愿错过重要的信息一样!

所以,回到我们的误解:props 与重新渲染无关。我们的 <BigCountNumber> 组件不是因为 count prop 改变而重新渲染的。

当组件重新渲染时,是因为它的一个状态变量被更新了,这个重新渲染会像瀑布一样一直向下级联,以便 React 能够填充这个新草图的细节,捕获一个新的快照。

这是标准的操作程序,但有一种方法可以稍微调整它。


🎯 创建纯组件:给组件装上"智能开关"!

好的,所以重新渲染是不可避免的,就像太阳每天都要升起一样。但是如果我们能跳过某些组件的重新渲染会怎样呢?这就像给房间装上智能开关,只有需要的时候才开灯!

你可能熟悉 React.memo,或者 React.PureComponent 类组件。这两个工具允许我们忽略某些重新渲染请求。

这就是它的样子:

function Decoration() {
  return (
    <div className="decoration">
      ⛵️
    </div>
  );
}
export default React.memo(Decoration);

通过用 React.memo 包装我们的 Decoration 组件,我们告诉 React:"嘿,我知道这个组件很乖。除非它的 props 改变,否则你不需要重新渲染它。"

这使用了一种称为记忆化(memoization)的技术。

虽然缺少字母 R,但我们可以把它想象成"记忆化"。想法是 React 会记住之前的快照。如果没有 props 改变,React 会重用那个过时的快照,而不是费事地生成一个全新的快照。就像记住了一个数学题的答案,下次遇到同样的题目直接写答案就行。

让我们假设我用 React.memo 助手包装了 BigCountNumberDecoration。这会影响重新渲染的方式:

钉钉录屏_2025-08-14 113637.gif

count 改变时,我们重新渲染 Counter,React 会尝试渲染两个后代组件。

因为 BigCountNumber 接受 count 作为 prop,而且那个 prop 已经改变,所以 BigCountNumber 被重新渲染。但是因为 Decoration 的 props 没有改变(因为它没有任何 props),所以使用了原始快照。

我喜欢假装 React.memo 有点像懒惰的摄影师。如果你要求它拍摄 5 张完全相同的东西的照片,它会拍摄 1 张照片并给你 5 份副本。只有当你的指示改变时,摄影师才会拍新照片。就像复印机一样,一次扫描,多次打印!

这里有一个实时代码版本,如果你想自己试试的话。每个记忆化组件都添加了一个 console.info 调用,所以你可以在控制台中看到每个组件确切何时渲染:

💡 译者注:下面这块的代码请跳到原文查看~

钉钉录屏_2025-08-14 114644.gif

你可能会想:为什么这不是默认行为??这不是我们大多数时候想要的吗?如果我们跳过不需要渲染的组件,性能肯定会提高吧?

我认为作为开发者,我们倾向于高估重新渲染的成本。在我们上面的 Decoration 组件的情况下,重新渲染是闪电般快速的,就像眨眼一样快。

如果一个组件有一堆 props 但没有很多后代,检查是否有任何 props 改变实际上可能比重新渲染组件更慢。

💡 译者注:原文作者表示没有这个说法的具体来源,但他看到过像 Dan Abramov 这样的知名开发者在社交媒体上提出过这个观点。

因此,记忆化我们创建的每个组件会适得其反。React 被设计为可以非常快速地捕获这些快照!但在特定情况下,对于有很多后代的组件或做大量内部工作的组件,这个助手可以帮上很多忙。就像给每个房间都装智能开关,虽然高级,但可能有点过度了。

😄 这在未来可能会改变!

React 团队正在积极研究是否有可能在编译步骤中自动记忆化代码。它仍处于研究阶段,但早期实验看起来很有希望。就像自动驾驶一样,虽然还在测试,但未来可期!

🎭 画外音
React.memo 就像给组件装了个"智能门卫",只有真正需要的时候才放行,否则一律拦截。这大大提高了性能!就像给公司装了门禁系统,只有员工才能进,闲杂人等一律挡在门外!


🔄 Context 呢?这个"隐形杀手"!

我们还没聊过 context 呢,不过好消息是,它并没有让事情变得太复杂!

简单来说,如果组件的状态变了,它的所有"孩子"都会重新渲染。所以,不管你是通过 context 把状态传给所有后代,还是用其他方式,结果都一样——那些组件都得重新渲染!就像你感冒了,全家人都得戴口罩一样,谁也跑不掉。用 context 传状态并不会改变这个事实,该重新渲染的还是得重新渲染!

现在,对于纯组件来说,context 就像"隐形的 props",或者说是"藏在肚子里的 props"。

让我们看个例子。这里有个消费 UserContext 的纯组件:

const GreetUser = React.memo(() => {
  const user = React.useContext(UserContext);
  if (!user) {
    return "Hi there!";
  }
  return `Hello ${user.name}!`;
});

在这个例子中,GreetUser 看起来是个没有 props 的纯组件,但它其实有个"隐形"的依赖——藏在 React 状态里,通过 context 传递的用户信息。就像你表面上没带钱包,但口袋里其实有钱一样!

如果用户状态变了,就会触发重新渲染,GreetUser 会生成一个新的"照片",而不是用上次的旧照片。React 很聪明,它能看出来这个组件在"偷吃"这个 context,所以把它当作一个 prop 来处理。

这基本上等同于这样:

const GreetUser = React.memo(({ user }) => {
  if (!user) {
    return "Hi there!";
  }
  return `Hello ${user.name}!`;
});

来玩玩这个可以实时运行的例子:

import React from 'react';

const UserContext = React.createContext();

function UserProvider({ children }) {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    // Pretend that this is a network request,
    // fetching user data from the backend.
    window.setTimeout(() => {
      setUser({ name: 'Kiara' });
    }, 1000)
  }, [])

  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}

function App() {
  return (
    <UserProvider>
      <GreetUser />
    </UserProvider>
  );
}

const GreetUser = React.memo(() => {
  const user = React.useContext(UserContext);
  console.log('Render with user', user);

  if (!user) {
    return "Hi there!";
  }

  return `Hello ${user.name}!`;
});

export default App;

钉钉录屏_2025-08-14 133612.gif

注意,这只发生在纯组件消费 context 时,使用 React.useContext hook。你不必担心 context 破坏一堆不试图消费它的纯组件。

🚨 画外音
这是 React 中一个重要的性能陷阱!Context 虽然方便,但会破坏纯组件的优化,就像方便面虽然好吃,但吃多了对身体不好一样!所以在使用 context 时要格外小心。


🔍 使用 React Devtools 进行分析:你的私人侦探!

如果你使用 React 一段时间了,你可能有过试图弄清楚为什么特定组件重新渲染的令人沮丧的经历。在真实世界的情况下,这通常一点都不明显!幸运的是,React Devtools 可以提供帮助。

首先,你需要下载 React Devtools 浏览器扩展。它目前在 Chrome 和 Firefox 上可用。为了本教程的目的,我假设你使用的是 Chrome,尽管说明不会有太大差异。

Ctrl + Shift + I(或在 MacOS 上用 + Option + I)打开 devtools。你应该看到两个新标签出现:

image.png

我们感兴趣的是"Profiler"。选择那个标签。

点击小齿轮图标,启用标记为"在分析时记录每个组件渲染的原因"的选项:

钉钉录屏_2025-08-14 140819.gif

一般流程是这样的:

  1. 通过点击小蓝色"记录"圆圈开始记录。
  2. 在你的应用程序中执行一些操作。
  3. 停止记录。
  4. 查看记录的快照以了解更多关于发生了什么。

每次渲染都被捕获为单独的快照,你可以使用箭头浏览它们。关于组件为什么渲染的信息在侧边栏中可用:

钉钉录屏_2025-08-14 140946_20250814141043.gif

通过点击到你感兴趣的组件,你可以看到特定组件重新渲染的确切原因。在纯组件的情况下,它会让我们知道哪个 prop(s) 负责这次更新。

我个人不经常使用这个工具,但当我使用时,它就是一个救星!

🎨 高亮重新渲染

还有一个技巧:React profiler 有一个选项,你可以高亮重新渲染的组件。

这是问题中的设置:

image.png

启用此设置后,你应该看到绿色矩形在重新渲染的组件周围闪烁:

钉钉录屏_2025-08-14 141157_20250814141218.gif


🚀 深入探索:React 的"小秘密"

当你开始使用 profiler 时,你会注意到一件事:有时,纯组件即使看起来什么都没改变也会重新渲染!这就像你以为"我没动,别人就看不见我"一样天真!

React 有个微妙而令人困惑的地方:组件其实就是 JavaScript 函数。当我们渲染组件时,实际上就是在调用那个函数。(在类组件的情况下,我们调用的是与类关联的 render 方法,道理是一样的。)

这意味着在 React 组件内定义的任何东西,都会在每次渲染时重新创建。就像每次做饭都要重新准备食材一样!

举个简单的例子:

function App() {
  const dog = {
    name: 'Spot',
    breed: 'Jack Russell Terrier'
  };

  return (
    <DogProfile dog={dog} />
  );
}

每次我们渲染这个 App 组件时,都会生成一个全新的对象。这会破坏我们的纯组件;这个 DogProfile 子组件无论是否用 React.memo 包装,都会重新渲染!就像每次都给同一个演员发新的身份证一样,虽然人还是那个人,但身份证号变了!

想继续学习? 我写了第二篇博客文章,Understanding useMemo and useCallback,它更深入地挖掘了记忆化和优化的概念。我们扩展了在这篇文章中构建的心理模型,学习如何使用 React 的两个最神秘的 hooks。

译者注:上面说的那篇文章,我已经进行了翻译,可以直接点击下面的连接跳转阅读:

👉 终极指南:彻底搞懂 React 的 useMemo 和 useCallback!(译)

我还有个坦白: 这些教程都来自我全新的课程"React 的乐趣"。

我使用 React 已经超过十年了,学到了很多如何有效使用它的知识。我绝对热爱用 React 工作;我尝试过几乎所有前端框架,但没有什么让我感觉像 React 一样高效。

在《React 的乐趣》中,我们会构建一个关于 React 真正如何工作的心理模型,深入挖掘像这个教程中这样的概念。不过,与博客文章不同,我的课程采用"多模态"方法,混合了书面内容、视频内容、练习、交互式探索,甚至一些迷你游戏!

你可以在这里了解更多课程信息:


🎁 性能提示:React 性能优化的"锦囊妙计"!

React 中的性能优化是个大话题,我完全可以写好几篇博客文章来聊这个。希望这个教程能帮你打好基础,让你更好地学习 React 性能!

不过,我还是想分享几个我在 React 性能优化上学到的小技巧,就像武林高手的独门秘籍一样珍贵:

  • React Profiler 的数字不可信:它显示渲染花费的毫秒数,但这个数字不太靠谱。我们通常在"开发模式"下分析,而 React 在"生产模式"下快得多。要真正了解你的应用有多快,应该用"Performance"标签测试部署后的生产应用。这样你看到的才是真实世界的数字,不仅仅是重新渲染,还有布局和绘制变化。
  • 在低端设备上测试:我强烈建议在低端硬件上测试你的应用,看看第 90 百分位用户的体验如何。具体设备取决于你构建的产品,但对于这个博客,我经常在小米 Redmi 8 上测试,这是几年前在印度很火的预算手机。
  • Lighthouse 分数仅供参考:Lighthouse 的性能分数不是真实用户体验的准确反映。我宁愿相信实际使用应用的感受,也不愿迷信任何自动化工具的数据。
  • 关注"加载后"体验:几年前我在 React Europe 做过一个关于 React 性能的演讲!它更关注"加载后"体验,这是很多开发者容易忽视的地方。你可以在 YouTube 上找到它
  • 不要过度优化! 学 React profiler 时,很容易陷入优化狂欢,想着尽可能减少渲染次数...但说实话,React 默认就已经很优化了。这些工具最好用来解决实际的性能问题,而不是为了优化而优化。

💡 画外音
第90百分位(90th percentile)是个统计学概念,意思是90%的用户比这个快,10%的用户比这个慢。在性能测试中,我们关注第90百分位是为了了解普通用户(而不是高端用户)的真实体验。比如测试网页加载时间:第50百分位是1.2秒,第90百分位是2.8秒,这意味着90%的用户都能在2.8秒内加载完成,这个性能水平对大部分用户来说是可以接受的。


🎯 总结:恭喜你成为 React 重新渲染的"专家"!

恭喜你!现在你已经理解了 React 重新渲染的核心机制,就像从"菜鸟"升级到了"高手"!

有了这些知识,你现在可以:

  • 理解为什么组件会重新渲染
  • 使用 React.memo 优化性能
  • 避免常见的性能陷阱
  • 使用正确的工具来调试性能问题

🚀 画外音
理解 React 的重新渲染机制是成为高级 React 开发者的关键一步。这就像理解了汽车的发动机原理,让你能更好地驾驶和维护它!也像学武功,理解了内功心法,招式才能发挥出真正的威力!


👏 感谢阅读:让我们一起在 React 的江湖中闯荡!


🔖 关键词标签:React、重新渲染、性能优化、React.memo、React Devtools、前端开发