跳到主要内容
预计阅读 50 分钟

性能优化 —— 让React应用飞起来

React 在框架层面已经做了大量优化:虚拟 DOM、diff 算法、Fiber 架构、时间分片……但这并不意味着开发者可以高枕无忧。当你的商品列表滚动时掉帧、输入框打字有延迟、首屏加载白屏三秒以上,用户不会关心你用了什么框架——他只知道”这个页面卡”。性能优化的本质不是炫技,而是用最小的成本换来最大的用户体验提升。本章将从渲染机制讲到并发特性,带你建立一套完整的 React 性能优化方法论。

📋 开篇自测:你已经知道多少?

  1. 父组件 re-render 时,子组件是否一定会跟着 re-render?你能说出阻止的方法吗?
  2. useMemouseCallback 分别缓存什么?什么时候不应该使用它们?
  3. 一个页面需要渲染 10000 条数据,你有几种方案来保证它不卡顿?

一、React 渲染机制 —— 理解何时、为什么组件会重渲染

在谈优化之前,你必须先搞清楚一件事:React 组件到底什么时候会重新渲染?

1.1 渲染 ≠ 操作 DOM

很多人把”组件渲染”和”浏览器重绘”混为一谈。实际上,React 的渲染分为两个阶段:

  • Render 阶段:执行组件函数(或类组件的 render 方法),生成新的虚拟 DOM 树,然后通过 diff 算法对比新旧树的差异。这个阶段完全发生在 JavaScript 中。
  • Commit 阶段:把 diff 出来的变化真正应用到浏览器 DOM 上。只有这个阶段才会触发浏览器的重排和重绘。

打个比方:Render 阶段像是建筑师在纸上改图纸,Commit 阶段才是工人拿着锤子去改房子。即使图纸改了十遍,只要最终结论是”不用改”,工人就一锤子都不会敲。

所以,组件执行了 render 并不等于浏览器做了实际渲染。React 的 diff 算法会帮你过滤掉绝大部分无效更新。

1.2 触发重渲染的三种情况

function Parent() {
  const [count, setCount] = useState(0);
  console.log('Parent 渲染了');

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child name="小明" />
    </div>
  );
}

function Child({ name }) {
  console.log('Child 渲染了');
  return <div>你好,{name}</div>;
}

点击按钮时,控制台会同时打印”Parent 渲染了”和”Child 渲染了”。但 Child 接收的 name 从未改变,它的渲染完全是浪费的。

这就引出了 React 重渲染的三条规则:

触发条件说明
自身 state 变化调用 setState / useState 的 setter
父组件重渲染父组件 re-render 时,所有子组件默认跟着 re-render
消费的 Context 值变化通过 useContext 订阅的 Context 发生变化

第二条是最容易被忽略的,也是大部分”不必要渲染”的根源。

1.3 什么时候需要关注性能?

在正常情况下,不必过分担心 React 的重渲染——JavaScript 的执行速度远快于浏览器的 DOM 操作,React 的 diff 算法也会帮你兜底。但以下三种场景值得警惕:

  1. 数据可视化 / 大数据量组件:一次更新可能伴随成百上千个 diff 计算。
  2. 高频交互的表单页面:受控组件的每次输入都会触发状态更新,可能让整个页面高频 re-render。
  3. 靠近根组件的状态更新:根组件 re-render 会像多米诺骨牌一样波及整棵组件树。

🤔 想一想 你的项目中,是否存在一个顶层组件管理着全局状态,每次状态变化都导致几十个子组件重新渲染的情况?你会如何改造它?


二、减少不必要渲染 —— memo、useMemo、useCallback 的正确使用时机

2.1 React.memo:给组件加一层”变化检测”

React.memo 是一个高阶组件,它会对 props 进行浅比较。如果前后两次的 props 相同,就跳过渲染,直接复用上次的结果。

import { memo, useState } from 'react';

// 用 memo 包裹子组件
const ExpensiveChild = memo(function ExpensiveChild({ data }) {
  console.log('ExpensiveChild 渲染了');
  // 假设这里有大量渲染逻辑
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

function Parent() {
  const [count, setCount] = useState(0);
  const [list] = useState([
    { id: 1, name: '苹果' },
    { id: 2, name: '香蕉' },
  ]);

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      {/* list 引用不变,ExpensiveChild 不会重渲染 */}
      <ExpensiveChild data={list} />
    </div>
  );
}

点击按钮改变 count 时,ExpensiveChild 不会重新渲染,因为 list 的引用没有变化。

但要注意”浅比较”的坑:memo 默认只比较 props 的第一层引用。如果你每次渲染都创建新的对象或函数作为 props,memo 就会失效。

2.2 useCallback:缓存函数引用

考虑这段代码:

const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
  console.log('ExpensiveChild 渲染了');
  return <button onClick={onClick}>点我</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // 每次 Parent 渲染都会创建一个新的函数对象
  const handleClick = () => {
    console.log('clicked');
  };

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

虽然用了 memo,但每次 Parent 渲染时 handleClick 都是一个新函数,memo 浅比较发现 props 变了,照样重渲染。这就是 useCallback 的用武之地:

function Parent() {
  const [count, setCount] = useState(0);

  // 用 useCallback 缓存函数,依赖数组为空表示函数永远不变
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

现在 handleClick 的引用在每次渲染间保持不变,memo 就能真正发挥作用了。

2.3 useMemo:缓存计算结果

useMemouseCallback 是一对兄弟,区别在于:

  • useCallback(fn, deps) 缓存的是函数本身
  • useMemo(() => value, deps) 缓存的是函数的返回值
function ProductList({ products, keyword }) {
  // 只有 products 或 keyword 变化时才重新过滤
  const filtered = useMemo(() => {
    console.log('执行过滤计算');
    return products.filter(p => p.name.includes(keyword));
  }, [products, keyword]);

  return (
    <ul>
      {filtered.map(p => (
        <li key={p.id}>{p.name} - ¥{p.price}</li>
      ))}
    </ul>
  );
}

2.4 不要滥用:什么时候不需要优化

缓存本身不是免费的——useMemouseCallback 都需要 React 额外维护一份缓存和依赖数组的比较逻辑。对于简单组件和轻量计算,加上它们反而可能降低性能。

一个实用判断标准:

场景是否需要优化
子组件被 memo 包裹,且接收函数/对象 props需要用 useCallback / useMemo
计算量大的派生数据(过滤、排序、聚合)需要 useMemo
简单的字符串拼接、数字运算不需要
组件本身渲染开销极小不需要 memo

记住一句话:先测量,后优化。 没有性能问题的代码不需要”预防性优化”。


三、代码分割与懒加载 —— React.lazy + Suspense

当你的应用体积膨胀到几 MB 时,用户打开页面要等所有代码下载完才能看到内容。代码分割的思路是:先加载用户当前需要的,其余的按需加载

3.1 基本用法

import { lazy, Suspense } from 'react';

// 不再直接 import,而是用 lazy 包裹动态 import
const OrderPage = lazy(() => import('./pages/OrderPage'));
const ProfilePage = lazy(() => import('./pages/ProfilePage'));

function App() {
  const [page, setPage] = useState('home');

  return (
    <div>
      <nav>
        <button onClick={() => setPage('order')}>订单</button>
        <button onClick={() => setPage('profile')}>我的</button>
      </nav>

      {/* Suspense 提供加载中的 fallback UI */}
      <Suspense fallback={<div className="loading">页面加载中...</div>}>
        {page === 'order' && <OrderPage />}
        {page === 'profile' && <ProfilePage />}
      </Suspense>
    </div>
  );
}

工作流程

  1. 打包工具(Vite / Webpack)看到 import('./pages/OrderPage') 时,会把 OrderPage 拆分成独立的 JS 文件。
  2. 用户点击”订单”时,React 发现 OrderPage 还没加载,于是抛出一个 Promise。
  3. Suspense 捕获这个 Promise,显示 fallback 内容。
  4. JS 文件下载完成后,Promise resolve,Suspense 重新渲染,展示 OrderPage

3.2 路由级别的代码分割

在实际项目中,代码分割通常配合路由使用:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<GlobalSkeleton />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/product/:id" element={<ProductDetail />} />
          <Route path="/cart" element={<Cart />} />
          <Route path="/checkout" element={<Checkout />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

这样每个页面都是独立的 chunk,用户进入哪个页面才加载哪个页面的代码,首屏体积可以减少 50% 以上。


四、虚拟列表 —— 大数据量列表渲染方案

假设你的商品列表有 10000 条数据,一次性渲染 10000 个 DOM 节点会怎样?答案是:页面直接卡死。浏览器创建和维护大量 DOM 节点的成本极高——不仅是渲染,后续的任何交互都要遍历这些节点。

虚拟列表的核心思想是:只渲染用户能看到的那部分,其余的用空白区域占位。

想象一下你站在一栋 100 层的大楼前往上看,你的眼睛同时只能看到大约 10 层楼。虚拟列表就是只建造你能看到的那 10 层,当你抬头时动态拆掉下面、建造上面。

4.1 使用 react-window 实现虚拟列表

react-window 是社区中最成熟的虚拟列表方案之一,体积仅 6KB:

import { FixedSizeList as List } from 'react-window';

// 假设有 10000 条商品数据
const products = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `商品 ${i + 1}`,
  price: (Math.random() * 1000).toFixed(2),
}));

function ProductRow({ index, style }) {
  const product = products[index];
  return (
    <div style={style} className="product-row">
      <span>{product.name}</span>
      <span>¥{product.price}</span>
    </div>
  );
}

function VirtualProductList() {
  return (
    <List
      height={600}        // 可视区域高度
      width="100%"         // 宽度
      itemCount={products.length}  // 总数据量
      itemSize={60}        // 每行高度
    >
      {ProductRow}
    </List>
  );
}

不管数据有 10 条还是 100 万条,DOM 中始终只有大约 10-15 个节点(视区域高度和行高而定)。滚动时,react-window 动态计算当前应该显示的行,替换 DOM 内容,实现”无限滚动”的错觉。

4.2 可变高度列表

如果每条数据的高度不固定,使用 VariableSizeList

import { VariableSizeList as List } from 'react-window';

// 根据内容动态计算高度的函数
const getItemSize = (index) => {
  // 带图片的商品行高 120px,纯文本行高 60px
  return products[index].hasImage ? 120 : 60;
};

function VirtualProductList() {
  return (
    <List
      height={600}
      width="100%"
      itemCount={products.length}
      itemSize={getItemSize}
    >
      {ProductRow}
    </List>
  );
}

🤔 想一想 虚拟列表虽然解决了渲染性能问题,但它也有局限性——比如 Ctrl+F 搜索只能找到已渲染的内容。如果你的场景要求浏览器原生搜索可用,你会怎么处理?


五、图片与资源优化 —— 懒加载、响应式图片、预加载

在电商场景中,图片往往是页面体积的大头。一个商品列表页可能有几十张商品图,如果全部同时加载,不仅浪费带宽,还会严重拖慢首屏速度。

5.1 图片懒加载

最简单的方式是使用浏览器原生的 loading="lazy" 属性:

function ProductCard({ product }) {
  return (
    <div className="product-card">
      <img
        src={product.imageUrl}
        alt={product.name}
        loading="lazy"           // 浏览器原生懒加载
        width={300}
        height={200}
      />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
    </div>
  );
}

设置 loading="lazy" 后,浏览器只有在图片即将进入视口时才会发起请求。不需要任何 JavaScript 库。

提示:始终为 <img> 设置 widthheight,这样浏览器可以在图片加载前就预留空间,避免布局抖动(CLS)。

5.2 响应式图片

不同设备需要不同尺寸的图片。手机屏幕加载 2000px 宽的大图纯粹是浪费:

function ResponsiveImage({ product }) {
  return (
    <img
      src={product.imageUrl}
      srcSet={`
        ${product.imageUrl}?w=400 400w,
        ${product.imageUrl}?w=800 800w,
        ${product.imageUrl}?w=1200 1200w
      `}
      sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
      alt={product.name}
      loading="lazy"
    />
  );
}

浏览器会根据设备宽度和像素密度自动选择最合适的图片尺寸。

5.3 关键资源预加载

对于用户下一步大概率会访问的资源,可以提前预加载:

import { useEffect } from 'react';

function ProductCard({ product }) {
  // 鼠标悬停时预加载详情页的代码
  const handleMouseEnter = () => {
    import('./pages/ProductDetail'); // 预加载但不渲染
  };

  return (
    <div
      className="product-card"
      onMouseEnter={handleMouseEnter}
    >
      <img src={product.imageUrl} alt={product.name} loading="lazy" />
      <h3>{product.name}</h3>
    </div>
  );
}

用户把鼠标移到商品卡片上时,详情页的 JS 就开始下载了。等到真正点击进入时,代码已经就绪,页面几乎瞬间展示。


六、React DevTools Profiler —— 找到性能瓶颈

优化的第一步永远是测量,而不是猜测。React DevTools 的 Profiler 面板就是你的性能显微镜。

6.1 使用 Profiler 录制渲染

  1. 安装 React DevTools 浏览器扩展。
  2. 打开 DevTools,切换到 Profiler 面板。
  3. 点击录制按钮(蓝色圆点),然后在页面上执行操作(点击、滚动、输入等)。
  4. 点击停止按钮,查看录制结果。

6.2 解读 Profiler 数据

Profiler 会展示一个火焰图(Flamegraph),每个色块代表一个组件:

  • 灰色:该组件在这次提交中没有渲染。
  • 蓝色/绿色:渲染了,耗时较短。
  • 黄色/橙色:渲染了,耗时较长——这就是你的优化目标。

点击任意组件色块,可以看到:

  • 渲染原因(Why did this render?):是因为 state 变了、props 变了,还是父组件渲染了?
  • 渲染耗时(Duration):该组件这次渲染花了多少毫秒。

6.3 代码级别的 Profiler

除了 DevTools,React 还提供了 <Profiler> 组件,可以在代码中精确测量:

import { Profiler } from 'react';

function onRender(id, phase, actualDuration) {
  console.log(`${id} [${phase}] 耗时: ${actualDuration.toFixed(2)}ms`);
}

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList />
    </Profiler>
  );
}

onRender 回调会在组件树提交更新时触发,actualDuration 是整个子树渲染所花费的毫秒数。你可以把这些数据上报到监控系统,持续追踪生产环境的渲染性能。


七、React 并发特性 —— useTransition 与 useDeferredValue

React 18 引入了并发模式(Concurrent Mode),核心理念是:不是所有更新都同等重要,紧急的更新应该优先处理。

7.1 问题:高优先级和低优先级任务互相阻塞

想象一个典型场景:搜索框 + 大列表。用户在输入框输入关键词,同时要过滤并重新渲染一个 10000 条数据的列表。

function SearchPage() {
  const [keyword, setKeyword] = useState('');
  const [list, setList] = useState(generateHugeList());

  const handleChange = (e) => {
    const value = e.target.value;
    setKeyword(value);  // 高优先级:输入框要立刻响应
    setList(filterList(value));  // 低优先级:列表可以稍后更新
  };

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      <HugeList data={list} />
    </div>
  );
}

没有并发特性时,setKeywordsetList 会被合并成一次更新,如果 HugeList 渲染耗时 200ms,那么输入框也会卡 200ms 才响应——用户打字时明显感觉到延迟。

7.2 useTransition:标记低优先级更新

useTransition 可以把某些状态更新标记为”过渡任务”,React 会优先处理其他紧急更新,等浏览器空闲时再处理过渡任务:

import { useState, useTransition, memo } from 'react';

const HugeList = memo(function HugeList({ data }) {
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

function SearchPage() {
  const [keyword, setKeyword] = useState('');
  const [filteredList, setFilteredList] = useState(generateHugeList());
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    // 紧急更新:输入框立刻响应
    setKeyword(value);

    // 过渡更新:列表更新可以被中断和延迟
    startTransition(() => {
      setFilteredList(filterList(value));
    });
  };

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      {/* isPending 为 true 时显示过渡状态 */}
      {isPending && <p className="hint">搜索中...</p>}
      <HugeList data={filteredList} />
    </div>
  );
}

效果对比:

  • 不用 useTransition:输入每个字符都要等列表渲染完才能继续输入,明显卡顿。
  • 用了 useTransition:输入框丝滑响应,列表延迟更新但不会阻塞交互。

isPending 是一个布尔值,在过渡任务处于等待状态时为 true,你可以用它来显示加载提示。

7.3 useDeferredValue:让值”延迟跟上”

如果你无法把状态更新拆成两个 setState,可以用 useDeferredValue 产生一个”延迟版”的值:

import { useState, useDeferredValue, memo } from 'react';

const HugeList = memo(function HugeList({ keyword }) {
  const filtered = filterList(keyword); // 假设这是耗时操作
  return (
    <ul>
      {filtered.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

function SearchPage() {
  const [keyword, setKeyword] = useState('');
  // deferredKeyword 会滞后于 keyword 更新
  const deferredKeyword = useDeferredValue(keyword);

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
      />
      {/* 用延迟值传给耗时组件 */}
      <HugeList keyword={deferredKeyword} />
    </div>
  );
}

useDeferredValue 本质上等价于 useState + useEffect + startTransition。它接收一个值,返回该值的”延迟版本”。当原始值快速变化时,延迟版本会跳过中间状态,只在浏览器空闲时更新到最新值。

7.4 useTransition 与 useDeferredValue 怎么选

场景推荐方案
你能控制状态更新的代码(能拆 setState)useTransition
你只能拿到一个值,无法控制更新源useDeferredValue
需要知道过渡任务是否完成(显示 loading)useTransition(提供 isPending)

🤔 想一想 startTransitionsetTimeout(fn, 0) 都能让更新”延后执行”,它们的本质区别是什么?提示:思考一下 setTimeout 里的任务能否被中断。


八、实战:优化一个卡顿的商品列表页

现在我们把前面学到的所有技术串起来,优化一个真实场景:一个带搜索、筛选、无限滚动的商品列表页。

8.1 问题版本:全都卡

function ProductPage() {
  const [keyword, setKeyword] = useState('');
  const [category, setCategory] = useState('all');
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);

  // 问题1:每次渲染都重新过滤和排序
  const filtered = products
    .filter(p => p.name.includes(keyword))
    .filter(p => category === 'all' || p.category === category)
    .sort((a, b) => b.sales - a.sales);

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="搜索商品"
      />
      <CategoryFilter value={category} onChange={setCategory} />
      {/* 问题2:一次性渲染所有商品,可能有数千条 */}
      <div className="product-grid">
        {filtered.map(product => (
          // 问题3:每次父组件渲染,所有 ProductCard 都重渲染
          <ProductCard
            key={product.id}
            product={product}
            // 问题4:每次渲染创建新的函数引用
            onAddToCart={() => addToCart(product.id)}
          />
        ))}
      </div>
    </div>
  );
}

这段代码至少有四个性能问题。让我们逐一解决。

8.2 优化版本:丝滑体验

import {
  useState,
  useEffect,
  useMemo,
  useCallback,
  useDeferredValue,
  lazy,
  Suspense,
  memo,
} from 'react';
import { FixedSizeGrid as Grid } from 'react-window';

// 优化1:ProductCard 用 memo 包裹,避免不必要的重渲染
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
  return (
    <div className="product-card">
      <img
        src={product.imageUrl}
        alt={product.name}
        loading="lazy"
        width={200}
        height={200}
      />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>加入购物车</button>
    </div>
  );
});

// 优化2:详情页懒加载
const ProductDetail = lazy(() => import('./ProductDetail'));

function ProductPage() {
  const [keyword, setKeyword] = useState('');
  const [category, setCategory] = useState('all');
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);

  // 优化3:用 useCallback 缓存函数引用,配合 memo 生效
  const onAddToCart = useCallback((productId) => {
    addToCart(productId);
  }, []);

  // 优化4:用 useDeferredValue 把筛选关键词标记为低优先级
  const deferredKeyword = useDeferredValue(keyword);
  const handleKeywordChange = (e) => {
    setKeyword(e.target.value); // 输入框立即响应
  };
  const isPending = keyword !== deferredKeyword; // 当输入值和延迟值不一致时,说明还在筛选中

  // 优化5:用 useMemo 缓存过滤和排序结果(使用 deferredKeyword,不阻塞输入)
  const filtered = useMemo(() => {
    return products
      .filter(p => p.name.includes(deferredKeyword))
      .filter(p => category === 'all' || p.category === category)
      .sort((a, b) => b.sales - a.sales);
  }, [products, deferredKeyword, category]);

  // 优化6:虚拟列表,只渲染可见区域
  const COLUMN_COUNT = 4;
  const ROW_COUNT = Math.ceil(filtered.length / COLUMN_COUNT);

  const Cell = useCallback(({ columnIndex, rowIndex, style }) => {
    const index = rowIndex * COLUMN_COUNT + columnIndex;
    if (index >= filtered.length) return null;
    const product = filtered[index];
    return (
      <div style={style}>
        <ProductCard product={product} onAddToCart={onAddToCart} />
      </div>
    );
  }, [filtered, onAddToCart]);

  return (
    <div>
      <input
        value={keyword}
        onChange={handleKeywordChange}
        placeholder="搜索商品"
      />
      <CategoryFilter value={category} onChange={setCategory} />

      {isPending && <div className="loading-bar">筛选中...</div>}

      <Grid
        columnCount={COLUMN_COUNT}
        columnWidth={260}
        height={800}
        rowCount={ROW_COUNT}
        rowHeight={320}
        width={1080}
      >
        {Cell}
      </Grid>
    </div>
  );
}

8.3 优化清单对照

问题优化手段效果
每次渲染都重新过滤排序useMemo 缓存计算结果只有数据或条件变化时才重新计算
所有 ProductCard 跟着重渲染memo 包裹组件只有 props 变化的卡片才重渲染
函数 props 导致 memo 失效useCallback 缓存函数引用保持引用稳定
一次性渲染数千个 DOM虚拟列表 react-windowDOM 节点始终只有几十个
输入框卡顿useTransition 标记低优先级输入响应丝滑
图片同时加载loading="lazy"只加载可见图片
详情页代码打包在首屏React.lazy + Suspense首屏体积减半

九、前瞻:React Compiler —— 让优化变成自动的事

本章讲了大量手动优化技巧——memo、useMemo、useCallback、懒加载、虚拟列表。但 React 生态正在发生一个重要变化:React Compiler(原名 React Forget)已于 2025 年 10 月稳定发布,它可以在编译阶段自动分析组件的依赖关系,自动插入 memoization 代码。

这意味着什么?你不再需要手动写 useMemouseCallbackReact.memo——编译器会帮你做这件事:

// 你只需写最自然的代码
function ProductList({ products, category }) {
  const filtered = products.filter(p => p.category === category);

  return (
    <ul>
      {filtered.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </ul>
  );
}

// React Compiler 在编译时自动:
// - 为 filtered 加上 memoization
// - 为 ProductCard 的 props 加上浅比较优化

需要注意的是,React Compiler 是一个独立的 opt-in 工具,不是 React 19 包的一部分,需要单独安装和配置(通过 Babel 或 Vite 插件)。它能自动处理大部分 memo 场景,但虚拟列表、代码分割、图片懒加载等优化仍然需要你手动完成。

理解原理依然重要——你需要知道什么是不必要的重渲染、为什么引用稳定性很重要,才能在编译器无法覆盖的边界场景中做出正确判断,以及在调试性能问题时快速定位根因。


📝 掌握度自测

完成本章学习后,试着回答以下问题来检验你的掌握程度:

  1. React 的 Render 阶段和 Commit 阶段分别做什么?为什么说”组件渲染了”不等于”DOM 更新了”?

  2. 写一段代码:父组件有两个 state(countname),子组件只依赖 name。如何保证改变 count 时子组件不重渲染?

  3. React.lazy 的底层原理是什么?Suspense 是如何知道组件”还没加载完”的?

  4. 虚拟列表的核心原理是什么?它有哪些局限性?至少说出两点。

  5. useTransition 和 setTimeout 在”延后更新”这件事上有什么本质区别?在什么设备上差异最明显?

💡 自我评估

  • 能回答 1-2 题:你理解了基础的渲染优化原理,可以在日常开发中避免明显的性能陷阱。
  • 能回答 3-4 题:你已经能够诊断和解决中等复杂度的性能问题,可以独立优化一个列表页或表单页。
  • 能回答全部 5 题:你对 React 性能优化有系统性的理解,可以为团队制定性能优化策略和规范。

购买课程解锁全部内容

从组件到架构:12 章系统掌握现代 React

¥29.90