React 性能优化

目录

前言

先测量:用 Web Vitals / Profiler 定位问题(LCP、FID/INP、CLS)。

按成本优先级修复:首屏/交互流畅 > 长列表渲染 > JS bundle 体积 > 问题微调。

组合手段:Server Components / SSR + Suspense + 缓存(React Query)+ 并发特性(useTransition)+ 精细化局部更新。

一、性能优化常识

1、性能优化的基本原理(为什么会慢)

  • 渲染耗时:组件渲染/调和(reconciliation)+ commit(DOM 更新)耗时。
  • JS 主线程阻塞:解析/执行/垃圾回收会阻塞 UI。
  • 网络与资源加载:大 bundle、图片、字体、API 延迟影响首屏(LCP)与交互感受。
  • 不必要重渲染:粗粒度状态更新 / 不稳定 props / 无谓创建函数/对象 会触发大量重新渲染。
  • Hydration 成本:客户端需要重新绑定事件、补全 DOM,hydration 太重会阻塞交互。

理解这几点就能对症下药。

2、先测量:定位问题的工具与指标

(1)、核心指标(Web Vitals)

  • LCP(Largest Contentful Paint) --- 首屏感知;
  • FID / INP(交互延迟) --- 输入响应;
  • CLS(布局偏移) --- 视觉稳定性。

(2)、测量工具

  • Lighthouse / PageSpeed Insights(高层问题)
  • Web Vitals(in-page 实时采集)
  • Chrome DevTools Performance / CPU profiling(JS 执行)
  • React DevTools Profiler(组件渲染时间、渲染次数)
  • Profiler API ()
  • bundle analyzers(webpack-bundle-analyzer / rollup-plugin-visualizer / Vite plugin)
  • Trace Event、Performance.mark / measure(自定义标记)

实践:先跑 Lighthouse,找到首屏大块时间来源(JS、图片、字体、TTFB);再用 React Profiler 看哪些组件 heavy。

二、React 项目性能优化(全面汇总)

在优化之前必须了解现代 React 专用关键技术栈(趋势与组合):

  • 服务端优先:RSC(Server Components)+ Streaming SSR → 减少客户端 JS。

  • 并发特性:useTransition / useDeferredValue,优先保持交互流畅。

  • 数据缓存层:React Query / RTK Query / SWR 做服务器状态缓存、乐观更新、并发请求合并。

  • 按需加载:React.lazy + Suspense + route-based code-splitting。

  • 细粒度本地状态:Zustand / Jotai(按订阅更新),避免 Context 粗粒度重渲染。

  • 虚拟化:react-window / react-virtual / TanStack Virtual 用于大列表。

  • 构建器 :Vite(dev), Turbopack/ESBuild(下一代)。

    这些是当前最有效的组合。

1、常见的性能优化(⭐️⭐️⭐️⭐️⭐️)

(1)、避免不必要的重新渲染(最核心)(⭐️)

React 的最大性能瓶颈就是 组件重新渲染次数过多。

最常见的导致重复渲染的写法:

cpp 复制代码
function Parent() {
  const obj = { a: 1 };  // 每次 render 都是新对象
  return <Child data={obj} />;
}

优化:

  • useMemo() 保持稳定引用
  • useCallback() 保持稳定函数
  • 使用 React.memo() 避免子组件重复渲染

(2)、列表渲染优化(⭐️)

列表是 React 项目的最大渲染压力源之一。

错误 key:

cpp 复制代码
items.map((item, index) => <Item key={index} />)

正确 key:

cpp 复制代码
items.map(item => <Item key={item.id} />)

进阶优化:

  • 虚拟列表(react-virtual / react-window)
  • 分页加载(无限滚动)
  • 避免列表元素嵌套太深

(3)、useCallback 正确使用(⭐️)

仅在满足以下情况使用:

  • 子组件依赖 props 且使用 React.memo
  • 计算量大的运算(如过滤、排序)
  • 不稳定引用导致重渲染

误用(无意义):

cpp 复制代码
function Comp() {
  const handle = useCallback(() => setX(x + 1), [x]);
  return <Child onClick={handle} />;
}

如果 Child 本身没有 memo,则没有意义。

正确:当 Child 是 memo 且依赖稳定:

cpp 复制代码
const Child = React.memo(({onClick}) => <button onClick={onClick}>OK</button>);
function Parent() {
  const handle = useCallback(() => setX(x => x + 1), []);
  return <Child onClick={handle} />;
}

(4)、useMemo 注意事项(⭐️)

  • 别把行为逻辑放入 useMemo。因为:
    • useMemo不是保证缓存,只是优化提示(hint)------React 可能会在某些情况下选择丢弃或重新计算缓存值。
  • 只有当计算昂贵时才用 useMemo。

适用场景:

  • 计算成本高的操作:当有复杂计算或数据转换操作时。例如:
cpp 复制代码
const sortedList = useMemo(() => {
  return largeArray.sort(complexSortFn);
}, [largeArray]);
  • 引用类型依赖:防止子组件不必要渲染。例如:
cpp 复制代码
const config = useMemo(() => ({
  mode: 'advanced',
  threshold: 0.8
}), []);

不适用场景:

  • 简单计算(可能反而会增加性能开销)
  • 保证数据持久化的场景(应考虑使用 state 或上下文)
  • 需要精确控制缓存生命周期的场景

(5)、使用 "React.memo + key 策略" 优化子组件

  • 不要滥用 memo,简单的组件可能反而会降低性能
  • 确保 key 的稳定性,避免使用随机数或时间戳
  • 对于函数类型的 props,考虑使用 useCallback 配合
①、React.memo 的深度优化

作用原理:通过浅比较 props 来避免不必要的渲染。

使用方式:

cpp 复制代码
const Button = React.memo(function Button({ onClick, label }) {
  console.log('Button rendered:', label);
  return <button onClick={onClick}>{label}</button>;
});

适用场景:

  • 纯展示型组件
  • 渲染开销较大的复杂组件
  • 频繁接收相同 props 的组件

避免:

  • 子组件内部有 useEffect 依赖 props
  • 子组件 props 变化频繁
②、key 属性的策略性使用

核心作用:帮助 React 识别列表中哪些元素发生了变化

最佳实践:

  • 使用稳定唯一标识(如数据 ID)而非数组索引
  • 在动态列表中添加 key 可以显著提升 diff 算法效率,例如:
cpp 复制代码
{items.map(item => (
  <Button 
    key={item.id}  // 使用稳定的唯一标识
  />
))}
③、组合使用场景
  • 列表渲染优化:当渲染大型列表时,同时使用 memo 和 key
  • 状态更新优化:父组件状态变化时,未变化的子组件不会重新渲染
  • 性能对比:比单纯使用 PureComponent 更灵活可控

例如:

cpp 复制代码
function Parent({ list }) {
  return (
    <>
      {list.map((item) => (
        <Button
          key={item.id}           // ✔ 稳定 key
          onClick={item.onClick}  // ❗可能不稳定
          label={item.label}
        />
      ))}
    </>
  );
}

(6)、避免在渲染期间创建函数、对象、数组

常见错误:

cpp 复制代码
<Modal style={{ width: 100 }} />
<Button onClick={() => doSomething()} />

优化:

cpp 复制代码
const style = useMemo(() => ({ width: 100 }), []);
const handleClick = useCallback(() => doSomething(), []);

(7)、正确管理状态位置(State colocation)

React 状态越靠上层 → 重渲染树越大 → 性能越差

🚫 错误:把所有状态定义在根组件

✅ 正确:状态放在最小必要的组件中

(8)、拆分组件(component splitting)

大型组件 = 重新渲染范围大

拆成多个小组件:

  • 更容易用 React.memo 包装
  • 渲染颗粒度更细
  • 逻辑更清晰

(9)、避免在 useEffect 中做耗时操作

常见错误:

cpp 复制代码
useEffect(() => {
  heavyCompute()
}, [])

应当:

  • 搬到 Web Worker
  • 搬到 useMemo
  • 后端处理

(10)、使用 lazy() + Suspense 拆分 bundle

减少首屏体积:

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

配合:

cpp 复制代码
<Suspense fallback={<Loading />}>
  <Chart />
</Suspense>

(11)、使用虚拟列表优化长列表

React 的渲染能力有限,一次渲染上千行 DOM 会卡。

解决方案:

  • react-window(最稳定)
  • react-virtual(TanStack 官方)
  • react-virtuoso(高质量,含大量功能)

(12)、Stale closures(闭包陷阱)与 useRef 解决

问题:

cpp 复制代码
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // 仅日志初始计数

解决:用 ref

cpp 复制代码
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
useEffect(() => {
  const id = setInterval(() => console.log(countRef.current), 1000);
  return ()=>clearInterval(id);
}, []);

(13)、useSyncExternalStore 小示例(自定义外部 store)

cpp 复制代码
function createStore() {
  let state = 0;
  const listeners = new Set();
  return {
    get: () => state,
    set: (v) => { state = v; listeners.forEach(l => l()); },
    subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); }
  }
}
const store = createStore();
function useCount() {
  return useSyncExternalStore(store.subscribe, store.get);
}

2、中级优化(适用于中大型项目)

(1)、使用 React Query / SWR

它们能减少:

  • 重复请求
  • 缓存失效渲染
  • loading 状态更新造成的渲染抖动

React Query 特别适合大量接口的复杂项目。

(2)、使用 useTransition 避免卡顿

适合输入框搜索、过滤、排序:

cpp 复制代码
const [isPending, startTransition] = useTransition();

startTransition(() => {
  setFilter(keyword);
});

能避免 UI 直接卡住。

(3)、useDeferredValue 让渲染更流畅

cpp 复制代码
const deferredValue = useDeferredValue(searchValue);

用于大组件渲染,与用户输入解耦。

(4)、使用 memoized selector

如果使用 Redux:

cpp 复制代码
const selectUser = createSelector([state => state.user], user => user)

能显著减少组件渲染。

(5)、事件委托 + useEvent(React 19)(⭐️)

React 19 新 Hook:

cpp 复制代码
const onClick = useEvent(onClickHandler)

能让事件回调稳定,不再触发刷新。

3、进阶优化("更高级但常被忽视")

(1)、RSC(React Server Components)

React 未来核心方向:

  • 更少的客户端 JS
  • 更快的首屏渲染
  • 更小的 bundle
  • 清除大量 useEffect

如果你使用 Next.js 14+,你已经在用。

(2)、懒加载 + 分包 + 预加载技术

  • React.lazy
  • dynamic import
  • preload / prefetch(Next.js 自动处理)

(3)、避免不必要的 context 传递

Context 一旦变,所有子组件都刷新。

优化:

  • 拆分 context
  • 使用 selector context(像 Zustand)
  • 将 UI 状态放入组件,不放在 context 里

(4)、避免在 render 中做逻辑判断

最坏案例:

cpp 复制代码
<select>
  {options.map(...) /* 大计算 */}
</select>

优化:

cpp 复制代码
const renderedOptions = useMemo(() => options.map(...), [options]);

(5)、使用生产环境构建

确保 Webpack/Vite 启用:

  • tree-shaking
  • minify
  • terser
  • dead code elimination

4、总结(面试最标准答案)(⭐️⭐️⭐️)

React 项目最常见的性能优化包括:

  • 避免不必要的渲染(React.memo、useMemo、useCallback)
  • 优化列表渲染
  • 状态下沉(减少重渲染范围)
  • 拆分组件、懒加载
  • 避免在 render/useEffect 中做重运算
  • 采用虚拟列表
  • 使用 React Query / 状态选择器减少刷新
  • 使用 useTransition / useDeferredValue 优化交互
  • 在大型项目中使用 RSC 提升首屏性能

三、React 项目性能优化(定点爆破)

1、首屏(Initial load / LCP)优化(⭐️⭐️⭐️)

(1)、削减 JS 初始体积

  • 分析 bundle,移除 polyfills 多余部分,开启 tree-shaking、sideEffects。
  • 把大依赖拆到异步加载或服务器端执行(例如数据处理、渲染逻辑)。
  • 使用 ES modules + HTTP/2 或 HTTP/3 + CDN。

(2)、代码分割(Route + Component)

cpp 复制代码
const Page = lazy(() => import('./HeavyPage'));
<Suspense fallback={<Spinner/>}><Page/></Suspense>
  • 优先在路由边界拆分;把管理控制面板的 heavy libs 异步。

(3)、Server Components / SSR / Streaming

  • 把可在 server 渲染的部分移到 RSC,减少客户端 JS。
  • 使用流式渲染(Next.js / custom server)降低 TTFB 后首屏出现时间。

(4)、图片 & 字体优化

  • 使用 responsive images ()、AVIF/WebP、placeholder LQIP 或 blur-up。
  • preload LCP 关键资源;font-display: swap。
  • 使用 CDN + cache headers。

(5)、Critical CSS

  • 抽取关键样式 inline,延迟非关键 CSS。

2、交互流畅(输入/动画/滚动 --- FID/INP)

  • 并发特性
cpp 复制代码
const [isPending, startTransition] = useTransition();
function onChange(e) {
  setInputValue(e.target.value); // 紧急
  startTransition(() => setFilter(e.target.value)); // 非紧急
}
复制代码
- 把非关键更新放入 startTransition,保证输入响应。
  • useDeferredValue
    • 当你有昂贵计算基于输入时:const deferred = useDeferredValue(value); 然后在计算中用 deferred,保持输入流畅。
  • 避免长任务
    • 把 CPU 密集型计算放到 Web Worker(尤其数据可视化),或拆分为 requestIdleCallback / requestAnimationFrame 批处理。
  • 节流/防抖
    • 对滚动、resize、输入搜索使用 listenerApi.delay/debounce 脚本或 Listener Middleware。

3、避免不必要的重渲染(核心是"减少渲染次数与大小")(⭐️⭐️⭐️)

  • 状态设计:局部化 & 精细订阅
    • 小状态放在本组件或子组件;避免把大对象放入 Context。
    • 使用 Zustand、Jotai、useSyncExternalStore 等做按需订阅。
  • 组件拆分
    • 把大组件拆成小组件,减少每次状态改变要重新渲染的树范围。
  • React.memo 与 selectors
    • React.memo + stable props:避免无谓渲染。
    • 使用 reselect / createSelector 做计算缓存。
  • useCallback/useMemo 的正确使用
    • 只在作为 props 传递给 memoized 子组件时使用 useCallback。
    • useMemo 用于昂贵计算,避免过度使用(创建缓存也有成本)。
      常见正确范式:
cpp 复制代码
const handleClick = useCallback(() => doThing(id), [id]);
const filtered = useMemo(() => expensiveFilter(list), [list]);
  • 避免创建不稳定引用
cpp 复制代码
// ❌ 不要在 render 里每次创建新对象传 child:
<Child style={{color: 'red'}} />
// ✅ 抽出常量或用 useMemo。
  • immutable 更新 + immer
    • 使用 Immer(RTK)便捷写法,但确保 reducer 不产生无谓替换。

4、大列表优化(virtualization & windowing)(⭐️⭐️⭐️)

  • 推荐使用 react-window / react-virtual 做大列表,基本示例:
cpp 复制代码
<FixedSizeList height={600} itemSize={50} itemCount={items.length}>
  {({index, style}) => <Row index={index} style={style} />}
</FixedSizeList>
  • 动态高度:使用 VariableSizeList 或测量缓存(react-virtual 提供更好支持)。
  • 避免重建 item renderer(稳定 key 与 props)。
  • 复用 DOM 节点 & shouldComponentUpdate / memo。

5、数据层优化(服务器状态与缓存)

  • React Query / RTK Query:自动缓存、并发请求合并、后台刷新、乐观更新。
  • 使用 staleTime、cacheTime 做粒度控制。
  • 乐观更新模式:
cpp 复制代码
onMutate: async (newTodo) => {
  await queryClient.cancelQueries(['todos']);
  const prev = queryClient.getQueryData(['todos']);
  queryClient.setQueryData(['todos'], old => [...old, newTodo]);
  return { prev };
}
onError: (_err, _vars, ctx) => queryClient.setQueryData(['todos'], ctx.prev)
  • 避免重复请求:useQuery 的 enabled、select、keepPreviousData 等参数。

6、渲染与布局(CSS、动画、paint)

  • 避免触发 layout thrash(强制回流)
    • 避免在 JS 中频繁读取 layout 属性(offsetWidth);若必须,批量读取再批量写入。
    • 使用 requestAnimationFrame 做读写交换。
  • GPU 加速动画
    • 动画使用 transform: translate3d、opacity;避免 top/left 大量布局变更。
    • 使用 will-change 谨慎(只用于短期)。
  • 减少重绘区域:把需要动画的元素单独放层,减少 paint 范围。

7、Hydration 与 RSC 相关实践

  • Partial Hydration / Streaming
    • 使用 streaming SSR,使关键 UI 先渲染,交互边界后 hydrating。
  • 避免大规模 hydration
    • 把很多 UI 做为静态 SSR(Server Component),只 hydrate 必要的 Client components。
  • useId / SSR-friendly IDs 处理无冲突的 ID。

8、构建与交付优化(Bundle / Network)

  • Bundle 分析:找到大包(lodash、date-fns、chart 库),替换为按需导入或轻量实现。
  • Tree-shaking 与 按需导入。
  • HTTP/2/3、brotli/gzip、资源缓存。
  • Critical asset hints: LCP 图片 & 关键字体。
  • Use modern bundler:Vite + Rollup(生产)、Turbopack(Next.js)提升 build & dev 体验。

四、架构级建议(中大型项目)

  • 状态分层策略:UI state(local) vs UI-shared(Zustand) vs Server state(React Query) vs Persisted/global(RTK)。
  • 组件目录原则:feature-based folders;每个 feature 自带 hooks、components、selectors。
  • 性能守护(CI):在 CI 增加 bundle-size 检查、Lighthouse 基线测试、关键交互回归测试。
  • 可观察性:埋点 LCP/INP、错误率、Action 成功率(尤其乐观更新失败率)。
  • 迁移策略:逐步RSC迁移:先把静态、非交互视图迁到 Server Components,保留 client-only 才 hydrate。
相关推荐
terminal0073 小时前
浅谈useRef的使用和渲染机制
前端·react.js·面试
Faith_xzc3 小时前
Doris内存问题指南:监控、原理与高频OOM解决方案
大数据·性能优化·doris
Dontla5 小时前
React Tailwind CSS div布局demo
前端·css·react.js
metaRTC5 小时前
webRTC IPC客户端React Native版编程指南
react native·react.js·ios·webrtc·p2p·ipc
菥菥爱嘻嘻6 小时前
React + ECharts 实践:构建可交互的数据可视化组件
react.js·信息可视化·echarts
在逃的吗喽8 小时前
性能优化方向
前端·性能优化
上海云盾第一敬业销售8 小时前
高防CDN助力网络安全与性能优化
安全·web安全·性能优化
wordbaby9 小时前
React 性能优化误区:结合实战代码,彻底搞懂 useCallback 的真正用途
前端·react.js
敲敲了个代码10 小时前
React组件命名为什么用小写开头会无法运行?
前端·javascript·react.js·面试·职场和发展·前端框架