渲染
服务器组件
React服务器组件允许您编写可以在服务器上呈现并可选择缓存的UI。 在Next.js中,渲染工作进一步分割为路由片段,以实现流式传输和部分渲染, 并有三种不同的服务器渲染策略:
本页面将介绍服务器组件的工作原理,何时可能使用它们以及不同的服务器渲染策略。
服务器渲染的好处
在服务器上进行渲染工作有一些好处,包括:
- 数据获取:服务器组件允许您将数据获取移到服务器上,靠近您的数据源。这可以通过减少用于渲染所需数据的获取时间以及客户端需要进行的请求次数来提高性能。
- 安全性:服务器组件允许您将敏感数据和逻辑保留在服务器上,例如令牌和API密钥,而无需将其风险暴露给客户端。
- 缓存:通过在服务器上进行渲染,结果可以被缓存并在后续请求和用户之间重复使用。这可以通过减少每个请求上的渲染和数据获取量来提高性能并减少成本。
- 捆绑大小:服务器组件允许您将以前可能影响客户端JavaScript捆绑大小的大型依赖项保留在服务器上。对于具有较慢互联网或较弱设备的用户来说,这是有益的,因为客户端不必下载、解析和执行服务器组件的任何JavaScript。
- 初始页面加载和首次内容绘制(FCP):在服务器上,我们可以生成HTML,使用户可以立即查看页面,而无需等待客户端下载、解析和执行渲染页面所需的JavaScript。
- 搜索引擎优化和社交网络共享:渲染的HTML可以被搜索引擎爬虫用于索引您的页面,并且社交网络爬虫可以生成您页面的社交卡片预览。
- 流式传输:服务器组件允许您将渲染工作拆分成块并在准备就绪时将其流式传输到客户端。这使用户可以在无需等待整个页面在服务器上渲染完成的情况下更早地看到页面的部分。
在Next.js中使用服务器组件
默认情况下,Next.js使用服务器组件。 这使您可以在没有额外配置的情况下自动实现服务器渲染, 并且您可以在需要时选择使用客户端组件, 详见客户端组件。
服务器组件是如何渲染的?
在服务器上,Next.js使用React的API来编排渲染。 渲染工作被分成块:按独立的路由片段和Suspense边界。
每个块分两个步骤进行渲染:
- React将服务器组件渲染为一种称为React服务器组件负载(RSC Payload)的特殊数据格式。
- Next.js使用RSC Payload和客户端组件JavaScript指令来在服务器上渲染HTML。
然后,在客户端上:
- 使用HTML可以立即显示路 由的快速非交互式预览 - 仅用于初始页面加载。
- 使用React服务器组件负载来协调客户端和服务器组件树,并更新DOM。
- 使用JavaScript指令来使客户端组件水合并使应用程序可交互。
什么是React服务器组件负载(RSC)?
RSC Payload是呈现的React服务器组件树的紧凑二进制表示。 它由React在客户端上使用以更新浏览器的DOM。RSC Payload包含:
- 服务器组件的渲染结果
- 应该呈现客户端组件的占位符以及对它们的JavaScript文件的引用
- 从服务器组件传递给客户端组件的任何props
服务器渲染策略
服务器渲染有三个子集: 静态、 动态和 流式。
静态渲染(默认)
使用静态渲染时,路由在构建时渲染, 或在数据重新验证 后在后台渲染。 结果被缓存,并可以推送到内容交付网络(CDN)。 这种优化允许您在用户和服务器请求之间共享渲染工作的结果。
静态渲染在路由具有不适用于用户的个性化的数据且可以在构建时得知的情况下非常有用, 例如静态博客文章或产品页面。
动态渲染
使用动态渲染时,为每个用户在请求时渲染路由。
动态渲染在路由具有适用于用户的个性化数据或只能在请求时得知的信息(例如cookies或URL的搜索参数)的情况下非常有用。
带有缓存数据的动态路由
在大多数网站中,路由既不是完全静态也不是完全动态 - 这是一个谱。 例如,您可以拥有一个电子商务页面,该页面使用已缓存的产品数据,定期重新验证,但同时也具有未缓存的个性化客户数据。
在Next.js中,可以拥有具有同时使用已缓存和未缓存数据的动态渲染路由。 这是因为RSC Payload和数据分别被缓存。 这使您可以选择动态渲染,而无需担心在请求时获取所有数据的性能影响。
切换到动态渲染
在渲染期间,如果发现动态函数 或未缓存的数据请求, Next.js将切换到动态渲染整个路由。 此表总结了动态函数和数据缓存对路由是静态还是动态渲染的影响:
动态函数 | 数据 | 路由 |
---|---|---|
否 | 已缓存 | 静态渲染 |
是 | 已缓存 | 动态渲染 |
否 | 未缓存 | 动态渲染 |
是 | 未缓存 | 动态渲染 |
在上表中,要使路由完全静态,必须缓存所有数据。 但是,您可以具有同时使用已缓存和未缓存数据获取的动态渲染路由。
作为开发人员,您无需在静态和动态渲染之间进行选择, 因为Next.js将根据所使用的功能和API自动为每个路由选择最佳渲染策略。 相反,您选择何时 缓存或重新验证特定数据, 并可能选择流式传递UI的部分。
动态函数
动态函数依赖于仅在请求时才能知道的信息,例如用户的cookies、当前请求标头或URL的搜索参数。 在Next.js中,这些动态函数包括:
cookies()
和headers()
:在服务器组件中使用这些函数将使整个路由在请求时切换到动态渲染。useSearchParams()
:- 在客户端组件中,它会跳过静态渲染,而是在客户端上渲染到最近的父
Suspense
边界上的所有客户端组件。 - 我们建议将使用
useSearchParams()
的客户端组件包装在<Suspense/>
边界中。这将允许其上面的任何客户端组件被静态渲染。示例。
- 在客户端组件中,它会跳过静态渲染,而是在客户端上渲染到最近的父
searchParams
:使用Pages prop将页面切换到在请求时动态渲染。 使用这些函数之一将使整个路由在请求时切换到动态渲染。
流式传输
流式传输使您能够从服务器逐步呈现UI。工作被分成块,并在准备就绪时流式传输到客户端。 这使用户可以在整个内容完成渲染之前立即看到页面的部分。
流式传输默认内置到Next.js App Router中。 这有助于改善初始页面加载性能,以及依赖于较慢数据获取的UI,该数据获取会阻止整个路由的渲染, 例如产品页面上的评论。
您可以使用loading.js
和具有React Suspense
的UI组件开始流式]传输路由片段。
有关更多信息,请参见Loading UI and Streaming部分。
客户端组件
客户端组件允许您编写可以在请求时在客户端上呈现的交互式UI。 在Next.js中,客户端渲染是可选择的(opt-in), 这意味着您必须明确决定React应在客户端上渲染哪些组件。
本页面将介绍客户端组件的工作原理、它们是如何渲染的以及何时使用它们。
客户端渲染的好处
在客户端进行渲染工作有一些好处,包括:
- 互动性:客户端组件可以使用状态、效果和事件侦听器,这意味着它们可以为用户提供即时反馈并更新UI。
- 浏览器API:客户端组件可以访问浏览器API, 如地理位置或 localStorage,使您能够为特定用例构建UI。
在Next.js中使用客户端组件
要使用客户端组件,
您可以在文件的顶部(在导入之前)添加React的"use client"
指令。
"use client"
用于声明服务器和客户端组件模块之间的边界。
这意味着通过在文件中定义"use client"
,导入到该文件的所有其他模块,包括子组件,都被视为客户端捆绑的一部分。
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
下面的图示显示,如果未定义"use client"
指令,则在嵌套组件(toggle.js
)中使用onClick
和useState
会导致错误。
这是因为默认情况下,在服务器上渲染组件,这些API是不可用的。
通过在toggle.js
中定义"use client"
指令,
您可以告诉React在客户端上渲染组件及其子组件,其中这些API是可用的。
定义多个"use client"
入口点:
您可以在React组件树中定义多个"use client"入口点。这允许您将应用程序拆分为多个客户端捆绑包(或分支)。
但是,并不需要在每个需要在客户端上渲染的组件中定义"use client"。一旦定义了边界,所有子组件和导入到其中的模块都被视为客户端捆绑的一部分。
客户端组件是如何渲染的?
在Next.js中,客户端组件的渲染方式取决于请求是属于完整页面加载(对应应用程序的初始访问或浏览器刷新触发的页面重新加载)还是后续导航的一部分。
完整页面加载
为了优化初始页面加载,Next.js将使用React的API在服务器上为客户端和服务器组件渲染静态HTML预览。这意味着当用户首次访问您的应用程序时,他们将立即看到页面的内容,而无需等待客户端下载、解析和执行客户端组件JavaScript捆绑包。
在服务器上:
- React将服务器组件渲染为一种称为React服务器组件负载(RSC Payload)的特殊数据格式,其中包括对客户端组件的引用。
- Next.js使用RSC Payload和客户端组件JavaScript指令在服务器上为路由渲染HTML。
然后,在客户端上:
- 使用HTML可以立即显示路由的快速非交互式初始预览。
- 使用React服务器组件负载来协调客户端和服务器组件树,并更新DOM。
- 使用JavaScript指令来使客户端组件水合 并使其UI可交互。
什么是水合?
水合是将事件侦听器附加到DOM的过程,以使静态HTML变得可交互。在幕后,
水合是使用hydrateRoot
React API完成的。
后续导航
在后续导航中,客户端组件完全在客户端上渲染,无需服务器渲染的HTML。
这意味着将下载并解析客户端组件JavaScript捆绑包。 一旦捆绑包准备好,React将使用RSC Payload来协调客户端和服务器组件树,并更新DOM。
返回服务器环境
有时,在声明了"use client"边界之后,您可能希望回到服务器环境。 例如,您可能希望减小客户端捆绑包大小、在服务器上获取数据或使用仅在服务器上可用的API。
即使嵌套在客户端组件内,您仍然可以将代码保留在服务器上,方法是交错使用客户端和服务器组件以及 服务器操作。 有关更多信息, 请参见Composition Patterns页面。
服务器和客户端组合模式
在构建React应用程序时,您需要考虑应该在服务器端还是客户端端渲染应用程序的哪些部分。 本页面涵盖了在使用服务器组件和客户端组件时的一些推荐组合模式。
何时使用服务器组件和客户端组件?
以下是服务器组件和客户端组件的不同用例的快速摘要:
要执行的操作是什么? | 服务器组件 | 客户端组件 |
---|---|---|
获取数据 | ✅ | ❌ |
访问后端资源(直接) | ✅ | ❌ |
在服务器上保存敏感信息(访问令牌、API密钥等) | ✅ | ❌ |
将大型依赖项保留在服务器上/减少客户端JavaScript | ✅ | ❌ |
添加互动性和事件侦听器 (onClick()、onChange()等) | ❌ | ✅ |
使用状态和生命周期效果(useState()、useReducer()、useEffect()等) | ❌ | ✅ |
使用仅浏览器API | ❌ | ✅ |
使用依赖于状态、效果或仅浏览器API的自定义挂钩 | ❌ | ✅ |
使用React类组件 | ❌ | ✅ |
服务器组件模式
在选择客户端渲染之前,您可能希望在服务器上执行一些工作,比如获取数据或访问数据库或后端服务。
在处理服务器组件时,以下是一些常见的模式:
在组件之间共享数据
在服务器上获取数据时,可能存在需要在不同组件之间共享数据的情况。 例如,您可能有一个布局和一个依赖于相同数据的页面。
您可以使用fetch或React的cache函数,在组件中获取相同的数据,而无需担心为相同数据发出重复请求,
而不是使用React Context
(在服务器上不可用)或将数据传递为props。
这是因为React会扩展fetch
以自动记忆数据请求,当fetch
不可用时,可以使用cache
函数。
了解有关React中记忆 的更多信息。
将仅在服务器上运行的代码排除在客户端环境之外
由于JavaScript模块可以在服务器和客户端组件模块之间共享,因此仅打算在服务器上运行的代码可能会不经意地进入客户端。
例如,考虑以下数据获取函数:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
乍一看,看起来getData
在服务器和客户端上都可以工作。但是,此函数包含一个API_KEY
,旨在仅在服务器上执行。
由于环境变量API_KEY
未以NEXT_PUBLIC
为前缀,它是一个只能在服务器上访问的私有变量。
为防止环境变量泄漏到客户端,Next.js将私有环境变量替换为空字符串。
因此,即使可以在客户端导入并执行getData()
,它也不会按预期工作。
而使变量公开将使函数在客户端上正常工作,但您可能不希望将敏感信息暴露给客户端。
为防止这种意外使用服务器代码的情况,我们可以使用server-only
包,
以便在其他开发人员意外导入其中一个这些模块到客户端组件时给予构建时错误。
要使用server-only
,首先安装该软件包:
npm install server-only
然后将该软件包导入到包含服务器端代码的任何模块中:
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
现在,导入getData()
的任何客户端组件都将收到一个构建时错误,说明此模块只能在服务器上使用。
相应的client-only
软件包可用于标记包含仅客户端代码的模块,例如访问window
对象的代码。
使用第三方软件包和提供者
由于服务器组件是React的新功能,
生态系统中的第三方软 件包和提供者刚刚开始将"use client"
指令添加到使用仅客户端功能
(如useState
、useEffect
和createContext
)的组件中。
今天,许多来自npm
软件包的组件,这些组件使用仅客户端功能,尚未具有该指令。
这些第三方组件在Client Components中使用时会按预期工作,
因为它们具有"use client"
指令,但在Server Components中不起作用。
例如,假设您已安装了假设的acme-carousel软件包,该软件包具有<Carousel />
组件。
该组件使用useState
,但尚未具有"use client"
指令。
如果在Client Component中使用<Carousel />
,它将按预期工作:
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* 有效,因为Carousel在Client Component中使用 */}
{isOpen && <Carousel />}
</div>
)
}
然而,如果尝试直接在Server Component中使用它,您将看到一个错误:
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 错误:`useState`不能在Server Components中使用 */}
<Carousel />
</div>
)
}
这是因为Next.js不知道<Carousel />
正在使用仅客户端功能。
为解决此问题,您可以在自己的Client Components中包装依赖于客户端功能的第三方组件:
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
现在,您可以直接在Server Component中使用<Carousel />
:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 有效,因为Carousel是Client Component */}
<Carousel />
</div>
)
}
我们不希望您需要包装大多数第三方组件,因为您可能会在Client Components中使用它们。 但是,有一个例外是提供者,因为它们依赖于React状态和上下文,并且通常需要在应用程序的根部。
有关以下的第三方上下文提供程序的更多信息。
使用上下文提供程序
上下文提供程序通常在应用程序的根部附近呈现,以共享全局关注点,如当前主题。 由于React context 不受服务器组件支持,因此在应用程序的根部创建上下文将导致错误:
import { createContext } from 'react'
// createContext在服务器组件中不受支持
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
为解决此问题,请创建您的上下文并在Client Component内部呈现其提供者:
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
现在,您的Server Component将能够直接渲染您的提供者,因为它已被标记为Client Component:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
在根部呈现提供程序后,您应用程序中的所有其他Client Component都将能够使用此上下文。
您应该尽可能在树中的深层次呈现提供程序-
请注意ThemeProvider仅包装了{children}
而不是整个<html>
文档。
这使得Next.js更容易优化服务器组件的静态部分。