性能优化 —— 让React应用飞起来
React 在框架层面已经做了大量优化:虚拟 DOM、diff 算法、Fiber 架构、时间分片……但这并不意味着开发者可以高枕无忧。当你的商品列表滚动时掉帧、输入框打字有延迟、首屏加载白屏三秒以上,用户不会关心你用了什么框架——他只知道”这个页面卡”。性能优化的本质不是炫技,而是用最小的成本换来最大的用户体验提升。本章将从渲染机制讲到并发特性,带你建立一套完整的 React 性能优化方法论。
📋 开篇自测:你已经知道多少?
- 父组件 re-render 时,子组件是否一定会跟着 re-render?你能说出阻止的方法吗?
useMemo和useCallback分别缓存什么?什么时候不应该使用它们?- 一个页面需要渲染 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 算法也会帮你兜底。但以下三种场景值得警惕:
- 数据可视化 / 大数据量组件:一次更新可能伴随成百上千个 diff 计算。
- 高频交互的表单页面:受控组件的每次输入都会触发状态更新,可能让整个页面高频 re-render。
- 靠近根组件的状态更新:根组件 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:缓存计算结果
useMemo 和 useCallback 是一对兄弟,区别在于:
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 不要滥用:什么时候不需要优化
缓存本身不是免费的——useMemo 和 useCallback 都需要 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>
);
}
工作流程:
- 打包工具(Vite / Webpack)看到
import('./pages/OrderPage')时,会把OrderPage拆分成独立的 JS 文件。 - 用户点击”订单”时,React 发现
OrderPage还没加载,于是抛出一个 Promise。 Suspense捕获这个 Promise,显示fallback内容。- 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> 设置 width 和 height,这样浏览器可以在图片加载前就预留空间,避免布局抖动(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 录制渲染
- 安装 React DevTools 浏览器扩展。
- 打开 DevTools,切换到 Profiler 面板。
- 点击录制按钮(蓝色圆点),然后在页面上执行操作(点击、滚动、输入等)。
- 点击停止按钮,查看录制结果。
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>
);
}
没有并发特性时,setKeyword 和 setList 会被合并成一次更新,如果 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) |
🤔 想一想
startTransition和setTimeout(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-window | DOM 节点始终只有几十个 |
| 输入框卡顿 | useTransition 标记低优先级 | 输入响应丝滑 |
| 图片同时加载 | loading="lazy" | 只加载可见图片 |
| 详情页代码打包在首屏 | React.lazy + Suspense | 首屏体积减半 |
九、前瞻:React Compiler —— 让优化变成自动的事
本章讲了大量手动优化技巧——memo、useMemo、useCallback、懒加载、虚拟列表。但 React 生态正在发生一个重要变化:React Compiler(原名 React Forget)已于 2025 年 10 月稳定发布,它可以在编译阶段自动分析组件的依赖关系,自动插入 memoization 代码。
这意味着什么?你不再需要手动写 useMemo、useCallback、React.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 场景,但虚拟列表、代码分割、图片懒加载等优化仍然需要你手动完成。
理解原理依然重要——你需要知道什么是不必要的重渲染、为什么引用稳定性很重要,才能在编译器无法覆盖的边界场景中做出正确判断,以及在调试性能问题时快速定位根因。
📝 掌握度自测
完成本章学习后,试着回答以下问题来检验你的掌握程度:
-
React 的 Render 阶段和 Commit 阶段分别做什么?为什么说”组件渲染了”不等于”DOM 更新了”?
-
写一段代码:父组件有两个 state(
count和name),子组件只依赖name。如何保证改变count时子组件不重渲染? -
React.lazy 的底层原理是什么?Suspense 是如何知道组件”还没加载完”的?
-
虚拟列表的核心原理是什么?它有哪些局限性?至少说出两点。
-
useTransition 和 setTimeout 在”延后更新”这件事上有什么本质区别?在什么设备上差异最明显?
💡 自我评估
- 能回答 1-2 题:你理解了基础的渲染优化原理,可以在日常开发中避免明显的性能陷阱。
- 能回答 3-4 题:你已经能够诊断和解决中等复杂度的性能问题,可以独立优化一个列表页或表单页。
- 能回答全部 5 题:你对 React 性能优化有系统性的理解,可以为团队制定性能优化策略和规范。
购买课程解锁全部内容
从组件到架构:12 章系统掌握现代 React
¥29.90