React 18 如何提升应用性能(二)

简介: React 18 如何提升应用性能(二)

3. 过渡

我们可以通过使用 useTransition 钩子提供的 startTransition 函数将更新标记为非紧急。这是一个强大的新功能,允许我们将某些状态更新标记为过渡(transitions),表示它们可能会导致视觉变化,如果它们同步渲染,可能会影响用户体验

通过将状态更新包装在 startTransition 中,我们可以告诉 React 我们可以推迟中断渲染,以优先处理更重要的任务,以保持当前的用户界面的交互性。

这样的做法可以提高应用程序的性能,并确保用户界面的流畅和响应性。

import { useTransition } from "react";
function Button() {
  const [isPending, startTransition] = useTransition();
  return (
    <button 
      onClick={() => {
        // 紧急更新
        urgentUpdate();
+        startTransition(() => {
+          nonUrgentUpdate() // 非紧急更新
+        })
      }}
    >...</button>
  )
}

当过渡开始时, 并发渲染器会在后台准备新的组件树。一旦渲染完成,它会将结果保存在内存中,直到 React 调度程序能够高效地更新 DOM 来反映新的状态。

这个时机可能是当浏览器处于空闲状态,并且没有更高优先级的任务(比如用户交互)在等待执行时。

在这个时机,React 将会将新的渲染结果提交到 DOM,更新用户界面,以确保更新的呈现是流畅的,并避免对用户体验产生不良影响。

image.png


使用过渡功能对于 CitiesList 示例非常适合。不必在每次输入时直接更新传递给 searchQuery 参数的值,这样会导致每次键入都触发同步渲染调用。相反,我们可以将状态分成两个值,并在 searchQuery 的状态更新中使用 startTransition

这告诉 React,状态更新可能会导致对用户造成视觉上的干扰,因此 React 应该尽力保持当前用户界面的交互性,同时在后台准备新的状态,而不立即提交更新。通过这样的方式,React 可以更加智能地管理渲染优先级,优化用户体验,确保用户界面的流畅和响应性。

import React, { useState, useTransition } from "react";
import CityList from "./CityList";
export default function SearchCities() {
  const [text, setText] = useState("太原");
  const [searchQuery, setSearchQuery] = useState(text);
+  const [isPending, startTransition] = useTransition();
   return (    
      <main>      
          <input  
              type="text" 
              value={text}
              onChange={(e) => {
                 setText(e.target.value)
+                 // 减低该操作的,渲染优先级
                 startTransition(() => {
                    setSearchQuery(e.target.value)
                 })
             }}  />      
          <CityList searchQuery={searchQuery} />    
      </main>  
     );
};

现在,当我们在输入框中输入时,用户输入保持流畅,在按键之间没有任何视觉延迟出现。这是因为文本状态仍然同步更新,输入框使用该状态作为其值。

后台React 在每次输入时开始渲染新的组件树。但是,与同步任务的all-or-nothing不同,React 开始在内存中准备新版本的组件树,同时当前用户界面(显示“旧”状态)仍然对进一步的用户输入保持响应

查看性能选项卡,将状态更新包装在 startTransition 中显著减少了长时间任务的数量和总阻塞时间,相比没有使用过渡的实现的性能图表。这表明使用过渡功能对于优化应用程序的性能和用户体验是非常有效的

image.png

性能选项卡显示长任务数量和总阻塞时间明显减少了。

过渡(transitions)React 渲染模型中的一个基本变革,使 React 能够同时渲染多个版本的用户界面,并在不同任务之间管理优先级。这使得用户体验更加流畅和响应,尤其在处理高频更新CPU 密集的渲染任务时。过渡功能的引入为 React 应用程序的性能和交互性带来了显著的提升。


4. React Server Components

React Server Components(简称RSC) 是 React 18 中的实验性功能,但是,有些框架已经决定适配该功能.

传统上,React 提供了几种主要的渲染方式。

客户端渲染CSR

完全在客户端渲染所有内容

image.png

服务端渲染SSR

在服务器上将组件树渲染为 HTML,并将这个静态 HTML 与 JavaScript 捆绑包一起发送到客户端,用于在客户端进行组件的挂载

image.png

这两种方法都依赖于一个事实,即同步的 React 渲染器需要使用已经传递的 JavaScript 捆绑包在客户端重新构建组件树,尽管这个组件树在服务器上已经可用

CSRSSR中,都需要通过 JavaScript 捆绑包在客户端重建组件树。

  • CSR 中,整个渲染过程发生在客户端的浏览器中JavaScript 捆绑包负责生成组件树和渲染用户界面。
  • SSR 中,服务器预先将组件树渲染为 HTML 并将其与 JavaScript 捆绑包一起发送到客户端,然后客户端接管渲染过程并挂载组件,使其成为可交互。

在这两种情况下,组件树都需要在客户端重新构建,尽管在服务器上已经有一个可用的组件树。这可能导致加载时间增加,并潜在地影响性能和用户体验。


RSC 允许 React 将实际序列化的组件树发送给客户端。客户端的 React 渲染器理解这种格式,并使用它来高效地重构 React 组件树,而无需发送 HTML 文件或 JavaScript 捆绑包

image.png

我们可以通过将 react-server-dom-webpack/serverrenderToPipeableStream 方法与 react-dom/clientcreateRoot 方法结合使用来采用这种新的渲染模式。

  • react-server-dom-webpack/serverrenderToPipeableStream 方法用于在服务器端将组件树序列化为可流式传输的格式,然后将其发送给客户端。
  • react-dom/clientcreateRoot 方法用于在客户端接收并高效地重构从服务器端传输的组件树,从而完成渲染。

server/index.cjs

const express = require('express');
const {renderToPipeableStream} = require('react-server-dom-webpack/server');
const ReactApp = require('../src/App').default;
const PORT = process.env.PORT || 4000;
const app = express();
app.listen(PORT, () => {
  console.log(`Listening at ${PORT}...`);
});
app.get('/rsc', async function(req, res) {
  const {pipe} = renderToPipeableStream(
    React.createElement(ReactApp),
  );
  return pipe(res);
});

src/index.js

"use client";
import { createRoot } from 'react-dom/client';
import { createFromFetch } from 'react-server-dom-webpack/client';
export function Index() {
  ...
  return createFromFetch(fetch('/rsc'));
}
const root = createRoot(document.getElementById('root'));
root.render(<Index />);

默认情况下,React 不会对 RSC 进行挂载。这些组件不应该使用任何客户端属性,比如访问 window 对象,或使用像 useStateuseEffect 这样的钩子。

要将一个组件及其导入添加到 JavaScript 捆绑包中,并将其发送到客户端,从而使其具有交互性,可以在文件的顶部使用 use client 捆绑器指令。这会告诉捆绑器将此组件及其导入添加到客户端捆绑包,并告诉 React 在客户端进行挂载以增加交互性。这样的组件被称为客户端组件(Client Components)。

image.png

注意:不同的框架实现可能会有所不同。例如,Next.js 会在服务器上预渲染客户端组件为 HTML,类似于传统的 SSR 方法。然而,默认情况下,客户端组件的渲染方式类似于 CSR 方法。

确实,当使用客户端组件时,优化捆绑包大小是开发者的责任。开发者可以通过以下方式实现优化:

  1. 确保只有交互组件的最终子节点定义了 use client 指令。这可能需要对一些组件进行解耦。
  2. 通过 props 传递组件树,而不是直接导入它们。这使得 React 可以将子组件渲染为 RSC,而无需将它们添加到客户端捆绑包中。这样可以减少客户端捆绑包的大小。

5. Suspence

另一个重要的新并发功能是 Suspense。虽然 Suspense 并不是完全新的,因为它在 React 16 中用于 React.lazy代码拆分,但在 React 18 中引入了新的功能,将 Suspense 扩展到数据获取领域

使用 Suspense,我们可以延迟组件的渲染,直到满足特定条件,比如从远程源加载数据。同时,我们可以渲染一个占位组件,表示该组件仍在加载中。

通过声明式地定义加载状态,我们减少了对条件渲染逻辑的需求。将 SuspenseRSC结合使用,我们可以直接访问服务器端的数据源,而无需额外的 API 端点,比如数据库或文件系统。

async function BlogPosts() {
  const posts = await db.posts.findAll();
  return '...';
}
export default function Page() {
  return (
    <Suspense fallback={<Skeleton />}>
      <BlogPosts />
    </Suspense>
  )
}

使用 RSCSuspense 的结合可以无缝地工作,这允许我们在组件加载过程中定义加载状态

Suspense 的真正威力在于它与 React并发特性深度整合。当一个组件被暂停(例如因为它仍在等待数据加载),React 并不会无所作为,直到组件接收到数据为止。相反,它会暂停被挂起组件的渲染,并将重点转向其他任务。

这种行为使得 React 能够更加智能地管理任务优先级,优化应用程序的性能和用户体验。当一个组件暂停时,React 会继续处理其他重要任务,如用户交互或渲染其他已准备好的组件。一旦挂起的组件获取到所需的数据,React 就会恢复其渲染,保证用户界面的流畅和响应。这种能力使得 Suspense 与并发特性的结合能够实现更高效的数据加载和渲染过程,提升应用程序的性能和用户体验。

image.png

在此期间,我们可以告诉 React 渲染一个备用的用户界面,以指示该组件仍在加载中。一旦等待的数据可用,React 就可以无缝地以中断的方式恢复先前被暂停的组件渲染。

React 还可以根据用户交互重新设置组件的优先级。例如,当用户与一个当前未被渲染的挂起组件进行交互时,React 会暂停正在进行的渲染,并将用户正在交互的组件设为优先级。

通过这种方式,React 能够更加智能地管理任务的优先级,根据用户交互动态地调整组件的渲染优先级,从而提供更好的用户体验。Suspense 与并发特性的结合为 React 提供了强大的渲染控制能力,使得应用程序的渲染过程更加灵活高效,同时保证了用户界面的流畅性和响应性。

image.png

一旦准备好,React 将其提交到 DOM,并恢复先前的渲染。这确保了用户交互的优先级,并使用户界面保持响应,并随着用户输入实时更新。

SuspenseRSC 的流式格式的结合允许高优先级的更新在准备好后立即发送到客户端,而无需等待较低优先级的渲染任务完成。这使得客户端能够更早地开始处理数据,并通过逐步以非阻塞的方式展示内容,提供更流畅的用户体验。

这种可中断的渲染机制与 Suspense 处理异步操作的能力相结合,为用户提供了更加流畅和以用户为中心的体验,特别适用于具有大量数据获取需求的复杂应用程序。

通过这些并发特性,React 能够更加智能地管理任务的优先级,实现更高效的渲染和数据处理过程,为用户提供更好的交互体验,使得应用程序在处理异步操作时更加平滑和高效。


6. 数据获取

除了渲染更新外,React 18 还引入了一种新的 API 来高效地获取数据并对结果进行记忆

React 18 现在有一个 cache 函数,它可以缓存函数调用的结果。如果在同一次渲染过程中使用相同的参数再次调用相同的函数,它将使用记忆化的值,而无需再次执行该函数。

import { cache } from 'react'
export const getUser = cache(async (id) => {
  const user = await db.user.findUnique({ id })
  return user;
})
getUser(1)
getUser(1) // 传人的参数相同,使用缓存的数据

在数据获取的 fetch 调用中,React 18 现在默认包含了类似的缓存机制,无需使用 cache 函数。这有助于减少在单个渲染过程中的网络请求次数,从而提高应用程序的性能并降低 API 成本。

export const fetchPost = (id) => {
  const res = await fetch(`https://.../posts/${id}`);
  const data = await res.json();
  return { post: data.post } 
}
fetchPost(1)
fetchPost(1) //  传人的参数相同,使用缓存的数据

这些特性在使用 RSC时非常有用,因为它们无法访问 Context API。cachefetch 的自动缓存行为允许将单个函数从全局模块导出,并在整个应用程序中重复使用它,这样可以更加高效地处理数据获取和记忆化。这样的设计使得在 RSC中处理数据获取变得更加简便和高效。

image.png

async function fetchBlogPost(id) {
  const res = await fetch(`/api/posts/${id}`);
  return res.json();
} 
async function BlogPostLayout() {
  const post = await fetchBlogPost('123');
  return '...'
}
async function BlogPostContent() {
   // 请求参数和之前的一样,返回缓存后的值
  const post = await fetchBlogPost('123'); 
  return '...'
}
export default function Page() {
  return (
    <BlogPostLayout>
      <BlogPostContent />
    </BlogPostLayout>
  )
}

总结

总体而言,React 18 的最新特性在许多方面都提高了性能。

  • 使用 Concurrent,渲染过程可以被暂停、延迟或甚至放弃。这意味着即使正在进行大规模的渲染任务,用户界面仍可以立即响应用户输入。
  • Transitions API 允许在数据获取或屏幕切换期间实现更平滑的过渡,而不会阻塞用户输入。
  • RSC 允许开发者构建在服务器和客户端上都可用的组件,将客户端应用程序的交互性与传统服务器渲染的性能相结合,而无需付出 hydration 的代价。
  • 扩展的 Suspense 功能通过允许应用程序的部分内容在其他需要更长时间获取数据的部分之前渲染,提高了加载性能。

这些新特性共同为 React 应用程序带来了更高效和更流畅的用户体验。


后记

分享是一种态度

参考资料:

  1. Rust并发
  2. 浏览器之性能指标-TBT
  3. react 18 新特性

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关文章
|
2月前
|
前端开发 JavaScript UED
使用React Hooks优化前端应用性能
本文将深入探讨如何使用React Hooks来优化前端应用的性能,重点介绍Hooks在状态管理、副作用处理和组件逻辑复用方面的应用。通过本文的指导,读者将了解到如何利用React Hooks提升前端应用的响应速度和用户体验。
|
4月前
|
缓存 前端开发 JavaScript
React 18 如何提高应用性能?
React 18 如何提高应用性能?
|
5月前
|
前端开发 JavaScript API
React 18 如何提升应用性能(一)
React 18 如何提升应用性能(一)
|
10月前
|
缓存 前端开发 JavaScript
React 18 如何提升应用性能
React18 引入了并发功能,从根本上改变了React的渲染方式。一起探索:Concurrent/Transitions API/React Server Components/Suspense 的奥秘
59 0
React 18 如何提升应用性能
|
前端开发 JavaScript 算法
React-利用React-Profiler提升应用性能
React Profiler 的组成 推荐阅读指数 ⭐️⭐️⭐️ 如何通过React Profiler查询并改正页面耗时操作 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
100 0
React-利用React-Profiler提升应用性能
|
4月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
122 0
|
4月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
33 0
|
4月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
35 0
|
4月前
|
存储 前端开发 JavaScript
【第29期】一文学会用React类组件编写组件
【第29期】一文学会用React类组件编写组件
37 0
|
4月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
43 0