跳到正文
前端知识库
React

React 组件如何优化性能?(React性能优化的手段有哪些?)

从渲染证据选择 memo、状态下放和代码分割,并理解 React Compiler 的依赖分析、运行时缓存与适用边界。

约 7 分钟React · 性能 · React Compiler · memo · useMemo · Profiler

React 组件如何优化性能?(React性能优化的手段有哪些?)

问题解析

面试官考察点:

  • 是否了解 React 的渲染机制
  • 是否掌握常见的性能优化手段
  • 是否能在实际项目中应用优化策略
  • 对 React 原理的理解深度

核心概念

React 通过协调过程把状态映射到 UI;Virtual DOM 本身不保证比直接操作 DOM 更快。组件性能优化应先测量,再减少无收益的计算、组件执行和 DOM 操作。

渲染浪费的场景

父组件重新执行时,它创建的子组件通常也会参与渲染,即使传入数据没有变化;稳定的 JSX 对象、memo 或 Compiler 缓存等可以使 React 跳过部分工作。组件函数被调用不等于真实 DOM 一定变化,也不代表这次结果一定会提交。

详细解答

1. 避免不必要的 render

shouldComponentUpdate

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 对比 props 和 state,决定是否重新渲染
    if (this.props.value === nextProps.value) {
      return false; // 不重新渲染
    }
    return true; // 重新渲染
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

PureComponent

import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    console.log('render');
    return <div>{this.props.value}</div>;
  }
}

// PureComponent 内部实现了 shouldComponentUpdate
// 对 props 和 state 进行浅比较

React.memo(函数组件)

import React, { memo } from 'react';

const MyComponent = memo(function MyComponent({ value }) {
  console.log('render');
  return <div>{value}</div>;
});

// 支持自定义比较函数
const MyComponent2 = memo(
  function MyComponent({ value }) {
    return <div>{value}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true 表示不重新渲染
    return prevProps.value === nextProps.value;
  }
);

2. 使用 useMemo 和 useCallback

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

function ParentComponent({ data, onUpdate }) {
  // 缓存计算结果
  const processedData = useMemo(() => {
    return data.map(item => item * 2);
  }, [data]);

  // 缓存回调函数
  const handleClick = useCallback(() => {
    onUpdate(processedData);
  }, [onUpdate, processedData]);

  return <ChildComponent data={processedData} onClick={handleClick} />;
}

3. 代码分割与懒加载

import React, { Suspense, lazy } from 'react';

// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

4. 虚拟列表优化长列表

import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <List
      height={500}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </List>
  );
}

5. 事件绑定优化

函数引用是否值得稳定,取决于它是否影响下游 memo 比较、Effect 依赖或订阅接口。原生按钮上的简单内联事件通常无需缓存;先测量调用链,不要把“新函数对象”直接等同于性能问题。

// 每次渲染创建新函数:对普通 DOM 按钮通常可以接受
class BadExample extends React.Component {
  render() {
    return <button onClick={() => this.handleClick()}>Click</button>;
  }
}

// 需要稳定引用时:使用类属性或 constructor 绑定
class GoodExample extends React.Component {
  handleClick = () => {
    console.log('clicked');
  };

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

// 函数组件按需缓存,而不是给每个事件都加 useCallback
function FunctionExample() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <button onClick={handleClick}>Click</button>;
}

6. 使用 React Fragments 避免额外标记

// 不推荐:多余的 div
function BadExample() {
  return (
    <div>
      <h1>Title</h1>
      <p>Content</p>
    </div>
  );
}

// 推荐:使用 Fragment
function GoodExample() {
  return (
    <>
      <h1>Title</h1>
      <p>Content</p>
    </>
  );
}

7. 使用 Immutable 数据

import { Map, fromJS } from 'immutable';

// 使用 Immutable 数据便于比较
const state = fromJS({
  user: { name: 'John', age: 30 }
});

// 修改数据返回新对象
const newState = state.setIn(['user', 'age'], 31);

// 快速比较
console.log(state === newState); // false
console.log(state.get('user') === newState.get('user')); // false

深入理解

浅比较原理

function shallowEqual(objA, objB) {
  if (Object.is(objA, objB)) return true;

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) return false;

  for (let i = 0; i < keysA.length; i++) {
    if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
        !Object.is(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  }

  return true;
}

React 渲染流程

  1. 触发阶段:setState 或 props 变化触发更新
  2. 调和阶段(Reconciliation):构建 Virtual DOM 树,进行 Diff 比较
  3. 提交阶段(Commit):将变化应用到真实 DOM

性能优化的关键在于减少调和阶段的工作量。

React Compiler 的自动缓存究竟发生在哪里

Compiler 在构建时分析组件和 Hook 的数据流,生成用于复用计算结果、函数引用或 JSX 的代码;依赖值的比较与缓存读取仍在运行时发生。它不会把依赖用户输入的计算提前全部算完,也不是简单地给每个函数外面机械加一层 useCallback。

function Report({ items, query, theme }) {
  const visible = items.filter(item => item.name.includes(query));
  return <section className={theme}><Rows items={visible} /></section>;
}

假设这段代码已成功编译,且编译器能证明 items、元素字段和 query 的依赖关系,只有 theme 改变时,可以复用 visible 及相应的子树结果;items 或 query 改变时重新计算。实际缓存范围由编译分析决定,并非所有子组件都保证跳过执行。

构建期:解析组件 -> 检查 React 规则 -> 分析依赖 -> 生成缓存检查代码
运行期:读取新输入 -> 依赖可复用? -> 复用缓存 / 重新计算并写入缓存
追问 回答与边界
能否让组件只更新一个 DOM 属性,完全跳过 Fiber? 不能。缓存减少部分渲染工作,更新仍通过 React 协调和提交机制;它不是替换为另一套响应式运行时
是跨用户、跨组件实例的请求缓存吗? 不是。它面向组件/Hook 的 memoization,不提供查询 key、请求去重、失效或鉴权隔离
开启后能删除全部手写 memo 吗? 不要批量删除。先确认函数确实被编译,检查既有依赖语义,用 Profiler 和交互回归验证;错误状态建模和长任务仍需单独处理
只升级 React 19 就自动开启吗? 不会。需要构建链显式启用 Compiler;React 19 提供 react/compiler-runtime,目标为 React 17/18 时按官方配置设置 target 并安装兼容 runtime
检测到违反 Hooks/纯渲染规则会怎样? 对无法安全优化的函数可跳过并报告诊断;语法/配置错误仍可能使构建失败。ESLint 诊断不等于已安装编译插件或所有函数均被优化
如何定位接入后行为变化? 保存可复现用例,检查可变数据、第三方库和依赖;可临时用 "use no memo" 排查单个函数,修复后再恢复并回归验证

Compiler 的安装与跨框架接入沿用 React 与 Vue 运行时优化,这里保留 React 面试中需要解释的机制和边界。

最佳实践

  1. 组件拆分:将大组件拆分为小组件,减少不必要的渲染范围
  2. 合理使用状态:将状态尽量靠近使用它的组件
  3. 按证据稳定引用:昂贵派生计算或下游比较需要时使用 useMemo/useCallback,已启用 Compiler 的代码先验证编译覆盖和收益
  4. 使用生产环境构建:开发模式包含额外的警告和检查
  5. 启用 React DevTools Profiler:分析性能瓶颈
// 状态提升 vs 状态下沉
// 不推荐:所有状态都在顶层
function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  return (
    <div>
      <Counter count={count} setCount={setCount} />
      <Form name={name} setName={setName} />
    </div>
  );
}

// 推荐:状态靠近使用它的组件
function App() {
  return (
    <div>
      <Counter />
      <Form />
    </div>
  );
}

面试要点

  1. 理解 React 的渲染机制:Virtual DOM、Diff 算法、调和过程(知识点:React 渲染流程)
  2. 掌握多种优化手段:shouldComponentUpdate、PureComponent、React.memo、useMemo、useCallback(知识点:避免不必要的 render)
  3. 了解优化适用场景:不是所有场景都需要优化,过度优化反而增加代码复杂度(知识点:最佳实践)
  4. 实际项目经验:能够举例说明在项目中遇到的性能问题及解决方案(知识点:渲染浪费的场景)
  5. 性能分析工具:React DevTools Profiler、Chrome Performance(知识点:React 渲染流程)

常见追问:

资料来源与跨主题入口

来源:FE-006《React进阶&工程化面试全解.pdf》第 20–22 页。补充 Compiler 的编译/运行分工、缓存粒度和迁移边界;memo、PureComponent、代码分割、虚拟列表与状态下放复用本篇已有章节。

来源第 21–22 页的通用性能清单沿用现有专题,避免再复制一份:

  • 指标、采集和实验条件见 指标采集与问题诊断。当前核心指标是 LCP、INP、CLS,来源的 FID 属于历史口径;常用良好阈值为 LCP ≤ 2.5 s、INP ≤ 200 ms、CLS ≤ 0.1,现场按移动/桌面分别看第 75 百分位。
  • 图片/字体、资源提示、CDN、HTTP 缓存与协议见 网络与资源加载优化。defer 控制执行时机,不等于按需下载的懒加载;资源提示与文件合并需要关键路径证据。
  • 长任务拆分、Worker、重排重绘、滚动和动画见 渲染与运行时优化。requestAnimationFrame 不是后台线程,transform/opacity 也不保证任意动画都免费合成。
  • 压缩、Tree Shaking、分包及产物分析见 构建体积与性能治理;骨架屏和占位图仍需预留尺寸,实际进度未知时不要显示伪造百分比。

参考:React Compiler 简介、Compiler target、React.memo 与 Compiler。