返回笔记列表
01 · NOTES / WebGIS 开发

第 15 节 · 性能优化

2026 年 9 月 1 日

第 15 节 · 性能优化

📌 版本信息:React 19.x / React DevTools Profiler(2026-08-29 核对) 📚 来源:React 中文文档 · Preventing unnecessary re-rendersReact.lazy / Suspense

一、这一节的目标

  1. 掌握测量先行的调试流程(Profiler 录制与分析)
  2. 精通重渲染三件套:memo / useMemo / useCallback 的配合
  3. 掌握代码分割:React.lazy + Suspense 路由级懒加载
  4. 认识列表虚拟化与防抖节流在 React 里的落地
  5. 建立"先测量、再优化、不过早"的工作纪律

二、纪律:先测量,后优化

React DevTools Profiler 工作流(浏览器装 React Developer Tools 扩展):

  1. Profiler 面板 → 点录制 → 操作页面(切筛选/切页)→ 停止
  2. 火焰图看每个组件:渲染次数 × 渲染耗时,找"渲染了 20 次却每次都一样"的组件
  3. 开启 "Record why each component rendered"(因什么而渲染)——直接告诉你触发原因
  4. 只对测量出的热点动手(第 12/13 节的 useMemo/memo 此时才登场)

三、重渲染三件套的配合(原理串联)

// 子组件用 memo 包裹:props 浅比较相同 → 跳过重渲染
const QuakeRow = memo(function QuakeRow({ quake, onSelect }) { ... });

// 父组件里,传给 memo 子组件的值必须"引用稳定"才能生效:
const handleSelect = useCallback((id) => onSelect(id), [onSelect]);        // 函数要稳定
const processed = useMemo(() => quakes.filter(...), [quakes]);             // 对象/数组要稳定
<QuakeRow quake={q} onSelect={handleSelect} />
// 缺 useCallback:每次渲染新函数 → memo 的浅比较失败 → memo 白包

三件套是"联动套餐":memo 给子组件,useCallback/useMemo 给传下去的函数与对象——缺一个就漏气。另一个更彻底的解法(往往更好):把状态下沉——把 filter state 挪进 FilterPanel 自己,祖先就不因它重渲染了(第 13 节"状态下沉")。


四、代码分割:React.lazy + Suspense

问题:build 后一个巨型 JS 包(地图库 + 图表库 + 全部页面),首屏要下载全部。解法:按路由/重组件切割,用到再加载

import { lazy, Suspense } from 'react';

// 地图组件很重(Leaflet+插件几百 KB)——懒加载它
const MapView = lazy(() => import('./components/MapView.jsx'));

function ExplorePage() {
  return (
    <Suspense fallback={<div>地图加载中…</div>}>
      <MapView />
    </Suspense>
  );
}
// build 后 MapView 变成独立 chunk,进入该页才请求

落地规则:①路由级懒加载(每页一个 chunk,React Router 与 Next 都支持);②重组件懒加载(图表/地图/编辑器);③AntD 这类库按需引入(v5 已自动)。Next.js 的 next/dynamic 是同一思想的框架版(第 27 节地图页就靠它防 SSR 报错)。


五、列表与事件

// ① 虚拟列表:1 万行只渲染可视区的 ~20 行(滚动时换内容不换高度)
// 方案:react-window(轻量标准件);简单场景先做分页(AntD Table 自带)
import { FixedSizeList } from 'react-window';

// ② 防抖节流在 React 的落地:自定义 Hook(第 09 节 hooks/ 目录)
function useDebouncedValue(value, ms) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(t);          // 值再变就重置计时——闭包+清理函数的组合拳
  }, [value, ms]);
  return debounced;
}
// 用法:const kw = useDebouncedValue(keyword, 300); 再用 kw 去过滤/请求

六、动手跟练(无独立练习,融进 C-03a/C-02b)

  • C-02b:列表加 memo 行组件 + Profiler 验证筛选时只有列表区重渲染
  • C-03a:详情页懒加载 AntD 的重量组件;react-window 不急,分页先上
  • 所有项目:搜索框接 useDebouncedValue(把第 06 节"输入即过滤"的性能短板补上)

七、自测题

  1. Profiler 工作流的四步?"记录 why each component rendered"告诉你什么?
  2. memo/useMemo/useCallback 为什么是"联动套餐"?缺一个会怎样?
  3. 什么时候"状态下沉"比"三件套"更好?
  4. React.lazy 的产物是什么?Suspense 的 fallback 何时显示?
  5. useDebouncedValue 的实现里,清理函数起什么作用?

参考答案

  1. 录制→操作→停止→看火焰图的"次数×耗时";直接显示每个组件的渲染原因(哪个 state/props 变了)。
  2. memo 依赖 props 浅比较,而函数/对象 props 每次渲染都是新引用——必须 useCallback/useMemo 稳定它们;缺一个则 memo 判定"props 变了"照常重渲染。
  3. 状态只有单个子树关心时——下沉后祖先完全不感知它,无需 memo 套餐。
  4. 独立 chunk 文件;首次渲染懒组件时、chunk 还没下载完期间显示。
  5. 每次 value 变化先清掉上一个定时器——保证"停止变化 ms 毫秒后"才落地,实现防抖。

八、下一步

React 阶段二过半 → 第 16 节:React+TS 实战模式,把类型系统全面接入组件。

TAGSweb