React:常见的性能优化手段

React 性能优化

遇到性能问题时,不要一开始就盲目使用 memo、useMemo 或 useCallback。我们要先通过 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、流式渲染和缓存策略

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

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

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

使用 Context 时的注意事项

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

否则可能导致:

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

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

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

八、总结

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

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

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

相关推荐
Code_Solitude8 分钟前
C语言:关于二维数组的作业总结
java·c语言·前端
acd1200941 分钟前
前端随笔:数据明明变了,Vue 页面就是不更新
前端·javascript·vue.js
传人2 小时前
悬停旋转放大和位移效果如何写
前端·javascript·css
zhangzeyuaaa3 小时前
Ruby 方法参数完全指南:默认值、可变参数与关键字参数
前端·python·ruby
粥里有勺糖3 小时前
视野修炼-技术周刊第135期 | 鼠标跟随宠物
前端·github·aigc
粥里有勺糖3 小时前
拿 Codex 协助清理磁盘,Nice!
前端·后端·github
Highcharts.js3 小时前
可视化商用图表库对比,如何选择?
前端·javascript·学习·信息可视化·highcharts·前端可视化
TheITSea4 小时前
JavaScript Web APIs
开发语言·前端·javascript
恋猫de小郭4 小时前
Android CLI 支持 AI Agent 通过 Device Streaming 调试云真机
android·前端·flutter
Wang's Blog4 小时前
Java 项目实战: 外卖平台优化-前端dist部署与Nginx反向代理rewrite
java·前端·nginx