React性能优化与新hooks的应用
发布时间: 2026-02-23
前言
今天是马年春节假期的最后一天,过节的松弛基本上都用完了,最近因为一些原因和人交流了不少关于React性能优化的相关话题,我就想干脆写一篇汇总吧,也不完全是关于性能 优化,也有一些和性能无关的hooks的的内容,因为都比较零散,单独开一篇文章感觉必要性不是很大。
React为什么要更加关注性能优化(或者是保持性能不下滑)
React和Vue相比,它直接使用JSX编写组件,不需要将编写的组件转为ATS语法树,相对来说更加灵活可控,相应对编写组件的人的水平来说要求更高,需要程序员去避免性能下滑, 然后基于React的Fiber设计,父组件重新渲染都会引发子组件重新渲染,因此的确在设计组件上有一系列需要谨慎应对的场景,我利用我自身的粗略见解,来聊聊哪些地方来优化组件的性能, 或者说叫保持性能不下滑。
组件设计原则
基于父组件更新会导致子组件更新的逻辑,合理的拆分子组件,避免大量代码在同一个组件中, 但同时也需要把控拆分粒度,这中间是有一个阈值的,超过阈值你得到的未必有性能提升,反而会导致你的维护工作变得更加困难。
useMemo、useCallback、React.memo三板斧
这2个hook和memo的组合使用,很常见,效果好,函数组件的设计就是UI=f(state,props),因此每一次状态变化,都会需要重复执行一次你的函数,拿到新的虚拟WorkInProgress去 和Current树对比,所以你在函数组件中定义的普通变量每次都会重新赋值,如果变量作为prop传入子组件,那么子组件跟着重新渲染。对于没必要重新渲染,且渲染耗时的子组件(大、重)来说, 使用useMemo、useCallback、React.memo能控制子组件不重新渲染。
但是并不推荐给所有子组件都使用这2个hooks和memo包裹来减少渲染,因为任何事情都是有成本的,判断它需不需要更新也是一种消耗(比如memo对比时对象深层次对比的性能开销也不低), 这2个hooks也会带来一些新的问题,因此对于 轻量的子组件,并不需要畏惧它们的重新渲染。原则上有性能优化的必要时才使用useMemo、useCallback、React.memo来优化。加上在最新的React19中,底层是有做类似的优化的。
useEffect和useLayoutEffect
这个hooks本身也有性能开销,如果你写了一堆,就为了当Vue的watch使用,那可能不是一个好的选择,我的观点是useEffect个数要少,依赖数组长度要少,依赖数组的数据类型要简单。
如果要在useEffect测量页面dom的尺寸等操作,使用useLayoutEffect,但是内部不能写大量逻辑。它的执行时间是在React将更新Dom提交后,然后马上执行useLayoutEffect, 通常如果你没有useLayoutEffect,Dom提交后,浏览器会触发回流、重绘等逻辑,如果你有useLayoutEffect,会在提交后马上执行useLayoutEffect函数,此时测量Dom会触发回流重绘, 拿到的一定是最新的dom布局信息,因此如果你要获取某个dom的offsetHeight等会导致回流的API,最好是放到useLayoutEffect中。
useEffectEvent
在useEffect下单独说一下从18开始有一个新的实验性hook useEffectEvent,useEffect有一个比较常见的闭包陷阱问题,hooks在创建的时候内部使用的所有状态、变量都是创建这个 hooks时的闭包变量,只要hook的依赖数组没有变化,这个hooks是不会重新创建的,就存在一个问题,如果想要在内部拿到外面的一个state最新的值,过去只能通过创建一个ref来读取ref的 最新值,而不能读取state的最新值(设置state的最新值可以通过setState(prev=>prev+1)),而现在则可以使用这个新的实验性API来读取最新的state的值。
// 就像这样,只有roomId变化才会导致重新创建连接,但是如果theme变化,不会创建新连接,却可以读取到最新的theme值
function ChatRoom({ roomId, theme }) {
// useEffectEvent函数内永远都是最新的值,因此不需要依赖
const onConnected = useEffectEvent(() => {
console.log("Connected to " + roomId + " with theme: " + theme);
});
useEffect(() => {
const connection = connect(roomId);
connection.on('connect', () => {
onConnected();
});
return () => connection.disconnect();
}, [roomId]);
}useContext
这个虽然可以解决数据多组件透传,但是只能用来存一些不容易变化的数据,比如当前的主题、登录的用户信息、权限信息,因为它透传,它如果频繁变化,会导致大面积的父子组件跟着 重新渲染。
useTransition 低优先级更新任务
React的Fiber让我们中断对比,如果你有耗时且不希望阻塞UI的任务,可以使用这个hooks,React会优先保证输入框等高优先级任务的响应。
import { useState, useTransition } from 'react';
function FilterList() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [list, setList] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
// 1. 立即更新输入框,保证用户打字不卡顿
setQuery(value);
// 2. 过滤逻辑通常很重,不应阻塞输入。
startTransition(() => {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `${value} 的搜索结果 #${i}`
}));
setList(items);
});
};
return (
<div>
<input type="text" value={query} onChange={handleChange} />
{isPending ? <p>正在努力加载列表...</p> : (
<ul>
{list.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
)}
</div>
);
}useDeferredValue 延迟更新
它是一创建一个state的延时副本,用来给子组件,比如如下常见示例,假如用户在输入框每输入一个字符都要执行一个耗时的计算逻辑:
import { useState, useDeferredValue, useMemo } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
// 1. 获取 query 的“延迟版本”
// 当 query 快速变化时,deferredQuery 会暂时保持旧值
const deferredQuery = useDeferredValue(query);
const handleChange = (e) => {
setQuery(e.target.value); // 紧急更新:输入框必须流畅
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="输入搜索内容..." />
{/* 2. 将延迟值传递给耗时的子组件 */}
<SlowList text={deferredQuery} />
</div>
);
}
// 模拟一个渲染很慢的组件
const SlowList = ({ text }) => {
const items = useMemo(() => {
return Array.from({ length: 500 }, (_, i) => (
<li key={i}>{text} 的结果项 {i}</li>
));
}, [text]);
return <ul>{items}</ul>;
};写过输入防抖的朋友都会觉得这不就是防抖吗?虽然他们的效果很相似,都是推迟更新,但是原理上海市有点区别。
防抖函数是始终等用户输完后x毫秒后执行,而useDeferredValue更加智能,它会根据当前线程是否空闲,自动决定要不要马上执行,如果执行到一半输入了新字符它还可以丢掉 上次未执行完的任务,开始新的任务。
通常useDeferredValue用在前端渲染的列表过滤这种场景中,如果你每次输入字符都要触发新的服务端请求获取新数据,这个还是要使用标准的防抖函数来处理的。
list渲染
key一定要固定且唯一,让react diff算法可以确定哪些是需要更新的先决条件。另外还有虚拟滚动,这个对于上万级别的列表数据渲染优化不可或缺。
减少回流
这个比较宽泛,总之就是会引发回流的一些测量API之类的,使用一定要慎重,尤其是用在hooks或者一些循环执行的函数中,有可能引发性能灾难。对于确实需要高频变化的内容, 优先使用canvas绘制,走gpu绘制,跳过回流重绘直接进入合成阶段性能高。
长任务切片
就是把你的耗时的任务拆分成多个宏任务或者微任务,分批次执行,比如requestIdleCallback在每一帧做完所有工作后如果有空闲会通知到你,生成器函数来拆分一个具体的耗时的任务为多个片段。
