性能优化
好的应用应当有好的性能。尽然,好性能的应用不一定是好应用。
一、组件性能瓶颈定位
精准识别渲染耗时组件。
- 优先检查耗时 > 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) {
{
/* ... */
}
});
| 特性 | PureComponent | React.memo() |
|---|---|---|
| 适用组件 | 类组件 | 函数组件 |
| 比较内容 | Props + State | 仅 Props |
| 自定义比较 | ❌ 不支持 | ✅ 支持 |
| 避免 state 无效渲染 | ✅ 自动处理 | ❌ 需手动优化 |
PureComponent 相当于在组件中重写了 shouldComponentUpdate() 方法,且会同时比较 props 和 state 变化。而 React.memo() 只比较 props ,因为函数组件本身没有 state (除非使用 useState 等 Hook )
即使函数组件内部使用了 useState() 或 useReducer() , React.memo() 也不会比较这些内部状态的变化。内部的状态变化依旧仍会触发重新渲染,这是正常且必须的。
三、懒加载 ( React.lazy + <Suspense /> )
四、虚拟列表 ( Virtualized list )
适合在长列表中仅渲染可视区域内的元素,减少 DOM 节点数量。
1. 实现原理
- 算高度 : 计算整个列表总高度
- 看窗口 : 确定当前可视区域
- 精投放 : 只渲染可见区域及缓冲区内容
- 缓冲区 : 在窗口上下多显示 3 - 5 个元素
- 快速定位 : 通过计算确定列表项的位置
- DOM 复用 : 滑出视野的元素被回收
2. 使用示例
| 实现方式 | 包体积增加 | 渲染性能 | 功能丰富度 | 适用场景 |
|---|---|---|---|---|
| 传统 Table | 0KB | 差(1000 条数据) | 高 | 小数据集( < 100 条 ) |
| react window | ~4kb | 优(10 万条数据) | 基础 | 简单列表、对性能要求高 |
| react virtualized | ~34KB | 良( 1 万条数据) | 高 | 复杂表格、网格布局 |
- 使用 react window
- 使用 react virtualized
使用 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钩子实时测量元素尺寸
使用 react virtualized , 适用于复杂的表格、高级动画等高级功能。
<AutoSizer />: 自动调整容器大小<List />: 虚拟化列表<Table />: 复杂表格布局
import { VariableSizeList } from 'react-virtualized';
const rowRender = ({key, index, style}) => (<div key={key} style={style}>行: {index}</div>);
const MyList = () => (<List
height={600}
rowCount={100}
rowHeight={35}
rowRender- {rowRender}
width="100%"
/>)
3.优势
- 内存占用恒定(
O(可见区域高度)) - 渲染复杂度降至 0 (
O(可见项数量))
五、 首屏加载优化
1. 路由懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
<Routes>
<Route
path="/"
element={
<Suspense fallback={<Loader />}>
<Home />{' '}
</Suspense>
}
/>
</Routes>;
2. CDN 加速
3. 压缩技术
- 代码压缩 : Webpack TerserPlugin
- 图片优化 : WebP 格式 + 响应式图片
- Brotli/Gzip : 服务器启用压缩(节省 60% + 体积)
4. 其他技巧
- 预加载关键资源 :通过
<link rel="preload" href="critical.js" as="script" >提前加载首屏必须资源 - 减少第三方广告 : 非必要的广告、统计脚本延迟加载(如
async/defer) - SSR/SSG : 服务端渲染 ( Next.js ) 或静态生成,减少客户端首屏渲染时间
六、 运行时优化
const expensiveResult = useMemo(() => {
/* 高开销计算 */
return compute(a, b);
}, [a, b]); // 依赖变化时重新计算
/** 避免之组件因函数引用更新重渲染 */
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. 表格
| 特性 | useDeferredValue | useTransition |
|---|---|---|
| 核心关注点 | 单个值的延迟更新 | 状态更新的优先级标记 |
| 返回值 | 延迟版本的值 | [isPending , startTransition] |
| 适用场景 | 搜索结果、列表渲染等值的延迟计算 | 路由切换、数据过滤等状态更新 |
| 使用方法 | 将值包装成延迟版本 | 将状态更新包裹在 startTransition 中 |
| 是否提供加载状态 | 否 | 是(通过 isPending ) |
八、构建优化( Tree Shaking )
移除未使用的代码( Dead Code Elimination )。
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
};
- 使用 ES Module 倒入 (
import {func } form 'lib';) - 第三方库支持 Tree Shaking (如选
lodash-es代替lodash) - 检查打包结果
npx webpack-bundle-analyzer