React 项目性能优化技巧

2025-06-10

React 项目性能优化技巧

在本文中,我们将讨论使我们的 React 项目性能更快、更用户友好的技巧和技术。

通过阅读本文,您可以了解其他 React Hooks 并完成我们之前开始的 Hooks 之旅,并且您还可以通过阅读有关新的 React 18 功能来更新您的 React 知识,最后,您将学到许多用于优化 React 项目的技术。

1. useTransition()

这是 React 18 中引入的最酷的钩子之一,它真的很有用!
如果要解释它,我会先从一个例子开始:

想象一下,您的页面上显示了数千种产品,并且您有一个搜索输入框,可以通过键入来过滤这数千种产品,并通过按键盘上的任意按钮显示相关结果,整个过滤过程将重新开始以显示更新的结果,现在的问题是我们有太多产品,导致我们的过滤过程花费更多的时间,这使得我们的输入功能变慢,换句话说,过滤过程越长,您按下的字母在输入中显示的时间就越晚,您可能知道这个问题很滞后。

您可以查看此演示来了解我在说什么。只需尝试搜索 4444 来过滤产品,然后从输入中删除 4444。您会注意到从输入中删除 4444 需要一些时间。

在 React 中这个过程是这样的:我们有一个查询状态来设置搜索输入 onChnage 的值,并且状态的值传递给输入(状态改变并且输入得到更新),并且我们还有一个包含我们产品的状态,并且在搜索输入 onChnage 处理程序中除了设置查询状态之外,我们还过滤产品并将产品状态设置为过滤后的产品:

过滤过程
那么,搜索输入滞后且不方便用户使用的主要原因是什么?
React 会尝试更新所有状态,然后重新渲染组件并一次性显示包含所有更改的更新 UI。这意味着尽管查询状态更新速度更快,因为它不需要任何特殊过程或类似的东西,但它必须等到其他状态(如产品状态)需要昂贵的过程并需要更多时间才能完成,最后,更新的查询状态和更新的产品状态会传递到屏幕。通过了解这个过程,我们可以理解 React 中的所有状态都是紧急的,没有一个状态具有低优先级,并且 React 会使用所有新状态更改重新渲染组件一次。

注意:过去,大多数人通过限制页面上的数据长度并实现分页等功能来解决此滞后问题,以使过滤过程更轻松,更快速,但现在怎么样?

并发渲染

现在,React 有一个针对此问题的钩子,即 useTransition,除了分页之外,这个钩子使 React 能够具有非紧急状态:
我们希望任何不需要任何过程的状态(例如查询状态)都能得到更新并显示在屏幕上,而不必等待其他状态的更新过程,然后,每当这些重度状态得到更新时,它们就可以显示在屏幕上,这意味着我们希望 React 能够多次重新渲染组件,这称为“并发渲染”。

在现实世界的例子中,就像我计划写这篇文章,同时我必须吃午饭。所以,你认为这有意义吗?我写完了我的文章,但我没有发布它,只是因为我在等午饭准备好,然后我吃了午饭,而只要吃完最后一块午饭,我就发布了我的文章,这样我就同时完成了它们两部分!!嗯,这根本没有意义。使用并发选项,我可以写我的文章,同时,我把午饭放进烤箱准备,一旦我写完文章,我就发布它,不要等午饭准备好,因为它现在优先级很低!所以只要午饭准备好了,我就吃午饭。这样一切都更快更好,对吧?

注意:需要使用 react-dom 的新方法才能让 react 能够使用并发渲染选项:

反应DOM

那么我们该如何使用 useTransition hook 呢?
这个 hook 返回一个包含两个元素的数组:1. isPending,2. startTransition

“isPending”项是布尔值,其值为真,直到我们的非紧急状态更新,我们可以使用此项在 UI 中显示加载内容,以获得更好的用户体验。

“startTransition”项是一个接受回调的函数,在这个回调中,我们设置了所有应该具有低优先级的状态,以使 React 明白它不应该等待这些状态更新,并且它们是非紧急状态,并且它可以在紧急状态更新时首先渲染组件,然后在这些非紧急状态更新时渲染组件:

useTransition

您可以查看演示来尝试一下,看看它有多好。没有滞后的输入或类似的东西,而且我们还有非紧急状态更新的加载:

非紧急状态更新的加载

注意:请记住仅在这些情况下使用此钩子,因为 React 会做一些额外的事情来实现并发渲染并使这些状态变得不紧急,所以你的状态可能不值得承受额外工作的压力并使性能变得更糟!

2. useDeferredValue()

这个钩子的作用和 useTransition 一样,但区别在于,当组件内部可以使用 setState 函数时,我们会使用 useTransition;而有时我们只是将状态作为 prop 获取,而无法在组件内部访问 setState 函数,因此,这时我们就需要使用 useDiferredValue 钩子来让状态变得非紧急。
这个钩子只接受一个参数,那就是状态:

useDiferredValue

注意:请记住,此钩子没有 isPending 选项,因此请尽量使用 useTransition 钩子而不是 useDeferredValue 钩子。

3. useMemo()

假设我们有这样的组件:

图片描述
我们有一个名为greetingFunc的函数,该函数执行一个昂贵的过程并返回一个带有名称参数的字符串,并且我们有一个greeting变量,它等于greetingFucn的返回值(基本上,每次我们定义greeting变量时,我们都会调用greetingFunc,并赋予它一个名称状态以经历昂贵的过程并返回我们想要的值),我们还有一个依赖于darkTheme状态值的主题变量,并通过更改darkTheme状态来更改UI样式。

现在,如果我们通过单击更改主题按钮来更改 darkTheme 状态,React 将重新渲染组件,这意味着将再次声明 greet 变量,并调用该 greetFunc 并赋予它相同的名称状态,而该状态根本没有改变!(换句话说,通过更改 darkTheme 状态,我们还使用具有与以前相同的输入和输出的昂贵过程来调用该函数!)。因此,我们希望仅在输入不同时调用该昂贵函数,并避免不必要的昂贵过程。我们希望记住该函数的返回值,因此如果下次再次调用它,它会比较它收到的输入,如果它与以前不同,那么它可以再次被调用,否则则不能。

这就是 useMemo 处理的工作。useMemo 记住了我们昂贵函数的返回值,如果下次 React 想要再次调用此函数,它会比较旧输入和新输入,你可以将输入视为依赖项,如果输入值没有改变,则意味着返回值是相同的,所以 useMemo hook 已经记住了它 ;)

useMemo hook 接受两个参数,首先,一个回调函数,它返回我们想要记住的函数;其次,一个依赖项数组,用于告诉 React,每当这些依赖项的值发生变化时,React 就会调用我们的函数,并经历昂贵的过程:

使用备忘录

您可以查看演示并尝试一下,一个使用 useMemo 钩子,一个不使用 useMemo,看看每当您更改 darkTheme 状态时,greetingFunc 是否会再次被调用?

4. useCallback()

使用 useMemo 和 useCallback 钩子有两个主要原因:

  1. 引用相等
  2. 计算量巨大的计算

我们讨论了第二个问题(如何使用 useMemo hook 避免昂贵的计算过程)。所以 useCallback hook 的任务就是处理第一个问题(引用相等)。
我们先来看一个例子:

图片描述

正如您在上面的示例中看到的,有时我们将一个函数作为 prop 传递给 childComponent (在我们的示例中是 DummyButton),现在如果您使用增加按钮更改父组件中的状态会发生什么?
父组件将再次重新渲染,这会导致我们的 onClick 函数(我们将其作为 prop 传递给 childComponent)再次被创建。所以在 javaScript 中,当有两个看起来彼此相似的函数或对象时,它们实际上并不相等!因为它们在内存中具有不同的引用,这意味着 onClick 函数与以前不同,即使输出和所有内容都相同,并且每当 childComponent props 发生变化时,react 都会再次重新渲染 childComponent,只是因为新 prop 的引用与旧 prop 不同,这就是引用相等

这时 useCallback hook 就派上用场了。和 useMemo 类似,useCallback 接收两个参数:第一,我们需要记住的函数;第二,依赖项数组。两者语法上唯一的区别在于,在 useCallback 中,我们不会在回调参数中返回函数,而是将目标函数作为回调函数(在 useMemo 中,我们传递一个返回目标函数的回调函数)。因此,通过使用 useCallback hook,每当父组件重新渲染时,React 都会比较 useCallback 第二个参数中旧的和新的依赖项值,如果它们不同,它将使用不同的引用再次创建该函数,这会导致 childComponent 再次重新渲染。如果依赖项没有更改,则无需使用新的引用创建该函数并再次重新渲染 childComponent。可以使用 useCallback hook 修复上述示例,如下图所示。您也可以点击此演示
在线试用,添加 useCallback 并查看其工作原理:

使用回调

注意:useMemo 和 useCallback 之间的区别在于 useMemo 记住函数的返回值以避免再次调用该函数并修复“计算成本高昂的计算”,而 useCallback 记住函数本身以避免再次创建该函数并修复“引用相等”。

注意:仅出于我们上面讨论的这两个原因使用这两个钩子,否则,如果你用这两个钩子包装任何便宜的小函数,React 每次都会比较依赖关系,这对于小事情来说是不值得的,这会导致性能问题

5. React.memo()

图片描述
当父组件内有一堆子组件时,通过重新渲染父组件,其所有子组件都将再次重新渲染,即使它们的 props 没有改变,或者即使它们没有收到任何 props,也没关系,React 无论如何都会重新渲染它们,这会让性能下降!
React 必须在重新渲染之前比较组件的 props,以避免不必要的重新渲染,如果新旧 props 不同,那么 React 可以重新渲染组件,否则就不能,我们可以通过使用 memo 来做到这一点。react.memo
接收一个回调,它是我们想要记住的整个组件。当我们用 react.memo 包装组件时,React 每次都会比较组件的 props,避免不必要的重新渲染。
在上图中,我们没有使用 react.memo,所以每当 App 组件通过改变状态重新渲染时,React 都会再次重新渲染 ChildComponent。为了解决 react.memo 的这个问题,我们可以这样做:

React.memo
您可以通过单击此演示 进行尝试,并使用上面带有备忘录和不带有备忘录的示例,查看每当您通过单击“更新父组件”按钮更新状态时,ChildComponent 是否再次重新渲染,并且“子组件再次重​​新渲染!”文本是否再次记录?

6. 使用 lazy 和 Suspense 进行代码拆分

当我们想在组件中使用一堆组件时,我们只需导入它们即可使用,而导入组件是完全静态的,组件在编译时导入,我们无法告诉 React 在需要时才在父组件中加载导入的组件,或者换句话说,我们无法使其动态导入,以避免浪费时间加载组件,用户甚至可能不会向下滚动查看这些组件。
最常见的用例之一是,当我们在 App 组件中定义不同的路由并导入所有页面组件以用于每个路由时,我们希望在路由是我们指定的路由时加载每个页面组件,否则 React 会一次加载所有页面组件而不关心路径。这就是使用 lazy 和 suspense 进行代码拆分的时候了,这使我们能够在需要时动态加载组件。lazy
和 suspense 帮助我们在需要特定组件时加载组件,因此我们不必一次加载所有组件,并且它对性能有很大帮助:

懒惰和悬念
在上面的例子中,我们动态导入了 Home 和 Panel 组件,并且每当路由为 ' / ' 时,都会加载 Home 组件,每当路由为 ' /panel ' 时,都会加载 Panel 组件。lazy
接收一个返回 import 方法的回调,而 import 方法接收项目中的组件路径(上面例子中的第 5 和第 6 行)。
所有使用 lazy 导入的组件都应该用 suspense 包裹,suspense 接收一个名为 fallback 的 prop,fallback 值是一个 JSX,它用于加载目的,向用户显示加载状态,直到请求的组件准备好并加载完毕,这确实是一个很好的用户体验。

7. React 延迟加载图像组件

假设我们有一个页面,我们从服务器获取 200 张图片以显示在该页面上,每当用户导航到该页面时,它都会发送 HTTP 请求并逐一加载所有 200 张图片,这将花费一些时间来加载所有图片,而用户甚至可能不想向下滚动查看 200 张图片中的至少 10 张图片!那么我们为什么要加载尚未显示在屏幕上的图片呢?
在这种情况下,我们使用一个名为“ React Lazy Load Image Component ”的库,它的工作是通过在需要时动态加载图像来解决此性能问题,并且我们还可以使用占位符或效果等功能为图片显示模糊效果或任何我们想要的图片当图像太大而无法加载时。
我们像这样使用 React Lazy Load Image Component 库:

React 延迟加载图像组件
您可以在此处 查看整个文档

好了,就是这样!这些是一些最酷的技巧和技术,可以提高我们的 React 项目的性能并获得更好的用户体验。如果你仔细使用它们,你将会成为一个更好的 React 开发人员。

这篇文章可以称为“性能优化技巧”,也可以称为“React hooks:第 2 部分”。

再见,祝你好运🤞

鏂囩珷鏉ユ簮锛�https://dev.to/samaghapour/performance-optimization-tips-for-react-projects-4mj
PREV
JavaScript:何时应该使用 forEach 和 map?
NEXT
“Nextron”:Electron + Next.js 之梦