前言
看过我之前文章的应该知道我们学校为了降低成本选择自主开发考试系统,并对小编所在的实验室委以重任,在框架的选择上选用了 react 作为项目框架,有些写了多年 react 的会说 react 性能拉胯,其实大多数场景是因为机制没理解透,吃了 react 机制的亏。
性能问题
- 由于 useEffect 滥用,导致副作用泛滥,造成"页面闪烁、重渲染、数据竞态"问题
- prop drilling 导致不必要的渲染
- 没有请求缓存池,数据获取实时请求 API
- 渲染优先级未控制,所有更新一律同步卡主线程
- 非关键逻辑和关键逻辑放一起执行,block 住时间切片
实战示例
useEffect 滥用,数据一律实时请求 API
常见的 useEffect 中请求数据,例如:
js
useEffect(() => {
fetch(api).then(...)
},[])
这样子写有什么问题?
- 直接在 Effects 中获取通常意味着不会预加载或缓存数据。 例如,如果组件卸载,然后再次挂载,则它必须再次获取数据。
- 网速较慢时,容易造成网络瀑布
- 页面会先渲染 undefind ,再更新
- 请求的竞态问题(请求过快时,最终展示的是最先返回的数据)
- 回到上一页面时重新请求数据
举个在项目中出现的例子如图:当回到上一页面时会重新请求数据

为什么一个请求会被发送两次呢?
官方解释:"当严格模式启动时,React 将在真正的 setup 函数首次运行前,运行一个开发模式下专有的额外 setup + cleanup 周期。这是一个压力测试,用于确保 cleanup 逻辑"映射"到了 setup 逻辑,并停止或撤消 setup 函数正在做的任何事情。"
回到正题,如何减少请求的重发,小编想到的是构建请求缓存池,缓存请求结果,封装合并、缓存、过期逻辑。
示例代码:
js
// 构建缓存池
const fetchCache = new Map();
// 封装请求逻辑
export async function fetchData(key: string, fetchFn: () => Promise<any>) {
if(fetchCache.has(key)) return fetchCache.get(key);
return fetchFn().then((data: any) => {
fetchCache.set(key, data);
return data;
});
}
// 实战示例
useEffect(() => {
// 请求试卷信息,无需过期逻辑
const res = await fetchData(`select(+${id})`, () => select(+id));
...
}, [id]);
实现后效果:

也可以使用市面上开源的库如:用于数据请求的 React Hooks 库 -- SWR、TanStack Query 等等
官方方案:
js
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
在开发过程中,您将在 Network (网络) 选项卡中看到两个 fetch。 这没有错。使用上述方法,第一个 Effect 将立即被清理,因此其变量副本将被设置为 .因此,即使有额外的请求,由于检查,它也不会影响状态。
详情请看:与 Effects 同步 -- React、 You Might Not Need an Effect -- React
调度优先级控制
当同时有多个 State 状态更新时,会造成 react 更新慢,默认的 React 更新是同步阻塞型的,举个例子用 setState 更新 5 个 state,React 会一次性打包全部执行。
如果有一些不重要的动画、计数器、输入监听,最好拆出低优先级更新。
startTransition 可以让你在后台渲染 UI 的一部分,其内部的更新是低优先级的,非阻塞的
示例代码:
js
import { startTransition, useState } from 'react';
const [inputValue, setInputValue] = useState(18);
const onChange = (newValue) => {
startTransition(() => {
setInputValue(newValue);
})
};
避免不必要的渲染
在 react 开发过程中,相信不少人会遇到这种问题"只是更新单个组件,却导致其它无关的组件也跟着重渲染",我们应该怎样避免无关组件的重渲染呢?毕竟 react 的协调成本是很昂贵的。
小工具:React Developer Tools ,通过该工具中的 Profiler 分析组件渲染次数、开始时间及耗时。
如图:

由于考试的倒计时,触发的重渲染,导致下方导航栏也一起重渲染。
解决方案:使用 React.memo 包裹组件
在 React16.6 加入的一个专门用来优化 函数组件 (Functional Component)性能的方法: React.memo。React.memo() 是一个高阶函数,它与 React.PureComponent 类似。
示例代码:
js
import React from 'react';
export default React.memo(footerNav);
实现后效果如图:

其它方法:
- PureComponent 它内置了对 shouldComponentUpdate 的实现:PureComponent 将会在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。
使用: class footerNav extends React.PureComponent{}
缺点: 不管是 PureComponent 还是React.memo 都是对数据进行钱比较,如果引用类型的地址改变,内容没变,还是会被认定为改变。
- Immutable.js 保证修改操作返回一个新引用,并且只修改需要修改的节点。Immutable 的结构不可变性&&结构共享性,能够快速进行数据的比较。
缺点 :虽然 ImmutableJS 可以在某些情况解决重复渲染,但是如果需要频繁地与服务器交互,那么 Immutable 对象就需要不断地与原生 js 进行转换,操作起来显得很繁琐,并且这种方案某种层面上来说有一定心智成本。替换方案:Immer
- reselect缓存 将输入与输出建立映射,缓存函数产出结果。只要输入一致,那么会直接吐出对应的输出结果,从而保证计算结果不变。。这种方式是通过缓存,使用 reselect 缓存函数执行结果,来避免产生新的对象。小编也没有使用过,有兴趣的可以试试。
渲染阻塞识别与组件分段懒加载
一个页面白屏时间长,可能由某个组件加载速度引起的,例如:图表组件、长列表的数据加载等等
解决方案:用 lazy + Suspense 做模块切割
js
const ExamPage = lazy(() => import('@/pages/examPage'));
<Suspense fallback={<div>loading...</div>}>
<ExamPage data={data} />
</Suspense>
总结
本文主要介绍了一些利用 react 机制优化项目的技巧,小编对 react 的学习也不是很深,如果有什么更好的方案可以在评论区指点一下。
借鉴的文章有: