原文链接:www.debugbear.com/blog/measur…
作者:Anna Monus
React应用随着规模扩大往往会变慢。组件会不必要地重新渲染,代码包日益臃肿,交互开始出现延迟。结果导致核心网络指标(尤其是交互到下一次绘制指标) 恶化,用户体验受挫,整体操作迟缓。
尽管 React 应用存在性能问题且现今已有更轻量级的组件化框架(如 Preact、Lit、Solid 等),但 React 仍在持续引入新性能特性,为基于该框架构建高效应用提供了更多可能性。
本文将探讨如何测量和优化 React 性能。
如何衡量 React 性能
有效的性能优化始于测量。React 提供了一些利用浏览器开发者工具来衡量应用程序性能的方法,以便您能够识别性能瓶颈和不必要的重新渲染。
1. React 性能跟踪
React 性能跟踪功能始于 React 19.2 版本。这是 Chrome 开发者工具性能面板中的自定义时间线条目,可同时展示 React 专属事件、网络请求及 JavaScript 执行情况。
本质上,它们将 React 内部优先级系统与组件渲染时长及服务器活动可视化呈现,全面展示 React 的并发渲染功能。
性能面板中包含三个独立的 React 性能跟踪区域:
- 调度器轨迹展示了 React 内部工作调度在四个子轨迹中的分布:阻塞、过渡、悬挂和空闲。每个子轨迹使用彩色条形代表不同优先级的工作 (例如用户输入的阻塞优先级,或
startTransition包裹的更新的过渡优先级) 。若遇到交互迟缓的情况,调度器轨迹可帮助你判断 React 当前是正在处理任务还是处于等待状态。 - 组件轨迹:以火焰图形式可视化组件树,呈现React渲染组件及执行效果的过程。您将看到诸如挂载 (组件首次渲染时) 和卸载 (组件移除时) 等事件标签。颜色深浅反映渲染时长,深色区域表示相对较慢的渲染过程,有助于快速定位组件树中的性能瓶颈。
- 服务器跟踪:若使用 React 服务器组件,开发构建中将显示服务器组件和服务器请求。这些跟踪记录服务器端渲染活动(包括流式传输和水化),有助于调试 SSR 性能问题。
在开发构建中,React性能跟踪会自动显示。在性能分析构建中,调度器跟踪默认可见,而组件跟踪仅显示被<Profiler>包裹的组件——除非您已安装React开发者工具。
使用性能跟踪时,请打开Chrome开发者工具,转到性能选项卡并点击录制按钮。与应用交互后停止录制,这些跟踪数据将与其他浏览器事件一同显示在时间轴上。
2. React 开发者工具性能分析器
React 开发者工具是一款适用于 Firefox 和 Chromium 浏览器 (Chrome、Edge、Opera 等) 的浏览器扩展。它在开发者工具中新增了两个标签页:组件和性能分析器。
性能分析器标签页用于测量组件渲染性能。要记录性能数据,请打开该标签页,点击记录按钮,然后与应用程序进行交互。停止记录时再次点击该按钮。React开发者工具将生成火焰图,直观展示本次会话中渲染的组件。
如何解读火焰图
火焰图以层级结构展示组件:父组件位于顶层,子组件依次排列。每条柱状图宽度代表该组件 (及其子组件) 的渲染耗时。颜色编码直观呈现瓶颈:
- 灰色柱状图:本次提交未渲染的组件
- 绿色/蓝绿色柱状图:快速渲染
- 黄色/橙色柱状图:渲染较慢 (优化重点)
ⓘ注
在 React 中,提交(commit)指的是将更新 (在渲染阶段计算得出) 应用到 DOM 的阶段。一次录制会话通常会捕获多个提交 (例如下图截图中的 23 次)。
性能分析器选项卡顶部的彩色条形图显示了录制会话中的所有提交。通过点击条形或使用箭头按钮,您可以在提交之间导航并比较其性能:
如何检查组件渲染原因
React开发者工具还能精确显示每个组件的渲染原因,这在调试不必要的重新渲染时能避免猜测。要使用此功能,需在性能分析器设置中启用"在性能分析期间记录每个组件的渲染原因"选项 (点击性能分析器选项卡内的齿轮图标)。
分析完成后,将鼠标悬停在火焰图中的某个组件上。此时在"为何渲染此组件?"标题下的提示框中,将显示该组件渲染的原因 (例如:props 改变、状态改变、父组件渲染等):
3. Profiler 组件 API
React 的 Profiler 组件允许您通过编程方式测量渲染性能。您可以将其包裹在组件树的任意部分,以追踪渲染时长。例如:
import { Profiler } from "react";
function App() {
const handleRender = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
});
};
return (
<Profiler id="App" onRender={handleRender}>
<Navigation />
<MainContent />
</Profiler>
);
}
onRender 回调函数会在被包装的组件每次渲染时触发,提供渲染时长和渲染阶段等计时数据。开发过程中可利用这些数据识别渲染延迟问题,或追踪随时间变化的性能指标。
4. Chrome 开发者工具性能选项卡
除 React 性能跟踪外,Chrome 开发者工具的性能选项卡还包含通用分析工具,可显示所有浏览器活动。
主线程部分位于 React 性能跟踪下方。它可可视化主线程时间线,显示 JavaScript 执行的时间点。这既包含应用程序代码 (如下图所示的 DataProcessor、ChartRenderer 和 ChartGrid 等自定义组件),也包含 React 内部的渲染函数。较宽的区块表示执行耗时更长的函数:
在底部窗格中,调用树选项卡按函数细分CPU时间,为每个条目显示自身时间 (函数内部耗时) 和总耗时 (含所有子函数耗时)。请注意调用树涵盖所有JavaScript执行过程,不仅限于React组件,因此需要在更广泛的执行跟踪中识别React特有的工作负载。
如何优化 React 性能
使用上述工具识别瓶颈后,即可着手优化 React 应用的性能。重点关注对用户体验产生实际影响的环节,例如提升 INP 评分、加快加载速度以及减少耗时任务。
1. 运行时优化
运行时优化可在应用运行期间提升 React 性能。当性能分析发现不必要的重新渲染或耗时的组件更新时,可采用以下技术:
1.1. 备忘录法
备忘录法通过缓存计算结果避免重复工作。当输入未改变时,React会直接返回缓存结果而非重新计算。
React提供三种备忘录实现方式:
memo()– 组件备忘录useMemo()– 值备忘录useCallback()– 函数备忘录
下面简要说明它们的工作原理。
1.1.1. memo()
通过memo()函数,您可以对组件进行备忘录化处理,避免在props未改变时重新渲染。只需按以下方式将其包裹在组件声明外即可:
import { memo } from "react";
const MyComponent = memo(({ data, onAction }) => {
return <div onClick={() => onAction(data.id)}>{data.value}</div>;
});
虽然memo()对组件很有用,但它对props进行的是浅层比较,即通过引用逐个比较每个props。父组件中内联创建的对象和函数在每次渲染时都会获得新的引用,因此要保持稳定的引用,你需要使用:
useMemo()用于对象/数组useCallback()用于函数
1.1.2. useMemo()
通过 useMemo() 钩子,可对计算值进行备忘以避免重复计算,或在多次渲染中保持对象/数组引用稳定,例如:
import { useMemo } from "react";
function MyComponent({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter((item) => item.category === filter);
}, [items, filter]);
return <ChildComponent items={filteredItems} />;
}
💡提示
避免在简单运算或属性访问等低成本操作中使用 useMemo()。由于钩子本身存在开销,直接计算这些值更为合理。请将 useMemo() 保留用于复杂计算、大型数组操作或传递给备忘化子组件的值。
1.1.3. useCallback()
可通过 useCallback() 钩子对函数进行备忘化,避免每次渲染时重新创建函数:
import { useCallback } from "react";
function MyComponent({ items, onItemSelect }) {
const handleClick = useCallback(
(id) => {
onItemSelect(id);
},
[onItemSelect]
);
return <ChildComponent items={items} onClick={handleClick} />;
}
若不使用 useCallback(),函数在每次渲染时都会获得新的引用,导致子组件的备忘录机制失效。任何接收 handleClick() 的子组件都会在每次渲染时重新渲染,即使函数行为未发生改变。
1.2. 代码拆分
可通过 lazy() 和 <Suspense> 将 React 代码拆分为更小的块,并按需加载组件。lazy() 函数延迟加载直至组件被需要,而 <Suspense> 组件在加载期间显示备用界面。
以下是一个简单示例:
import { lazy, Suspense } from "react";
const MyComponent = lazy(() => import("./MyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}
1.3. 列表虚拟化
列表虚拟化 (也称为"窗口化") 是 React 用于渲染大量 (约 100+) 相似项或复杂项 (如含图片的卡片、图表或多个子组件) 的性能优化技术。长列表会损害网页性能,因为浏览器会渲染每个项,包括屏幕外的项。
虽然可手动实现虚拟化,但像 react-window 这样的库经过充分测试,能避免常见问题,如滚动卡顿、定位错误和内存泄漏。
在下面的代码示例中,react-window 每次仅渲染约 12-15 个项目 (可见区域为 600px / 50px,外加少量缓冲区):
import { FixedSizeList } from "react-window";
function MyComponent({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div key={items[index].id} style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
💡提示
虽然 react-window 库会使打包文件增加约 4-5 KB,但它通过避免生成冗余 DOM 节点(例如对 1000 项列表仅渲染可见项而非全部 1000 项)消除了大型列表的渲染开销。不过仅当性能分析显示列表渲染是瓶颈时才使用它。
1.4. 状态管理
React 在状态更新时会重新渲染组件及其所有子组件,因此状态放置位置直接影响应用性能。
状态就近放置是 React 的性能优化技巧,即尽可能将状态置于使用位置附近。除非需要共享状态,否则避免将其提升到父组件:
// Bad: state in parent causes all siblings to re-render
function MyComponent() {
const [value, setValue] = useState("");
return (
<>
<Child1 value={value} onChange={setValue} />
<Child2 />
<Child3 />
</>
);
}
// Good: state isolated to the component that needs it
function Child1() {
const [value, setValue] = useState("");
// ...
}
当确实需要在多个组件间共享状态时,请仅将其提升至最近的共同祖先组件,以最大限度减少不必要的重新渲染并避免过度传递属性。
另一种方案是使用 Context API,它能在避免属性钻取的前提下实现跨组件状态共享。但需注意 Context API 可能引发不必要的重渲染——当上下文值变更时,所有消费该上下文的组件都会重渲染,无论该组件是否实际使用了变更后的值。
可通过以下方式缓解此问题:拆分上下文 (即为不同数据创建独立上下文),或使用内置选择器优化的状态管理库 *(如轻量级的Zustand或Jotai)。这些库允许组件仅订阅所需的特定状态部分,避免无关状态变更引发的冗余重渲染。
2. 并发特性
React 18 引入了并发渲染功能,可在大量更新时保持响应性。当 React 处理耗时操作(如过滤大型数据集或渲染复杂组件)时,并发渲染能确保用户界面保持交互性。
可通过两个 React 钩子管理更新优先级:
useTransition用于将自定义状态更新标记为非紧急useDeferredValue用于延迟处理作为 props 接收的值
两者均能在后台处理延迟更新时,确保紧急更新保持响应性。
下面简要说明它们的工作原理。
2.1. useTransition
通过 useTransition,您可以标记非紧急更新,使 React 优先处理更重要的更新。状态更新仍会立即发生,但 React 可能会暂停或重启相关的渲染工作。
useTransition 钩子返回两个值:
isPending– 表示过渡是否正在进行的布尔值startTransition()– 用于封装低优先级更新的函数
在下面的代码示例中,您可以看到如何使用 useTransition 来加快大型数据表的过滤速度:
function DataTable() {
const [searchText, setSearchText] = useState("");
const [filteredRows, setFilteredRows] = useState(allRows);
const [isPending, startTransition] = useTransition();
const handleChange = (text) => {
// Urgent: update input immediately
setSearchText(text);
startTransition(() => {
// Non-urgent: update table when possible
const filtered = allRows.filter((row) => row.name.includes(text));
setFilteredRows(filtered);
});
};
return (
<>
<input
value={searchText}
onChange={(e) => handleChange(e.target.value)}
/>
<table>
{filteredRows.map((row) => (
<tr key={row.id}>{row.name}</tr>
))}
</table>
</>
);
}
此示例中存在两个竞争更新:
- 搜索输入框
- 过滤后的表格行
用户输入搜索框时必须获得即时反馈,但重新渲染数千行数据可以延迟处理。为优先处理更新,我们为非紧急更新添加 startTransition() 包装器。这样即使表格需要过滤数千行数据,用户也能获得即时反馈。
若未使用 useTransition,React 会将两项更新视为同等优先级处理,导致输入框响应迟滞直至表格重渲完成。
2.2. useDeferredValue
当需要延迟处理来自 props 或外部源的值时,可使用 useDeferredValue 钩子。此功能在无法直接控制状态更新时尤为实用(例如父组件管理状态的情形)。
在下例中,filterText 来自父组件。通过延迟处理,React 能优先保持用户输入的响应性,而非立即更新表格:
function DataTable({ filterText }) {
const deferredFilter = useDeferredValue(filterText);
const filteredRows = useMemo(() => {
return allRows.filter((row) => row.name.includes(deferredFilter));
}, [deferredFilter]);
return (
<table>
{filteredRows.map((row) => (
<tr key={row.id}>{row.name}</tr>
))}
</table>
);
}
如上所示,useDeferredValue 会延迟过滤操作,因此当用户在输入框中输入内容时,UI 仍能保持响应性。我们还将过滤逻辑包裹在 useMemo 中 (参见上文),确保仅在 deferredFilter 发生变化时才重新计算,避免每次渲染时进行不必要的重新计算。
💡提示
当直接更新状态时使用
useTransition,从 props 或第三方库获取值时使用useDeferredValue。
3. 包体积
JavaScript 包体积直接影响页面加载速度。体积越大的包会导致下载时间延长、解析工作量增加以及交互延迟,尤其在移动设备和低速网络环境下更为显著。
Chrome Lighthouse 提供的树状图视图可直观展示包的组成结构。在 Chrome 开发者工具中运行 Lighthouse 审计后,点击查看树状图按钮即可检查 JavaScript 的结构。
需注意开发构建的包组成可能存在误导性,因其未实施构建过程中的压缩、树摇动、代码拆分等优化技术。而生产构建中的树状图通常反映打包器生成的代码块而非独立包,除非在构建工具配置文件中启用了源映射功能。
不同构建工具启用源映射的方式如下 (详见Vite、Webpack、esbuild文档):
// Vite (vite.config.js)
build: {
sourcemap: true;
}
// Webpack (webpack.config.js)
devtool: "source-map";
// esbuild (CLI)
--sourcemap;
若需深入分析,可使用专用打包文件分析工具,例如 Webpack Bundle Analyzer 或 Rollup Plugin Visualizer (后者亦兼容 Vite)。这两款工具均能生成交互式树状图,直观展示哪些依赖项对打包文件体积贡献最大。您常会发现令人惊讶的情况:例如某个日期库竟占用 300 KB 空间,或是某个笨重的 UI 框架其实可被更轻量级的替代方案取代。
要缩减包体积,核心原则是仅导入必需组件。即使支持树摇的库,若提供默认加载全部内容的便捷入口,仍会显著增加包体积。典型案例是Chart.js库:通过chart.js/auto入口导入时,系统会立即注册Chart.js中的所有图表控制器、刻度、元素及插件:
import Chart from "chart.js/auto";
虽然这种方法能确保所有图表开箱即用,但也意味着所有图表类型都会在运行时被导入并注册,迫使打包器即使从未使用这些图表也必须将其包含在内。
Tree shaking技术在未使用代码从未被导入时效果最佳。例如,下面的代码仅导入并注册了条形图所需的组件:
import {
Chart as ChartJS,
BarController,
BarElement,
CategoryScale,
LinearScale,
} from "chart.js";
ChartJS.register(BarController, BarElement, CategoryScale, LinearScale);
4. 服务器端渲染
服务器端渲染(SSR)能显著提升React性能,其原理是向浏览器发送预渲染的HTML代码,浏览器仅需进行数据注入(hydration),而非在客户端从头渲染整个应用。这使用户能更快看到有效内容,通常能改善首次内容绘制(FCP)和最大内容绘制(LCP)指标。
流式SSR
自React 18起,SSR不再局限于单次渲染整页。React应用现可实现服务器端增量渲染:
renderToPipeableStreamAPI允许服务器在组件渲染完成后分批流式传输HTML至浏览器。这意味着可先交付页面首屏内容,数据密集型区域随后流式加载,从而使浏览器能更早开始解析和绘制。<Suspense>组件可定义渲染暂停与恢复的位置,同时显示备用界面。在服务器端,它支持选择性流式传输和增量内容展示;在客户端,它允许在水化过程中延迟渲染。
虽然这两种技术常被结合使用,但它们解决的是不同问题:流式传输控制HTML发送至浏览器的方式,而<Suspense>控制渲染的暂停与恢复点。不过两者均可独立使用——renderToPipeableStream API无需<Suspense>即可渐进式发送HTML,而<Suspense>也可在不使用流式传输的情况下,用于延迟客户端渲染 (正如我们在前文代码拆分章节中讨论的那样)。
React 服务器组件 (RSC)
React 18 还引入了 React 服务器组件,其采用不同方法将非交互式组件 (即不包含客户端状态或效果的组件) 的渲染移至服务器端。由于服务器组件永远不会在浏览器中运行,因此无需传输 JavaScript 代码,也无需进行水化处理。
与发送需客户端水合的 HTML 的完整服务器端渲染不同,RSC 发送的是组件树的序列化表示,React 的客户端运行时会将其重建并合并到 (交互式) 客户端组件中。需注意 RSC 的设计初衷是作为客户端组件的补充,而非替代或取代它们。
5. React Compiler
React Compiler 是一款构建时优化工具,可自动为 React 组件添加备忘录机制,通常无需手动添加 useMemo、useCallback 和 React.memo()。它不属于 React 核心组件,需作为 Babel 插件添加至构建管道。该工具于 2025 年 10 月正式稳定发布。
它开箱即用无需配置 (但支持自定义配置)。编译器在构建时分析组件代码,并在能安全提升性能的位置自动插入备忘机制。与手动备忘不同,编译器能更精准地应用备忘机制,包括对单个表达式和条件渲染路径的优化。
该机制基于组件纯净性的假设 (即相同输入产生相同输出,渲染过程中不产生副作用)。因此 React Compiler 要求代码遵循 React 规则:props 和 state 不可被修改,Hooks 必须正确使用。
虽然 React Compiler 能自动且一致地实现常见性能优化,但编译器生成的备忘录机制可能降低调试透明度,且不纯代码或遗留代码可能几乎无法受益。
如何监控生产环境中的 React 应用
开发工具仅能测量本地环境的性能表现;但您可能还想了解 React 应用在生产环境中的运行状况。
最佳方案是部署真实用户监控(RUM)工具,从而洞悉实际用户与应用的交互方式及遇到的性能问题。由于 React 性能随时间推移往往会逐渐下降,即使应用配置完善,您仍会不断遭遇意外状况。
DebugBear RUM 以直方图形式展示关键页面加载里程碑,同时呈现其他核心真实用户指标:
您还可以设置性能预算,当自动化实验室测试中某项特定性能指标 (例如核心网络指标) 超过预设阈值时,系统将自动向您发出警报:
要开始优化 React 性能,请注册免费 14 天试用,找出您的关键问题。