React 性能瓶颈定位面试题——原理深挖版

一、面试题:React 应用出现性能问题时,你会如何系统地定位性能瓶颈?

核心思路(一句话)

先复现问题并确定性能指标,再通过 React 组件层、浏览器运行时层、网络与页面加载层逐层缩小范围,最后定位到具体代码并通过优化前后数据验证效果。

主要矛盾

性能问题最大的矛盾不是"不会优化",而是:

不知道到底哪里慢、为什么慢,就盲目使用 memo、缓存、懒加载等优化手段。

次要矛盾

具体表现可能来自:

  1. React 不必要的重新渲染;
  2. 单个组件 render 计算量过大;
  3. JavaScript 长任务阻塞主线程;
  4. 大量 DOM 操作;
  5. 强制同步布局;
  6. 网络请求或者资源过大;
  7. JavaScript 包体积过大;
  8. 图片、字体等资源加载过慢;
  9. 第三方库执行耗时;
  10. 内存泄漏或者频繁创建对象导致垃圾回收压力。

解决方案流程图

text 复制代码
用户反馈"页面卡 / 点击慢 / 滚动掉帧 / 首屏慢"
                ↓
          先定义问题类型
                ↓
     ┌──────────┼──────────┐
     ↓          ↓          ↓
  加载慢      交互卡顿    页面抖动
     ↓          ↓          ↓
 Lighthouse   Performance  Performance
     ↓          ↓          ↓
  网络/资源   Main Thread   Layout/Paint
                ↓
        是否与 React 渲染有关?
                ↓
              是
                ↓
        React DevTools Profiler
                ↓
    找到高耗时 / 高频更新组件
                ↓
       分析 props / state / context
                ↓
       是否存在不必要重新渲染?
          ↓                ↓
         是                否
          ↓                ↓
 React.memo / 稳定引用      查 render 计算
 useMemo / useCallback      查第三方库
 状态拆分 / 状态下沉         查 Effect
          ↓                ↓
             再次录制验证
                ↓
       优化前后数据对比
                ↓
             问题闭环

二、面试题:React DevTools Profiler 是什么?什么时候使用?

核心思路(一句话)

Profiler 解决的是"React 到底哪些组件渲染了、渲染了多久、为什么频繁渲染"的问题。

适用场景

例如:

text 复制代码
点击一个按钮
↓
父组件 state 改变
↓
整个页面大量组件重新渲染
↓
页面明显卡顿

此时首先应该怀疑 React 组件层面的更新问题。

Profiler 非常适合分析:

  • 哪些组件发生了渲染;
  • 哪些组件渲染耗时较高;
  • 哪次更新产生了大量组件渲染;
  • 某个组件是否频繁重新渲染;
  • 不同交互对应哪些 commit;
  • 优化前后渲染耗时有没有下降。

解决方案流程图

text 复制代码
打开 React Developer Tools
        ↓
进入 Profiler
        ↓
点击 Record
        ↓
执行真实用户操作
        ↓
停止 Record
        ↓
选择一次 Commit
        ↓
查看组件渲染情况
        ↓
Flamegraph
   + 
Ranked
   +
组件更新信息
        ↓
找到异常组件
        ↓
检查 props / state / context
        ↓
寻找重新渲染原因

底层实现原理

React 的更新大致可以理解为:

text 复制代码
状态更新
  ↓
创建/调度更新
  ↓
Render Phase
  ↓
计算新的 React Fiber Tree
  ↓
比较前后节点
  ↓
Commit Phase
  ↓
更新 DOM / 执行相关副作用

Profiler 的价值就在于:

把 React 内部这次更新过程转换成开发者能够观察的数据。

需要特别区分:

Render ≠ DOM 更新

一个组件执行函数或者 render 方法,并不代表一定产生 DOM 修改。

例如:

jsx 复制代码
function Counter({ count }) {
  return <div>{count}</div>;
}

父组件重新渲染可能导致 Counter 重新执行。

但是 React 比较新旧 Fiber 后发现 DOM 没有实际变化,就可能不会产生对应 DOM 修改。

因此:

text 复制代码
组件重新渲染
        ≠
DOM 一定发生变化

这是性能面试非常容易被追问的点。


三、面试题:如何阅读 React Profiler 的 Flamegraph 和 Ranked?

核心思路(一句话)

Flamegraph 看一次更新的组件层级和时间分布,Ranked 看当前 Commit 中哪些组件耗时最高。

Flamegraph

可以理解成:

text 复制代码
App
├── Header
├── Sidebar
└── Content
    ├── List
    │   ├── Item
    │   ├── Item
    │   └── Item
    └── Footer

每个节点对应一个组件。

重点观察:

text 复制代码
组件
↓
渲染持续时间
↓
子组件耗时
↓
更新关系

一个重要纠正

不要简单背:

"火焰图越宽,说明这个组件自己越慢。"

更加准确的说法是:

节点持续时间反映该组件及其渲染子树相关工作的时间,因此需要进一步区分组件自身计算耗时和子组件贡献。

否则面试官继续问:

"父组件很宽,但是它自己其实没做什么计算,为什么?"

你就很容易答不上来。


Ranked

Ranked 更适合回答:

这次更新里面,哪些组件耗时最高?

例如:

text 复制代码
Item#10      35ms
List         20ms
Item#20      5ms
Header       2ms
Footer       1ms

此时可以快速锁定:

text 复制代码
Item#10

然后进一步分析:

text 复制代码
为什么 Item#10 慢?

而不是直接开始加 React.memo


四、面试题:Profiler 发现大量组件重新渲染,你会如何定位原因?

核心思路(一句话)

先判断"为什么渲染",再判断"是否值得阻止渲染",最后才决定是否使用缓存优化。

解决方案流程图

text 复制代码
组件重新渲染
      ↓
检查自身 state
      ↓
是否 state 改变?
 ┌────┴────┐
 是        否
 ↓          ↓
正常更新   检查 props
             ↓
       props 是否变化?
        ┌────┴────┐
       是        否
       ↓          ↓
引用变化?      检查 Context
       ↓          ↓
对象/函数       Context 更新?
每次创建?          ↓
       ↓          是
React.memo       ↓
比较失败       状态设计问题

高频问题 1:对象引用每次变化

错误写法:

jsx 复制代码
function Parent() {
  const [count, setCount] = React.useState(0);

  const user = {
    name: "张三",
    age: 20,
  };

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

      <UserCard user={user} />
    </>
  );
}

即使用户数据逻辑上没有变化:

text 复制代码
第一次:
user !== 第二次的 user

因为每次 render 都创建了一个新对象。

如果:

jsx 复制代码
const UserCard = React.memo(function UserCard({ user }) {
  console.log("UserCard render");

  return <div>{user.name}</div>;
});

React.memo 默认进行浅比较:

text 复制代码
旧 user === 新 user

结果:

text 复制代码
false

所以还是会重新渲染。


优化方案

jsx 复制代码
import React from "react";

/**
 * UserCard 使用 React.memo。
 *
 * React.memo 默认会对 props 做浅比较。
 * 如果 user 的引用没有变化,就可以跳过这次组件重新渲染。
 */
const UserCard = React.memo(function UserCard({ user }) {
  console.log("UserCard render");

  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
    </div>
  );
});

export default function App() {
  /**
   * count 改变时,App 会重新执行。
   */
  const [count, setCount] = React.useState(0);

  /**
   * useMemo 可以保证:
   *
   * 只要 name 和 age 没有变化,
   * user 对象就复用之前的引用。
   *
   * 注意:
   * useMemo 不是为了"让所有代码都更快",
   * 它是为了在引用稳定本身具有价值时进行缓存。
   */
  const user = React.useMemo(() => {
    return {
      name: "张三",
      age: 20,
    };
  }, []);

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

      <UserCard user={user} />
    </div>
  );
}

五、面试题:React.memo、useMemo、useCallback 应该什么时候使用?

核心思路(一句话)

它们不是"性能开关",而是用来降低重复计算或稳定引用;必须先通过性能分析证明缓存有收益。

三者解决的问题

API 解决什么问题
React.memo 避免组件在 props 没变化时重新渲染
useMemo 缓存计算结果
useCallback 缓存函数引用

1. React.memo

jsx 复制代码
const ListItem = React.memo(function ListItem({ item }) {
  return <div>{item.name}</div>;
});

只有当 props 的浅比较没有发现变化时,才可能跳过重新渲染。


2. useMemo

jsx 复制代码
const filteredList = React.useMemo(() => {
  return list.filter((item) => item.name.includes(keyword));
}, [list, keyword]);

适合:

text 复制代码
计算成本高
+
依赖变化频率低
+
结果可以复用

3. useCallback

jsx 复制代码
const handleDelete = React.useCallback((id) => {
  deleteItem(id);
}, []);

最典型的使用场景是:

text 复制代码
父组件
 ↓
React.memo 子组件
 ↓
子组件接收函数 props

如果函数每次都是新引用:

jsx 复制代码
const handleDelete = (id) => {
  deleteItem(id);
};

那么:

text 复制代码
旧函数 !== 新函数

即使逻辑完全相同,也可能导致 React.memo 无法跳过渲染。


但是不要滥用

错误理解:

"所有函数都应该 useCallback。"

这是不正确的。

因为缓存本身也有:

text 复制代码
依赖管理成本
+
内存成本
+
比较成本
+
代码复杂度

真正的原则应该是:

text 复制代码
先发现性能问题
      ↓
确认重复渲染
      ↓
确认引用变化是原因
      ↓
确认子组件渲染成本较高
      ↓
再使用 useCallback / useMemo / React.memo

六、面试题:Chrome DevTools Performance 面板和 React Profiler 有什么区别?

核心思路(一句话)

React Profiler 看"React 在干什么",Performance 看"整个浏览器主线程和渲染流水线在干什么"。

对比

工具 核心关注点
React Profiler React 组件渲染
Performance 浏览器整体运行过程
Lighthouse 页面质量和性能审计
Why Did You Render 辅助定位不必要重新渲染

Performance 关注的范围更大

例如:

text 复制代码
用户点击
 ↓
Event Handler
 ↓
JavaScript
 ↓
React Update
 ↓
Style Recalculation
 ↓
Layout
 ↓
Paint
 ↓
Composite

如果页面卡顿:

text 复制代码
可能是 React
也可能不是 React

例如:

text 复制代码
React render:5ms

第三方图表库:
JavaScript:120ms

这时候你光看 React Profiler 很可能找不到真正的瓶颈。


七、面试题:如何使用 Performance 面板定位页面卡顿?

核心思路(一句话)

录制真实用户操作,沿着 Main Thread 找长任务,再通过调用栈追到具体 JavaScript 代码,同时检查 Layout、Paint、网络和 React Performance tracks。

解决方案流程图

text 复制代码
打开 Chrome DevTools
        ↓
Performance
        ↓
开始录制
        ↓
执行问题操作
        ↓
停止录制
        ↓
观察时间线
        ↓
Main Thread
        ↓
寻找长任务
        ↓
展开调用栈
        ↓
找到具体函数
        ↓
继续分析
 ┌──────┼────────┐
 ↓      ↓        ↓
JS     Layout   Paint
 ↓      ↓        ↓
计算   强制布局  绘制压力

Long Task

主线程长时间执行 JavaScript:

text 复制代码
JavaScript
████████████████████████████████████████

如果一个任务超过约 50ms,就可能成为 Long Task。

例如:

jsx 复制代码
function handleClick() {
  // 模拟非常耗时的同步计算。
  // 在真实项目中可能来自:
  // 复杂数据处理、JSON 转换、排序、第三方库计算等。
  const start = performance.now();

  while (performance.now() - start < 100) {
    // 故意阻塞主线程 100ms。
  }

  console.log("计算结束");
}

执行期间:

text 复制代码
JavaScript 主线程
       ↓
持续执行 100ms
       ↓
浏览器无法及时处理其他任务
       ↓
用户感觉"点击没反应"

八、面试题:什么是强制同步布局?如何通过 Performance 定位?

核心思路(一句话)

避免在同一个 JavaScript 任务中反复交替读写布局信息,让浏览器被迫频繁执行 Layout。

错误示例

jsx 复制代码
function moveBoxes(boxes) {
  boxes.forEach((box) => {
    /**
     * 写操作:修改 DOM。
     */
    box.style.width = "200px";

    /**
     * 读操作:读取布局信息。
     *
     * 如果前面的 DOM 修改导致布局失效,
     * 浏览器为了返回准确的 offsetHeight,
     * 可能必须立即重新计算布局。
     */
    console.log(box.offsetHeight);
  });
}

如果大量循环:

text 复制代码
写 DOM
↓
读布局
↓
强制 Layout
↓
写 DOM
↓
读布局
↓
强制 Layout

就可能形成严重的 Layout Thrashing。

更好的方案

把读操作和写操作分离:

jsx 复制代码
function measureAndUpdate(boxes) {
  /**
   * 第一阶段:统一读取布局信息。
   *
   * 此阶段尽量不要修改 DOM。
   */
  const heights = boxes.map((box) => {
    return box.offsetHeight;
  });

  /**
   * 第二阶段:统一写入 DOM。
   *
   * 避免读写交替导致浏览器反复重新计算布局。
   */
  boxes.forEach((box, index) => {
    box.style.width = `${heights[index]}px`;
  });
}

Performance 中重点观察:

text 复制代码
Main
 ↓
Layout
 ↓
Recalculate Style
 ↓
调用栈

如果看到大量 Layout 与 JavaScript 交替出现,就应该重点排查 DOM 读写模式。


九、面试题:如何使用 Performance.mark 和 Performance.measure 分析业务代码?

核心思路(一句话)

用 User Timing API 给关键业务流程打点,把"浏览器耗时"与"业务代码"建立对应关系。

完整示例:

jsx 复制代码
import React from "react";

export default function SearchPage() {
  const [keyword, setKeyword] = React.useState("");
  const [result, setResult] = React.useState([]);

  /**
   * 模拟一个耗时的数据处理过程。
   */
  function processData(data) {
    return data
      .filter((item) => item.name.includes(keyword))
      .sort((a, b) => a.name.localeCompare(b.name));
  }

  async function handleSearch() {
    /**
     * 标记整个搜索流程开始。
     *
     * Performance 面板可以看到这个标记。
     */
    performance.mark("search-start");

    /**
     * 模拟请求。
     */
    const response = await fetch("/api/search");

    const data = await response.json();

    /**
     * 对数据进行处理。
     */
    const processedData = processData(data);

    /**
     * 更新 React 状态。
     */
    setResult(processedData);

    /**
     * 标记搜索流程结束。
     */
    performance.mark("search-end");

    /**
     * 计算两个标记之间的耗时。
     *
     * 这样可以在 Performance 面板中看到:
     *
     * search-start
     *       ↓
     *   业务逻辑
     *       ↓
     * search-end
     *
     * 总耗时 = search-duration
     */
    performance.measure(
      "search-duration",
      "search-start",
      "search-end"
    );

    /**
     * 读取测量结果。
     */
    const entries = performance.getEntriesByName("search-duration");

    console.log("搜索耗时:", entries[0]?.duration);

    /**
     * 清理标记,避免长期累积。
     */
    performance.clearMarks("search-start");
    performance.clearMarks("search-end");
    performance.clearMeasures("search-duration");
  }

  return (
    <div>
      <input
        value={keyword}
        onChange={(event) => {
          setKeyword(event.target.value);
        }}
        placeholder="请输入关键词"
      />

      <button onClick={handleSearch}>
        搜索
      </button>

      <ul>
        {result.map((item) => (
          <li key={item.id}>
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

这个方法的价值是:

text 复制代码
Performance
      ↓
看到 search-duration
      ↓
展开调用栈
      ↓
定位业务函数
      ↓
知道究竟哪一段代码慢

十、面试题:Lighthouse 和 Performance 面板有什么区别?

核心思路(一句话)

Lighthouse 是"体检报告",Performance 是"事故现场监控录像"。

Lighthouse 适合回答

text 复制代码
这个页面整体性能怎么样?
↓
首屏加载怎么样?
↓
资源有没有优化?
↓
JavaScript 是否过大?
↓
图片是否过大?
↓
缓存是否合理?
↓
Core Web Vitals 表现如何?

Performance 适合回答

text 复制代码
为什么点击按钮会卡?
↓
为什么这 100ms 页面没有响应?
↓
是哪一个 JavaScript 函数执行太久?
↓
为什么发生大量 Layout?
↓
哪一次 React 更新导致卡顿?

十一、面试题:Lighthouse 中应该重点关注哪些性能指标?

核心思路(一句话)

性能指标必须和用户体验关联,重点观察加载、交互和视觉稳定性,而不是只看一个总分。

重点理解:

Largest Contentful Paint

表示主要内容加载出来的速度。

text 复制代码
页面开始加载
    ↓
主要内容出现
    ↓
Largest Contentful Paint

重点反映:

用户什么时候感觉"页面主要内容出来了"。


Interaction to Next Paint

重点衡量用户交互之后,页面多久能够给出下一次视觉反馈。

例如:

text 复制代码
用户点击按钮
      ↓
事件处理
      ↓
JavaScript 计算
      ↓
浏览器更新画面
      ↓
Interaction to Next Paint

它对于"点击以后页面卡不卡"非常重要。


Cumulative Layout Shift

衡量页面视觉布局是否发生意外移动。

例如:

text 复制代码
用户准备点击按钮
       ↓
图片突然加载
       ↓
内容向下移动
       ↓
用户点到了其他位置

这就是典型的布局稳定性问题。


Total Blocking Time

它是 Lighthouse 中非常重要的实验室指标。

核心关注:

text 复制代码
主线程是否被大量长任务阻塞

但是必须注意:

Total Blocking Time 不应该和 Core Web Vitals 混为一谈。


十二、面试题:Why Did You Render 是什么?什么时候使用?

核心思路(一句话)

Why Did You Render 用于开发阶段辅助回答"这个组件为什么又渲染了一次"。

典型问题:

jsx 复制代码
function Parent() {
  const [count, setCount] = React.useState(0);

  const user = {
    name: "张三",
  };

  return (
    <>
      <button onClick={() => setCount((value) => value + 1)}>
        {count}
      </button>

      <User user={user} />
    </>
  );
}

如果:

jsx 复制代码
const User = React.memo(function User({ user }) {
  return <div>{user.name}</div>;
});

那么:

text 复制代码
count 改变
 ↓
Parent render
 ↓
user 创建新对象
 ↓
user 引用发生变化
 ↓
React.memo 比较失败
 ↓
User render

Why Did You Render 的价值就是帮助开发者快速发现这种问题。


但是它不是首选性能工具

正确理解:

text 复制代码
发现页面卡顿
      ↓
Performance
      ↓
发现 React 渲染异常
      ↓
React Profiler
      ↓
怀疑具体 props / 引用导致重复渲染
      ↓
Why Did You Render

它更像:

组件重渲染原因分析器

而不是完整的性能分析平台。

另外,由于 Why Did You Render 依赖 React 内部行为,升级 React 后应该检查兼容性;其 GitHub 项目中也可以看到与 React 新版本、Vite 等环境相关的问题记录。


十三、面试题:React 性能优化是不是"发现重新渲染就使用 React.memo"?

核心思路(一句话)

不是,性能优化的目标是减少用户可感知的成本,而不是单纯减少 render 次数。

这是一个非常重要的高级面试点。

例如:

text 复制代码
组件渲染 100 次
每次 0.1ms

总成本:

text 复制代码
10ms

另一个组件:

text 复制代码
只渲染 2 次
每次 30ms

总成本:

text 复制代码
60ms

所以:

text 复制代码
render 次数

不是唯一指标。

真正应该关注:

text 复制代码
用户交互延迟
+
渲染耗时
+
主线程阻塞
+
布局/绘制耗时
+
网络加载
+
内存

十四、面试题:如果一个 React 页面卡顿,你的完整排查流程是什么?

核心思路(一句话)

先量化问题,再分层定位,最后针对真正的瓶颈进行优化并通过数据验证。

标准流程

text 复制代码
① 明确问题
   ↓
"点击卡顿?"
"滚动掉帧?"
"首屏慢?"
"切换页面慢?"
   ↓

② 建立基线
   ↓
记录操作耗时 / 性能指标
   ↓

③ Performance
   ↓
确认是不是主线程瓶颈
   ↓

④ React Profiler
   ↓
确认是不是 React 渲染瓶颈
   ↓

⑤ 分析根因
   ↓
├─ state 更新范围太大
├─ props 引用频繁变化
├─ Context 更新范围太大
├─ render 计算复杂
├─ Effect 触发连锁更新
├─ 大量 DOM
├─ 第三方库耗时
├─ Layout Thrashing
├─ 网络资源过大
└─ JavaScript 包体积过大

⑥ 针对性优化
   ↓

⑦ 再次录制
   ↓

⑧ 对比优化前后数据
   ↓

⑨ 确认没有引入新的问题

十五、面试题:React 性能问题常见根因有哪些?

核心思路(一句话)

React 性能问题本质上通常是"更新范围过大、计算成本过高、主线程被阻塞"三类问题。


第一类:更新范围过大

例如:

text 复制代码
App state
 ↓
整个 App 更新
 ↓
大量子组件重新执行

优化方向:

text 复制代码
状态下沉
状态拆分
组件拆分
React.memo
Context 拆分
选择器

第二类:单次计算成本过高

例如:

jsx 复制代码
const result = hugeList
  .filter(...)
  .sort(...)
  .map(...);

优化方向:

text 复制代码
useMemo
算法优化
分页
虚拟列表
Web Worker
增量计算

第三类:主线程阻塞

例如:

text 复制代码
100ms JavaScript
+
Layout
+
Paint

优化方向:

text 复制代码
拆分任务
requestIdleCallback
requestAnimationFrame
Web Worker
React 并发更新
减少同步计算

十六、面试题:React 18/19 的并发能力与性能分析有什么关系?

核心思路(一句话)

并发能力解决的是"高优先级交互不要被低优先级更新长时间阻塞",但它不能消灭高成本计算本身。

例如搜索:

text 复制代码
用户输入
 ↓
更新 input
 ↓
立即响应

同时:

text 复制代码
根据关键词过滤 10000 条数据
 ↓
渲染结果

可以把非紧急更新放入 transition:

jsx 复制代码
import React from "react";

export default function SearchPage({ items }) {
  const [keyword, setKeyword] = React.useState("");
  const [filteredItems, setFilteredItems] = React.useState(items);

  /**
   * useTransition 用来告诉 React:
   *
   * filteredItems 的更新不是当前最紧急的更新。
   *
   * 输入框本身仍然应该保持及时响应。
   */
  const [isPending, startTransition] = React.useTransition();

  function handleChange(event) {
    const value = event.target.value;

    /**
     * 这是用户正在进行的直接交互。
     * 应该尽快更新。
     */
    setKeyword(value);

    /**
     * 将昂贵的列表更新标记为非紧急更新。
     *
     * 注意:
     * 这并不意味着 JavaScript 计算突然变快了。
     *
     * 它主要是让 React 可以根据优先级调度更新,
     * 避免低优先级更新过度阻塞高优先级交互。
     */
    startTransition(() => {
      const nextItems = items.filter((item) =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );

      setFilteredItems(nextItems);
    });
  }

  return (
    <div>
      <input
        value={keyword}
        onChange={handleChange}
        placeholder="搜索"
      />

      {isPending && <div>正在更新列表...</div>}

      <ul>
        {filteredItems.map((item) => (
          <li key={item.id}>
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

关键理解:

text 复制代码
并发更新
≠
多线程

并发更新
≠
让 100ms 计算变成 10ms

它解决的是:

text 复制代码
如何更合理地调度工作

而不是:

text 复制代码
如何凭空减少工作量

十七、面试题:React Performance tracks 是什么?和传统 Performance 分析有什么区别?

核心思路(一句话)

React Performance tracks 把 React 自身的调度和组件工作直接放进浏览器 Performance 时间线,让"React 为什么卡"和"浏览器为什么卡"可以在同一条时间轴上分析。

现代 React 提供了 React Performance tracks,其中可以看到 Scheduler、Components 等 React 相关轨道。Components track 可以显示组件渲染及其耗时,Scheduler track 可以帮助理解不同优先级的 React 工作。

架构关系

text 复制代码
Chrome Performance
│
├── Network
│
├── Main Thread
│   ├── JavaScript
│   ├── Style
│   ├── Layout
│   ├── Paint
│   └── Composite
│
├── Browser Events
│
└── React Performance tracks
    ├── Scheduler
    ├── Components
    └── Effects / Server 相关轨道

这意味着现在可以更加自然地进行:

text 复制代码
浏览器长任务
       ↓
找到 React 更新
       ↓
看到 React Scheduler
       ↓
找到具体组件
       ↓
继续查看调用栈

这比单纯把 React 和浏览器性能完全割裂开来更加有效。


十八、面试题:如果让我选择性能工具,你会怎么选择?

核心思路(一句话)

根据问题所在的层次选择工具,而不是固定使用某一个工具。

问题 首选工具 主要目的
React 哪些组件渲染慢 React DevTools Profiler 组件级定位
哪些组件频繁重新渲染 React DevTools Profiler 找更新范围
为什么 props 导致重新渲染 Why Did You Render 辅助分析
JavaScript 执行慢 Performance 主线程分析
页面点击卡顿 Performance Long Task / 调用栈
Layout 很慢 Performance Layout / Style
Paint 很慢 Performance Paint / Composite
首屏慢 Lighthouse + Performance 加载链路
Core Web Vitals Lighthouse / 真实用户监控 用户体验
资源过大 Lighthouse + Network 资源优化
React 调度问题 Performance + React Performance tracks React 调度分析

十九、面试题:为什么不能一上来就优化?

核心思路(一句话)

没有性能数据的优化,本质上是在猜。

例如:

text 复制代码
"我觉得这个组件应该 memo。"

这是猜测。

正确流程:

text 复制代码
Profiler
 ↓
发现 Item 组件频繁渲染
 ↓
发现 props 中的对象引用每次变化
 ↓
发现 Item 本身渲染耗时 20ms
 ↓
使用 React.memo + 稳定引用
 ↓
重新 Profiling
 ↓
20ms → 2ms

这才是完整的性能优化闭环。


二十、面试题:如何设计一个真正完整的 React 性能优化闭环?

核心思路(一句话)

性能优化不是"加缓存",而是"测量 → 定位 → 归因 → 优化 → 验证 → 持续监控"。

完整架构图

text 复制代码
                  性能问题
                     │
                     ▼
                建立性能基线
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
      加载          交互          渲染
        │            │            │
        ▼            ▼            ▼
   Lighthouse    Performance   React Profiler
        │            │            │
        │            ▼            ▼
        │       Main Thread    Component
        │            │          Render
        │            │            │
        └────────────┼────────────┘
                     ▼
                  定位根因
                     │
        ┌────────────┼─────────────┐
        ▼            ▼             ▼
     更新过多      计算过重       浏览器瓶颈
        │            │             │
        ▼            ▼             ▼
 React.memo      useMemo        Layout
 状态拆分        算法优化        Paint
 Context拆分    虚拟列表        Long Task
        │            │             │
        └────────────┼─────────────┘
                     ▼
                  重新测量
                     │
                     ▼
                对比性能数据
                     │
                     ▼
                 是否改善?
                ┌────┴────┐
               否         是
               │           │
               ▼           ▼
            继续定位      固化方案
                           │
                           ▼
                       持续监控

二十一、完整实战示例:一个典型的 React 性能问题如何定位?

下面模拟一个非常典型的面试场景。

问题

页面:

text 复制代码
Counter
+
1000 个 ListItem

点击 Counter:

text 复制代码
页面明显卡顿

有问题的代码

jsx 复制代码
import React from "react";

/**
 * 一个模拟比较昂贵的列表项。
 */
function ListItem({ item, onSelect }) {
  /**
   * 模拟耗时计算。
   *
   * 真实项目中可能对应:
   *
   * - 数据格式化
   * - 大量计算
   * - 图表数据处理
   * - Markdown 解析
   * - 大量字符串处理
   */
  let result = 0;

  for (let i = 0; i < 100000; i++) {
    result += i;
  }

  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name} - {result}
    </li>
  );
}

export default function App() {
  const [count, setCount] = React.useState(0);
  const [selectedId, setSelectedId] = React.useState(null);

  const items = React.useMemo(() => {
    return Array.from({ length: 1000 }, (_, index) => ({
      id: index,
      name: `Item ${index}`,
    }));
  }, []);

  /**
   * 问题:
   *
   * App 每次 render 时都会创建新的函数。
   *
   * 如果 ListItem 使用 React.memo,
   * 这个函数引用变化仍然会导致 memo 失效。
   */
  const handleSelect = (id) => {
    setSelectedId(id);
  };

  return (
    <div>
      <h1>React Performance Demo</h1>

      <button
        onClick={() => {
          setCount((value) => value + 1);
        }}
      >
        count:{count}
      </button>

      <p>selectedId:{selectedId}</p>

      <ul>
        {items.map((item) => (
          <ListItem
            key={item.id}
            item={item}
            onSelect={handleSelect}
          />
        ))}
      </ul>
    </div>
  );
}

二十二、如何通过工具分析这个问题?

第一步:Performance

录制:

text 复制代码
点击 count

观察:

text 复制代码
Main Thread

如果看到:

text 复制代码
JavaScript
████████████████████████████████

说明主线程存在明显计算压力。


第二步:React Profiler

录制同一个操作。

发现:

text 复制代码
App
 ↓
1000 个 ListItem
 ↓
全部重新渲染

此时性能根因已经非常明确:

text 复制代码
count 改变
 ↓
App render
 ↓
ListItem 全部 render
 ↓
每个 ListItem 都进行昂贵计算
 ↓
主线程被阻塞

二十三、优化版本

jsx 复制代码
import React from "react";

/**
 * 使用 React.memo。
 *
 * 如果 item 和 onSelect 的引用都没有变化,
 * React 可以跳过 ListItem 的重新渲染。
 */
const ListItem = React.memo(function ListItem({
  item,
  onSelect,
}) {
  /**
   * 模拟昂贵计算。
   *
   * 如果组件没有重新渲染,
   * 这段计算也就不会重复执行。
   */
  let result = 0;

  for (let i = 0; i < 100000; i++) {
    result += i;
  }

  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name} - {result}
    </li>
  );
});

export default function App() {
  const [count, setCount] = React.useState(0);
  const [selectedId, setSelectedId] = React.useState(null);

  /**
   * items 本身不会因为 count 改变而重新创建。
   *
   * 这样可以保证 item 的引用稳定。
   */
  const items = React.useMemo(() => {
    return Array.from({ length: 1000 }, (_, index) => ({
      id: index,
      name: `Item ${index}`,
    }));
  }, []);

  /**
   * 使用 useCallback 保持函数引用稳定。
   *
   * 因此:
   *
   * count 改变
   * ↓
   * App render
   * ↓
   * handleSelect 引用不变
   * ↓
   * item 引用不变
   * ↓
   * ListItem 的 props 没变化
   * ↓
   * React.memo 可以跳过 ListItem render
   */
  const handleSelect = React.useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <div>
      <h1>React Performance Demo</h1>

      <button
        onClick={() => {
          setCount((value) => value + 1);
        }}
      >
        count:{count}
      </button>

      <p>selectedId:{selectedId}</p>

      <ul>
        {items.map((item) => (
          <ListItem
            key={item.id}
            item={item}
            onSelect={handleSelect}
          />
        ))}
      </ul>
    </div>
  );
}

二十四、但是上面的优化还不是最终答案

高级面试官可能继续问:

"如果有 10000 个 Item 呢?"

这时候继续使用:

text 复制代码
React.memo
+
useCallback

不一定是最终方案。

因为即使不重新渲染:

text 复制代码
10000 个 DOM 节点

本身也可能造成:

text 复制代码
DOM 数量过大
+
Layout 成本
+
Paint 成本
+
内存成本

这时更应该考虑:

虚拟列表

核心思想:

text 复制代码
10000 条数据
      ↓
实际上只显示 20 条
      ↓
DOM 只保留可视区域附近的数据

架构:

text 复制代码
10000 数据
    ↓
Virtual List
    ↓
┌─────────────────┐
│ Item 100        │
│ Item 101        │
│ Item 102        │
│ ...             │
│ Item 120        │
└─────────────────┘

这说明:

性能优化不是固定使用某个 API,而是根据瓶颈减少真正昂贵的工作量。


二十五、React 性能优化中最重要的"主要矛盾"

可以把所有性能问题归纳成:

text 复制代码
性能成本
=
做了多少工作
×
每次工作有多贵
×
工作发生得有多频繁

因此优化方向只有三类:

1. 减少工作量

text 复制代码
虚拟列表
分页
代码分割
懒加载
减少 DOM

2. 降低单次成本

text 复制代码
算法优化
缓存计算
Web Worker
减少复杂渲染

3. 降低工作频率

text 复制代码
React.memo
useMemo
useCallback
状态拆分
Context 拆分
防抖
节流

这比简单背:

text 复制代码
React.memo
useMemo
useCallback

更加高级。


二十六、面试中如何回答"React 性能优化有哪些手段?"

可以按照四个层次回答。

text 复制代码
第一层:减少重新渲染
    ↓
React.memo
状态下沉
状态拆分
Context 拆分
稳定 props 引用

第二层:减少单次渲染成本
    ↓
useMemo
算法优化
组件拆分
避免重复计算

第三层:减少浏览器工作
    ↓
虚拟列表
减少 DOM
减少 Layout
减少 Paint
避免强制同步布局

第四层:减少加载成本
    ↓
代码分割
懒加载
资源压缩
图片优化
缓存
减少 JavaScript 包体积

二十七、最终满分答案

面试官:React 应用出现性能瓶颈时,你会怎么定位?

满分回答

我的思路不是直接进行性能优化,而是先定位瓶颈,再根据数据选择优化方案。

我一般会先明确问题属于哪一类,例如是首屏加载慢、点击交互卡顿、滚动掉帧,还是组件更新过多。

如果怀疑是 React 组件渲染问题,我会首先使用 React Developer Tools Profiler,录制出现问题的真实交互,然后重点观察 Commit、Flamegraph 和 Ranked,定位哪些组件渲染频繁、哪些组件耗时较高。

定位到组件以后,我会继续分析它为什么重新渲染,例如 state 更新范围是否过大、props 是否发生了不必要的引用变化、Context 是否导致大量组件更新,以及 render 中是否存在昂贵计算。

如果发现问题可能不只是 React,而是 JavaScript 执行、第三方库、Layout、Paint 或主线程阻塞,我会使用 Chrome DevTools Performance 进行分析。通过录制真实用户操作,重点查看 Main Thread,寻找长任务,再通过调用栈定位具体的 JavaScript 代码,同时检查 Style、Layout、Paint,以及现代 React 提供的 React Performance tracks,从而把浏览器性能和 React 更新联系起来。

如果问题是首屏加载或者整体 Web 性能,我会使用 Lighthouse 做宏观审计,重点关注 Largest Contentful Paint、Interaction to Next Paint、Cumulative Layout Shift 等用户体验指标,同时结合网络资源、JavaScript 包体积、图片和缓存情况分析。

对于比较具体的"为什么组件又渲染了"问题,在开发阶段也可以使用 Why Did You Render 辅助分析 props、state 和引用变化,但它属于辅助工具,不应该替代 React Profiler 和浏览器 Performance。

定位到根因之后,我会根据瓶颈选择方案:

text 复制代码
更新范围过大
→ 状态下沉 / 状态拆分 / React.memo / Context 拆分

引用频繁变化
→ useMemo / useCallback / 调整数据结构

单次计算成本高
→ 算法优化 / useMemo / Web Worker

DOM 数量过多
→ 虚拟列表 / 分页

主线程长任务
→ 拆分任务 / 降低同步计算 / Web Worker / 合理使用并发更新

Layout 过多
→ 避免强制同步布局 / 优化 DOM 读写

首屏加载慢
→ 代码分割 / 懒加载 / 图片优化 / 资源压缩 / 缓存

最后我一定会重新录制性能数据,用优化前后的实际指标进行对比,而不是认为"代码看起来更优雅"就代表性能真的变好了。

所以我理解 React 性能优化最核心的闭环是:

text 复制代码
发现问题
  ↓
建立基线
  ↓
性能录制
  ↓
定位瓶颈
  ↓
分析根因
  ↓
针对性优化
  ↓
再次测量
  ↓
对比验证
  ↓
持续监控

本质上不是为了减少 render 次数,而是减少用户真正需要等待的工作量、降低单次工作成本,并合理调度必须执行的工作。


最后记忆版

面试时可以直接记住这一句话:

React 性能定位我会采用"React 层 + 浏览器层 + 页面加载层"三层分析:React Profiler 定位组件渲染问题,Performance 定位主线程、JavaScript、Layout 和 Paint 等底层问题,Lighthouse 做整体 Web 性能审计;定位之后再根据根因选择 React.memo、useMemo、useCallback、状态拆分、虚拟列表、代码分割、Web Worker 等方案,并通过优化前后的性能数据验证结果。

再进一步记住:

text 复制代码
Profiler
= React 在干什么?

Performance
= 浏览器到底在干什么?

Lighthouse
= 页面整体健康状况怎么样?

Why Did You Render
= 为什么这个组件又渲染了?

最终目标
= 找到真正的性能瓶颈,而不是机械地添加缓存。
相关推荐
liangshanbo12158 小时前
React 性能优化实战
性能优化·react
FungLeo10 小时前
成为全栈·React 管理后台篇·按钮级权限:能力映射、菜单过滤与自锁保护
react·rbac·路由守卫·权限控制·前端权限·成为全栈
FungLeo11 小时前
成为全栈·React 管理后台篇·评论审核工作流:把状态下拉改成可理解的动作
react·内容安全·状态机·交互设计·成为全栈·评论审核
liangshanbo12151 天前
React 状态持久化面试题——原理深挖版
react·状态持久化
liangshanbo12151 天前
Redux 的中间件(Middleware)的工作机制
中间件·react·redux
FungLeo2 天前
成为全栈·React 管理后台篇·Markdown 编辑器:预览、暗色主题与连续图片粘贴
图片上传·react·响应式设计·markdown编辑器·异步编程·成为全栈
FungLeo2 天前
成为全栈·React 管理后台篇·表单页范式:校验、数据回填与未保存保护
react·表单设计·zod·react hook form·成为全栈·数据回填
名字还没想好☜2 天前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js
FungLeo2 天前
成为全栈·React 管理后台篇·列表页范式:让分页、筛选和返回位置进入 URL
react·前端架构·分页查询·react router·成为全栈·url状态