Skip to Content
全部文章万论前端React性能优化与新hooks的应用

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在每一帧做完所有工作后如果有空闲会通知到你,生成器函数来拆分一个具体的耗时的任务为多个片段。

最后编辑于

hi