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
,更新用户界面,以确保更新的呈现是流畅的,并避免对用户体验产生不良影响。
使用过渡功能对于 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
中显著减少了长时间任务的数量和总阻塞时间,相比没有使用过渡的实现的性能图表。这表明使用过渡功能对于优化应用程序的性能和用户体验是非常有效的
性能选项卡显示长任务数量和总阻塞时间明显减少了。
过渡(transitions)
是 React
渲染模型中的一个基本变革,使 React
能够同时渲染多个版本的用户界面,并在不同任务之间管理优先级。这使得用户体验更加流畅和响应,尤其在处理高频更新
或 CPU 密集
的渲染任务时。过渡功能的引入为 React
应用程序的性能和交互性带来了显著的提升。
4. React Server Components
React Server Components
(简称RSC
) 是 React 18
中的实验性功能,但是,有些框架已经决定适配该功能.
传统上,React
提供了几种主要的渲染方式。
客户端渲染CSR
完全在客户端渲染所有内容
服务端渲染SSR
在服务器上将组件树渲染为 HTML
,并将这个静态 HTML 与 JavaScript 捆绑包一起发送到客户端
,用于在客户端进行组件的挂载
这两种方法都依赖于一个事实,即同步的
React
渲染器需要使用已经传递的 JavaScript 捆绑包在客户端重新构建组件树,尽管这个组件树在服务器上已经可用。
在CSR
和SSR
中,都需要通过 JavaScript 捆绑包在客户端重建组件树。
- 在
CSR
中,整个渲染过程发生在客户端的浏览器中,JavaScript
捆绑包负责生成组件树和渲染用户界面。 - 在
SSR
中,服务器预先将组件树渲染为 HTML 并将其与JavaScript
捆绑包一起发送到客户端,然后客户端接管渲染过程并挂载组件,使其成为可交互。
在这两种情况下,组件树都需要在客户端重新构建,尽管在服务器上已经有一个可用的组件树。这可能导致加载时间增加,并潜在地影响性能和用户体验。
RSC
允许 React
将实际序列化的组件树发送给客户端。客户端的 React
渲染器理解这种格式,并使用它来高效地重构 React
组件树,而无需发送 HTML 文件或 JavaScript 捆绑包。
我们可以通过将 react-server-dom-webpack/server
的 renderToPipeableStream
方法与 react-dom/client
的 createRoot
方法结合使用来采用这种新的渲染模式。
react-server-dom-webpack/server
的renderToPipeableStream
方法用于在服务器端将组件树序列化为可流式传输的格式,然后将其发送给客户端。react-dom/client
的createRoot
方法用于在客户端接收并高效地重构从服务器端传输的组件树,从而完成渲染。
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
对象,或使用像 useState
或 useEffect
这样的钩子。
要将一个组件及其导入添加到 JavaScript 捆绑包中,并将其发送到客户端,从而使其具有交互性,可以在文件的顶部使用 use client
捆绑器指令。这会告诉捆绑器将此组件及其导入添加到客户端捆绑包,并告诉 React
在客户端进行挂载以增加交互性。这样的组件被称为客户端组件
(Client Components)。
注意:不同的框架实现可能会有所不同。例如,
Next.js
会在服务器上预渲染客户端组件为 HTML,类似于传统的 SSR 方法。然而,默认情况下,客户端组件的渲染方式类似于 CSR 方法。
确实,当使用客户端组件
时,优化捆绑包大小是开发者的责任。开发者可以通过以下方式实现优化:
- 确保只有交互组件的最终子节点定义了
use client
指令。这可能需要对一些组件进行解耦。 - 通过
props
传递组件树,而不是直接导入它们。这使得React
可以将子组件渲染为RSC
,而无需将它们添加到客户端捆绑包中。这样可以减少客户端捆绑包的大小。
5. Suspence
另一个重要的新并发功能是 Suspense
。虽然 Suspense
并不是完全新的,因为它在 React 16
中用于 React.lazy
的代码拆分,但在 React 18
中引入了新的功能,将 Suspense 扩展到数据获取领域。
使用
Suspense
,我们可以延迟组件的渲染,直到满足特定条件,比如从远程源加载数据。同时,我们可以渲染一个占位组件
,表示该组件仍在加载中。
通过声明式地定义加载状态,我们减少了对条件渲染逻辑的需求。将 Suspense
与 RSC
结合使用,我们可以直接访问服务器端的数据源,而无需额外的 API 端点,比如数据库或文件系统。
async function BlogPosts() { const posts = await db.posts.findAll(); return '...'; } export default function Page() { return ( <Suspense fallback={<Skeleton />}> <BlogPosts /> </Suspense> ) }
使用
RSC
与Suspense
的结合可以无缝地工作,这允许我们在组件加载过程中定义加载状态。
Suspense
的真正威力在于它与 React
的并发特性深度整合。当一个组件被暂停(例如因为它仍在等待数据加载),React
并不会无所作为,直到组件接收到数据为止。相反,它会暂停被挂起组件的渲染,并将重点转向其他任务。
这种行为使得 React
能够更加智能地管理任务优先级,优化应用程序的性能和用户体验。当一个组件暂停时,React
会继续处理其他重要任务,如用户交互或渲染其他已准备好的组件。一旦挂起的组件获取到所需的数据,React
就会恢复其渲染,保证用户界面的流畅和响应。这种能力使得 Suspense
与并发特性的结合能够实现更高效的数据加载和渲染过程,提升应用程序的性能和用户体验。
在此期间,我们可以告诉 React
渲染一个备用的用户界面,以指示该组件仍在加载中。一旦等待的数据可用,React
就可以无缝地以中断的方式恢复先前被暂停的组件渲染。
React
还可以根据用户交互重新设置组件的优先级。例如,当用户与一个当前未被渲染的挂起组件进行交互时,React
会暂停正在进行的渲染,并将用户正在交互的组件设为优先级。
通过这种方式,React
能够更加智能地管理任务的优先级,根据用户交互动态地调整组件的渲染优先级,从而提供更好的用户体验。Suspense
与并发特性的结合为 React
提供了强大的渲染控制能力,使得应用程序的渲染过程更加灵活高效,同时保证了用户界面的流畅性和响应性。
一旦准备好,React
将其提交到 DOM
,并恢复先前的渲染。这确保了用户交互的优先级,并使用户界面保持响应,并随着用户输入实时更新。
Suspense
与 RSC
的流式格式的结合允许高优先级的更新在准备好后立即发送到客户端,而无需等待较低优先级的渲染任务完成。这使得客户端能够更早地开始处理数据,并通过逐步以非阻塞的方式展示内容,提供更流畅的用户体验。
这种可中断的渲染机制与 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。cache
和 fetch
的自动缓存行为允许将单个函数从全局模块导出,并在整个应用程序中重复使用它,这样可以更加高效地处理数据获取和记忆化。这样的设计使得在 RSC
中处理数据获取变得更加简便和高效。
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
应用程序带来了更高效和更流畅的用户体验。
后记
分享是一种态度。
参考资料:
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。