跳到主要内容

性能优化

好的应用应当有好的性能。尽然,好性能的应用不一定是好应用。

一、组件性能瓶颈定位

精准识别渲染耗时组件。

  • 优先检查耗时 > 16ms 的组件(避免阻塞帧渲染)
  • 检查 props 变更是否有必要

1. 核心原理

  • 通过记录组件渲染生命周期,生成
    • 火焰图 ( Flame graph ) : 直观展示组件渲染层级,颜色越深表示渲染耗时越长
    • 排名图 ( Ranked graph ) : 按渲染耗时排序组件,快速定位“耗时大户”
    • commits 信息 : 每次状态更新触发的渲染批次,关注不必要的重复 commits (如无状态变化却频繁渲染)
  • 基于 React Fiber 架构,捕获 Fiber 节点的渲染耗时及触发原因

2. 重渲染原因

  • 父组件重渲染导致子组件被动渲染(即使 props 未变)
  • 组件自身 state 频繁更新(如不必要的 setState()
  • props 引用变化(如父组件每次渲染创建新对象/函数)

二、 memo / shallowEqual

避免核心的组件无效渲染。

1. 核心原理

  • 通过浅比较 ( shallow comparison ) 判断 props 是否有变化
  • 默认比较方式 :对对象的第一层属性进行 Object.is 比较
  • 核心逻辑 : shouldRender = !areEqual(prevProps, props)

2. 示例展示

import React, { memo } from 'react';

const Child = memo(({ data }) => {
/** 仅当 data 引用变化时重渲染 */
});

// 自定义比较函数
const areEqual = (prevProps, nextProps) => {
return preProps.id === nextProps.id;
};

memo(Child, qreEqual);

3.适应场景

  • 适用于纯组件(相同的 props 输出相同的 UI )
  • 配合 useCallback/useMemo 避免内联函数/对象导致的引用变化
  • 深层对象需要 shallowEqual (或自定义比较逻辑)
危险

避免过度使用,简单组件使用 memo 反而会增加成本

4. React.memo()PureComponent

React 中, React.memo()PureComponent 都用于优化性能的组件抽象。

  • PureComponent 是专门为 类组件(Class Components) 设计,继承 React.PureComponent(仅可在类组件中使用)
    class MyComponent extends React.PureComponent {
    {/* ... */}
    }
  • React.memo() 是一个 高阶组件(HOC) ,包裹 函数组件( Function Components ) (专门为)
    const MyComponent = React.memo(function MyComponent(props) {
    {
    /* ... */
    }
    });
特性PureComponentReact.memo()
适用组件类组件函数组件
比较内容Props + StateProps
自定义比较❌ 不支持✅ 支持
避免 state 无效渲染✅ 自动处理❌ 需手动优化

PureComponent 相当于在组件中重写了 shouldComponentUpdate() 方法,且会同时比较 propsstate 变化。而 React.memo() 只比较 props ,因为函数组件本身没有 state (除非使用 useState 等 Hook )

即使函数组件内部使用了 useState()useReducer()React.memo() 也不会比较这些内部状态的变化。内部的状态变化依旧仍会触发重新渲染,这是正常且必须的。

三、懒加载 ( React.lazy + <Suspense />

拆分代码块,减少首屏的负载

四、虚拟列表 ( Virtualized list )

适合在长列表中仅渲染可视区域内的元素,减少 DOM 节点数量。

1. 实现原理

  • 算高度 : 计算整个列表总高度
  • 看窗口 : 确定当前可视区域
  • 精投放 : 只渲染可见区域及缓冲区内容
  • 缓冲区 : 在窗口上下多显示 3 - 5 个元素
  • 快速定位 : 通过计算确定列表项的位置
  • DOM 复用 : 滑出视野的元素被回收

2. 使用示例

实现方式包体积增加渲染性能功能丰富度适用场景
传统 Table0KB差(1000 条数据)小数据集( < 100 条 )
react window~4kb优(10 万条数据)基础简单列表、对性能要求高
react virtualized~34KB良( 1 万条数据)复杂表格、网格布局

使用 react window ,适用于大多数基础列表需求。

  • <List /> : 用于一维列表(垂直/水平方向虚拟滚动)
  • <Grid /> : 用于二维表格场景
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => <div style={style}>{index}</div>;

const VirtualList = () => (
<List
height="600" // 容器高度
itemCount={10000} // 总项数
itemSize={35} // 每项高度
with={300} // 宽度
>
{Row}
</List>
);
  • 缓存策略 : 调整 overscanCount (推荐 5 ~ 10,默认 5 ) 预渲染上下方项目
  • 初始高度 : 设置 defaultRowHeight 为接近实际平均高度的值
  • 组件优化 : 使用 React.memo 缓存列表项组件,避免不必要的重渲染
  • 动态组件 : 使用 useDynamicRowHeight 钩子实时测量元素尺寸

3.优势

  • 内存占用恒定( O(可见区域高度)
  • 渲染复杂度降至 0 ( O(可见项数量)

五、 首屏加载优化

1. 路由懒加载

react router v6+
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

<Routes>
<Route
path="/"
element={
<Suspense fallback={<Loader />}>
<Home />{' '}
</Suspense>
}
/>
</Routes>;

2. CDN 加速

  • 静态资源托管至 CDN ( js/css/image )
  • 配置 HTTP/2 多路复用
  • 添加 Cache-Control 头缓存资源

3. 压缩技术

  • 代码压缩 : Webpack TerserPlugin
  • 图片优化 : WebP 格式 + 响应式图片
  • Brotli/Gzip : 服务器启用压缩(节省 60% + 体积)

4. 其他技巧

  • 预加载关键资源 :通过 <link rel="preload" href="critical.js" as="script" > 提前加载首屏必须资源
  • 减少第三方广告 : 非必要的广告、统计脚本延迟加载(如 async/defer
  • SSR/SSG : 服务端渲染 ( Next.js ) 或静态生成,减少客户端首屏渲染时间

六、 运行时优化

useMemo
const expensiveResult = useMemo(() => {
/* 高开销计算 */
return compute(a, b);
}, [a, b]); // 依赖变化时重新计算
useCallback
/** 避免之组件因函数引用更新重渲染 */
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
  • 仅用于传递给优化子组件(如 memo 组件 )
  • 避免滥用(缓存本身有开销)

七、JSX 届防抖

React 18 及以后的版本中,并发模式( Concurrent Rendering ) 是一项核心特性,它允许 React 在不阻塞主线程的情况下处理复杂渲染任务。

而为了解决 快速持续更新导致的界面卡顿问题 而引入了 useDeferredValue()useTransition() 两个 Hook。通过延迟非紧急渲染,让浏览器优先处理高优先级任务(如用户输入),提升用户体验。

1. useDeferredValue()

延迟一个值的更新,使依赖该值的组件不会立即重新渲染,而是等待 React 的空闲时机(如浏览器绘制完成后)再更新。

信息

useDeferredValue() 是监听输入值的变化,并记录为状态,但不立即更新。在下次(或者定义的间隔还没有看源码)空闲状态(下一个渲染的 requestAnimationFrame() ),如果没有再次变化,才再次计算返回新的返回值(且触发组件的重渲染)。

类似于防抖( Debounce ) ,但是是在 React 的内部调度,无需手动设置定时器。

  • 当某个值频繁变化时(如输入框的实时过滤)
  • 依赖该值的组件渲染成本较大(如大型列表)
import { useDeferredValue } from 'react';

function SearchResults({ query }) {
// 延迟 query 的更新
const deferredQuery = useDeferredValue(query);

// 使用 deferredQuery 进行高开销的操作(如过滤大列表)
const results = heavyFiltering(deferredQuery);

return <ResultList items={results} />;
}
  • 效果 : 当 query 快速变化时, heavyFiltering 不会阻塞主线程,界面保持响应

2. useTransition()

将状态标记为 非紧急过渡更新 , 允许 React 先处理高优先级更新(如点击事件),再执行过渡更新。

  • 同时有 紧急交互 (如输入框)和 非紧急渲染 (如列表过滤)
  • 需要显示加载状态(通过 isPending
import { useTransition } from 'react';

function SearchBox() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();

const handleChange = e => {
const value = e.target.value;
setQuery(value); // 紧急更新:输入框内容
// 将过滤操作标记为过渡更新
startTransition(() => {
setResults(heavyFiltering(value));
});
};

return (
<div>
{/** 显示加载状态 */}
{isPending && <Spinner />}
<input value={query} onChange={handleChange} />
<ResultsList items={result} />
</div>
);
}
  • 输入框输入时立即响应(无卡顿)
  • 列表过滤在后台显示,完成后显示 Spinner 并更新结果
  • 路由切换 : 导航到新页面时,旧页面保持响应,新页面在后台加载 (React Router 路由的 loader 原理)

3. 表格

特性useDeferredValueuseTransition
核心关注点单个值的延迟更新状态更新的优先级标记
返回值延迟版本的值[isPending , startTransition]
适用场景搜索结果、列表渲染等值的延迟计算路由切换、数据过滤等状态更新
使用方法将值包装成延迟版本将状态更新包裹在 startTransition
是否提供加载状态是(通过 isPending

八、构建优化( Tree Shaking )

移除未使用的代码( Dead Code Elimination )。

webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
};
  • 使用 ES Module 倒入 ( import {func } form 'lib';
  • 第三方库支持 Tree Shaking (如选 lodash-es 代替 lodash
  • 检查打包结果 npx webpack-bundle-analyzer