React 学习站
React›进阶›进阶

重渲染机制与性能优化

进阶进阶

一句话定义

React 性能问题几乎都是两类:渲染太多(不必要的重渲染传播)与渲染太贵(单棵子树计算量大);方法论是固定三步——测量 → 定位原因 → 按原因选手段,绝不跳步。

为什么重要

这是把 kp-008 的模型、kp-018 的工具串成实战能力的一篇。性能优化做反了(盲目 memo)会让代码更难维护且不解决问题;做对了(虚拟化、懒加载、转移优先级)收益是数量级的。

前置知识

  • kp-008 渲染/提交两阶段。
  • kp-018 三个记忆化 API。

核心概念

  • 四种重渲染触发:自身 state 变化;父组件重渲染;Context 值变化;(由前两者引起的)订阅外部 store 变化。
  • 渲染贵 ≠ 提交贵:Profiler 里 Render Duration 是渲染阶段(JS 计算),Commit Duration 才是 DOM 写入。
  • 大列表第一杠杆是虚拟化:只渲染视口内 ~20 行 DOM,而不是 5000 行;记忆化只解决「重复计算」,不解决「DOM 太多」。
  • 代码分割:React.lazy(() => import(...)) + Suspense,按路由/重组件分包,首屏只载必需代码。
  • 优化清单的顺序:先「不做」(不渲染),再「少做」(记忆化),最后「晚做」(分割、转移优先级)。

原理 / 机制

测量是第一步,工具链三件:

  1. React DevTools Profiler:录制一次交互 → 看 Flamegraph/Barchart:谁的 Render Duration 长、谁是「因父组件渲染而渲染」(灰色高亮 did not render)。
  2. why-did-you-render(或 DevTools 的 "Why did this render?"):直接打印某组件重渲染原因(props 引用变化/state/context)。
  3. Lighthouse / Performance 面板:看 INP、长任务,判断是 React 的问题还是网络/图片问题。

按原因选手段的对照表(本库知识点的总集成):

症状(Profiler 证据)根因手段对应 KP
子组件渲染原因 = "parent rendered"引用不稳定 propsuseCallback/useMemo + memokp-018
渲染原因 = "context changed"context 值内联/拆分不当useMemo value / 拆 Providerkp-012
列表行状态错乱且卡key 用 index数据 id 作 keykp-009
长列表 Commit Duration 高DOM 节点过多虚拟化(react-window/virtua)本篇
首屏 JS 大、交互延迟打包未分割lazy + Suspense 按路由分割kp-020
输入卡顿(渲染被高频触发)每键触发全页重渲染受控输入下沉子组件 / useDeferredValuekp-023
慢更新阻塞点击响应同一优先级排队useTransition 把更新标记为可中断kp-022/023

代码分割最小示例:

jsximport { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart')); // 图表库 ~300KB 不进首包

function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <Chart data={data} />
    </Suspense>
  );
}

虚拟化的直觉实现(真实项目用库):

jsx// 理念演示:只渲染可视区
function VirtualList({ items, rowH = 40, height = 400 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const start = Math.floor(scrollTop / rowH);
  const visible = Math.ceil(height / rowH) + 2;
  const slice = items.slice(start, start + visible);

  return (
    <div style={{ height, overflow: 'auto' }}
         onScroll={e => setDeferredScrollTop(e.currentTarget.scrollTop)}>
      <div style={{ height: items.length * rowH, position: 'relative' }}>
        {slice.map((it, i) => (
          <div key={it.id}
               style={{ position: 'absolute', top: (start + i) * rowH, height: rowH }}>
            {it.name}
          </div>
        ))}
      </div>
    </div>
  );
}

5000 行 → 常驻 DOM 22 行,Commit Duration 直接塌缩——这是任何 memo 都做不到的。

直观类比

性能优化像厨房出餐慢的排查:先看订单积压在哪(测量),再对症——菜谱太繁(渲染贵 → 简化/预制 = useMemo)、同一道菜反复做(重复渲染 → 记住成品 = memo)、灶台只有两个却开了十口锅(DOM 太多 → 关火 = 虚拟化)、高峰期把所有客人都排一队(优先级不分 → 贵宾优先 = useTransition)。没看订单先猛买锅,是新手最常见的浪费。

实例 / 案例

一次完整排查的记录模板(面试与工作中都好用):

text问题:搜索页输入时明显卡顿
1. Profiler 录制:输入 "react" 的 5 次键入
2. 发现:App → SearchResults 整棵树每次键入都重渲染
        Render Duration 12ms × 60fps = 明显卡顿
3. 根因:query state 放在 App 顶层,SearchResults 树大
4. 手段(组合拳):
   a. query 下沉到 SearchBox 组件(受控输入局部化)
   b. SearchResults 用 useDeferredValue(query) 接收
   c. 结果列表虚拟化
5. 复测:Render Duration 12ms → 1.2ms,无掉帧

常见误区

  • 凭感觉优化:没有 Profiler 证据就 memo 全家;代码复杂度上去了,帧率没动。
  • 把「重渲染」当原罪:重渲染本身廉价(虚拟 DOM diff),只有渲染贵或渲染频繁到挤掉交互才需要处理。
  • 先 memo 后虚拟化:长列表的正确第一杠杆是虚拟化;memo 是第二层的精修。
  • 忽略网络与资源:卡顿可能是图片未懒加载、脚本阻塞;React 之外的 Performance 面板先看一眼。
  • 优化一次就一劳永逸:性能是回归项;关键交互路径写 Profiler 断言或 CI 性能预算(kp-027)。

自测题

  1. 列出四种重渲染触发源,各举一个例子。
  2. 「5000 行列表卡」为什么 memo 不是首选?虚拟化省掉的是什么量级的工作?
  3. 现场排查一个「输入卡顿」页面,写出你的前三个动作。
  4. React.lazy 解决的是渲染慢还是加载慢?配合什么组件使用?
  5. Render Duration 与 Commit Duration 的区别是什么?各自高说明什么问题?

与其他知识点的关系

  • 向前:kp-008 提供模型,kp-018 提供工具。
  • 向后:kp-022/023 提供「转移优先级」这条与记忆化正交的路径;kp-020 的 lazy/Suspense 是首屏性能主力;kp-027 把性能回归纳入测试体系;kp-025 的 React Compiler 若普及,将自动覆盖表中第一行的多数场景。

延伸阅读

  • React DevTools Profiler:<https://react.dev/learn/react-developer-tools>
  • Optimizing Performance(官方清单式总览):<https://legacy.reactjs.org/docs/optimizing-performance.html>
  • INP 与响应性(web.dev):<https://web.dev/articles/inp>