React:常见的性能优化手段

React 性能优化

遇到性能问题时,不要一开始就盲目使用 memouseMemouseCallback。我们要先通过 React DevTools Profiler 找到真正的性能瓶颈,再进行针对性优化。

React 性能问题通常可以归纳为:

  • 不必要的重渲染
  • 单次渲染成本过高
  • DOM 节点过多
  • 更新频率过高
  • 首屏资源体积过大

本人推荐的教学视频:


一、减少不必要的重渲染:合理使用记忆化

使用 React.memo 包裹较重的子组件。当父组件重新渲染时,如果子组件的 props 没有发生变化(浅比较),React 可以跳过子组件的重新渲染。

对于复杂的数据,通常我们需要配合实现记忆化:

  • useMemo:保持数组、对象等引用稳定
  • useCallback:保持函数引用稳定
jsx 复制代码
const UserList = React.memo(function UserList({ users, onSelect }) {
  return users.map(user => (
    <div key={user.id} onClick={() => onSelect(user)}>
      {user.name}
    </div>
  ));
});

function App({ users }) {
  const visibleUsers = useMemo(
    () => users.filter(user => user.active),
    [users]
  );

  const handleSelect = useCallback((user) => {
    console.log(user);
  }, []);

  return (
    <UserList
      users={visibleUsers}
      onSelect={handleSelect}
    />
  );
}

需要注意的是,记忆化也有额外开销,不应为了"优化"而到处使用。只有在组件确实存在重复渲染成本时才值得使用。

我们也可以直接使用React Complier实现记忆化。

shell 复制代码
npm install -D babel-plugin-react-compiler@latest

二、合理拆分组件

合理建立状态边界和渲染边界:

  • state 放到真正使用它的最小组件中
  • 将频繁更新的部分拆分出来
  • 避免一个顶层组件管理过多状态
  • 让不相关的组件避免被连带重新渲染

例如,输入框的状态通常应该放在输入框组件内部,而不是放在整个页面的顶层组件中。

一个文件尽量只管理同一类职责的 state,例如表单数据由表单相关逻辑负责,加载状态由加载逻辑负责,避免不同类型的状态混杂在一起。


三、避免不必要的派生状态

如果一个值可以通过 props 或现有的 state 计算得到,就不应该再单独保存为 state

这样可以:

  • 减少状态数量
  • 避免状态不同步
  • 降低额外渲染次数
  • 保持单一真实数据来源
jsx 复制代码
function Cart({ items }) {
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return <div>总价:{total}</div>;
}

如果计算成本较高,可以使用 useMemo

jsx 复制代码
const total = useMemo(() => {
  return calculateTotal(items);
}, [items]);

四、控制更新频率

1. 防抖 Debounce

防抖会等待一段时间,确认事件不再触发后才执行。

适合:

  • 搜索输入
  • 实时校验
  • 自动保存
  • 用户停止操作后再请求接口
js 复制代码
function debounce(fn, delay = 300) {
  let timer = null;

  function debounced(...args) {
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  }

  debounced.cancel = () => {
    clearTimeout(timer);
    timer = null;
  };

  return debounced;
}

2. 节流 Throttle

节流会限制函数在固定时间内最多执行一次。

适合:

  • 滚动事件
  • 鼠标移动
  • 窗口缩放
  • 拖拽操作
js 复制代码
function throttle(fn, interval = 100) {
  let lastTime = 0;
  let timer = null;

  function throttled(...args) {
    const now = Date.now();
    const remaining = interval - (now - lastTime);

    if (remaining <= 0) {
      clearTimeout(timer);
      timer = null;
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        lastTime = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  }

  return throttled;
}

3. 使用 useTransition

useTransition 不会减少实际计算量,它主要用于降低非紧急更新对用户交互的阻塞。将指定的状态更新标记为非紧急更新,优先保证输入等交互操作的流畅性。

适合:

  • 大量搜索结果渲染
  • 复杂列表筛选
  • 页面局部内容切换
jsx 复制代码
const [isPending, startTransition] = useTransition();

function handleChange(value) {
  setInputValue(value);

  startTransition(() => {
    setSearchKeyword(value);
  });
}

五、优化大量数据渲染:减少 DOM 数量

当页面需要渲染大量数据时,可以采用以下方式:

保证列表 key 稳定

渲染列表时,应使用数据中稳定且唯一的标识作为 key,例如id。

分页

只渲染当前页数据,减少一次性创建的 DOM 节点。

预加载

在用户即将进入下一页或接近列表底部时,提前请求数据,减少等待时间。

渐进式加载

先渲染核心内容,再逐步加载其他数据,避免页面一次性渲染过多内容。

虚拟化

虚拟化只渲染视口附近的元素,适合超长列表或表格。核心思想是只渲染当前视口附近的少量元素。

常见方案包括:

  • react-window
  • react-virtualized

六、懒加载与代码分割

使用 lazy 和动态 import 实现组件级代码分割,再通过 Suspense 展示加载状态。

jsx 复制代码
const UserPage = React.lazy(() => import('./UserPage'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <UserPage />
    </Suspense>
  );
}

适合懒加载的内容包括:

  • 设置页面
  • 管理后台
  • 富文本编辑器
  • 图表组件
  • 不常访问的功能模块

这样可以避免用户首次进入页面时一次性下载所有代码,降低首屏资源体积。

除了组件懒加载,还可以关注:

  • 路由级代码分割
  • 图片压缩和懒加载
  • 第三方依赖体积
  • Tree Shaking
  • SSR、流式渲染和缓存策略

七、合理划分状态管理边界

不同类型的状态,应采用不同的管理方式。

状态类型 推荐方案 典型场景
局部临时状态 useStateuseReducer 输入框、表单内容、弹窗状态
低频跨层级状态 Context 主题、语言、用户信息
复杂客户端状态 Zustand 等状态库 购物车、当前 AI 模型、编辑器实例
服务端状态 服务端与数据库 权限控制、多端同步、海量数据
  • 客户端状态:弹窗、表单、购物车、编辑器状态
  • 服务端状态(通常需要处理缓存、重新请求、错误、加载和数据同步):用户信息、订单、列表数据、缓存、请求状态

使用 Context 时的注意事项

Context 适合跨层级传递且更新频率较低的数据,但不建议将所有状态放入一个大型 Context 中。

否则可能导致:

  • Context 更新时造成非预期的重新渲染
  • 状态边界不清晰
  • 组件之间耦合增加
  • 后期维护困难

可以根据业务拆分不同的 Context,例如:

jsx 复制代码
<ThemeProvider>
  <AuthProvider>
    <App />
  </AuthProvider>
</ThemeProvider>

八、总结

React 性能优化的核心原则是:

  1. 先通过 Profiler 定位瓶颈,再进行优化
  2. 使用 memouseMemouseCallback 减少不必要的重渲染
  3. 通过合理拆分组件建立状态边界和渲染边界
  4. 避免保存不必要的派生状态
  5. 使用防抖、节流和 useTransition 控制更新频率
  6. 通过分页、渐进式加载和虚拟化减少 DOM 数量
  7. 使用懒加载降低首屏资源体积
  8. 根据状态的作用范围选择合适的状态管理方案
  9. 不要过早优化,避免增加不必要的复杂度

日常开发中,首先应遵循合理的 React 设计模式,写出结构清晰、状态边界合理的代码。只有在真实出现性能问题时,再根据 Profiler 的分析结果进行针对性优化。

相关推荐
章鱼小丸子逃跑中14 小时前
【2025最新版】如何将fnm与node.js安装在D盘?【保姆级安装及人性话理解教程】
前端·javascript·npm·node.js
Larcher15 小时前
从 SDD 到可交付:我如何用规范驱动 AI 做出一个 Chrome 翻译插件
前端·后端·架构
suaizai_15 小时前
AI Agent如何懂你:四层能力拆解
java·前端·人工智能
invicinble15 小时前
把握前端项目的核心(vibecoding)
前端
0end116 小时前
AI Agent 学习笔记(三):上下文工程(下)—— KV Cache、提示词设计与 Agent Skills
前端·aigc·ai编程
CAD老兵16 小时前
在浏览器里对比 DWG/DXF 图纸 —— @mlightcad/cad-diff-viewer
前端·javascript·github
JasonYin16 小时前
AI 定义的 H5移动端 开发规范,直接抄作业!
前端
诗章与猫16 小时前
Leaflet 渲染 100 万 marker 卡成 PPT?我用 WebGL 重写了渲染器
前端
用户9210802628616 小时前
在 AI 代码生成项目里接入 Thought:别展示“玄学思维链”,只展示用户真正关心的工具调用
前端
艾醒(AiXing-w)17 小时前
LangChain 1.0 入门(二):LangChain 全模型标准化接入最佳实践(小白参数详解版)
前端·javascript·langchain