跳到主要内容

渲染

服务器组件

React服务器组件允许您编写可以在服务器上呈现并可选择缓存的UI。 在Next.js中,渲染工作进一步分割为路由片段,以实现流式传输和部分渲染, 并有三种不同的服务器渲染策略:

本页面将介绍服务器组件的工作原理,何时可能使用它们以及不同的服务器渲染策略。

服务器渲染的好处

在服务器上进行渲染工作有一些好处,包括:

  • 数据获取:服务器组件允许您将数据获取移到服务器上,靠近您的数据源。这可以通过减少用于渲染所需数据的获取时间以及客户端需要进行的请求次数来提高性能。
  • 安全性:服务器组件允许您将敏感数据和逻辑保留在服务器上,例如令牌和API密钥,而无需将其风险暴露给客户端。
  • 缓存:通过在服务器上进行渲染,结果可以被缓存并在后续请求和用户之间重复使用。这可以通过减少每个请求上的渲染和数据获取量来提高性能并减少成本。
  • 捆绑大小:服务器组件允许您将以前可能影响客户端JavaScript捆绑大小的大型依赖项保留在服务器上。对于具有较慢互联网或较弱设备的用户来说,这是有益的,因为客户端不必下载、解析和执行服务器组件的任何JavaScript。
  • 初始页面加载和首次内容绘制FCP):在服务器上,我们可以生成HTML,使用户可以立即查看页面,而无需等待客户端下载、解析和执行渲染页面所需的JavaScript。
  • 搜索引擎优化和社交网络共享:渲染的HTML可以被搜索引擎爬虫用于索引您的页面,并且社交网络爬虫可以生成您页面的社交卡片预览。
  • 流式传输:服务器组件允许您将渲染工作拆分成块并在准备就绪时将其流式传输到客户端。这使用户可以在无需等待整个页面在服务器上渲染完成的情况下更早地看到页面的部分。

在Next.js中使用服务器组件

默认情况下,Next.js使用服务器组件。 这使您可以在没有额外配置的情况下自动实现服务器渲染, 并且您可以在需要时选择使用客户端组件, 详见客户端组件

服务器组件是如何渲染的?

在服务器上,Next.js使用React的API来编排渲染。 渲染工作被分成块:按独立的路由片段和Suspense边界。

每个块分两个步骤进行渲染:

  1. React将服务器组件渲染为一种称为React服务器组件负载(RSC Payload)的特殊数据格式。
  2. Next.js使用RSC Payload和客户端组件JavaScript指令来在服务器上渲染HTML。

然后,在客户端上:

  1. 使用HTML可以立即显示路由的快速非交互式预览 - 仅用于初始页面加载。
  2. 使用React服务器组件负载来协调客户端和服务器组件树,并更新DOM。
  3. 使用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将页面切换到在请求时动态渲染。 使用这些函数之一将使整个路由在请求时切换到动态渲染。

流式传输

sequential parallel data fetching

流式传输使您能够从服务器逐步呈现UI。工作被分成块,并在准备就绪时流式传输到客户端。 这使用户可以在整个内容完成渲染之前立即看到页面的部分。

server rendering with streaming

流式传输默认内置到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",导入到该文件的所有其他模块,包括子组件,都被视为客户端捆绑的一部分。

app/counter.tsx
'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)中使用onClickuseState会导致错误。 这是因为默认情况下,在服务器上渲染组件,这些API是不可用的。 通过在toggle.js中定义"use client"指令, 您可以告诉React在客户端上渲染组件及其子组件,其中这些API是可用的。

use client directive

定义多个"use client"入口点:

您可以在React组件树中定义多个"use client"入口点。这允许您将应用程序拆分为多个客户端捆绑包(或分支)。

但是,并不需要在每个需要在客户端上渲染的组件中定义"use client"。一旦定义了边界,所有子组件和导入到其中的模块都被视为客户端捆绑的一部分。

客户端组件是如何渲染的?

在Next.js中,客户端组件的渲染方式取决于请求是属于完整页面加载(对应应用程序的初始访问或浏览器刷新触发的页面重新加载)还是后续导航的一部分。

完整页面加载

为了优化初始页面加载,Next.js将使用React的API在服务器上为客户端和服务器组件渲染静态HTML预览。这意味着当用户首次访问您的应用程序时,他们将立即看到页面的内容,而无需等待客户端下载、解析和执行客户端组件JavaScript捆绑包。

在服务器上:

  1. React将服务器组件渲染为一种称为React服务器组件负载(RSC Payload)的特殊数据格式,其中包括对客户端组件的引用。
  2. Next.js使用RSC Payload和客户端组件JavaScript指令在服务器上为路由渲染HTML。

然后,在客户端上:

  1. 使用HTML可以立即显示路由的快速非交互式初始预览。
  2. 使用React服务器组件负载来协调客户端和服务器组件树,并更新DOM。
  3. 使用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模块可以在服务器和客户端组件模块之间共享,因此仅打算在服务器上运行的代码可能会不经意地进入客户端。

例如,考虑以下数据获取函数:

lib/data.ts

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

然后将该软件包导入到包含服务器端代码的任何模块中:

lib/data.js
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"指令添加到使用仅客户端功能 (如useStateuseEffectcreateContext)的组件中。

今天,许多来自npm软件包的组件,这些组件使用仅客户端功能,尚未具有该指令。 这些第三方组件在Client Components中使用时会按预期工作, 因为它们具有"use client"指令,但在Server Components中不起作用。

例如,假设您已安装了假设的acme-carousel软件包,该软件包具有<Carousel />组件。 该组件使用useState,但尚未具有"use client"指令。

如果在Client Component中使用<Carousel />,它将按预期工作:

app/gallery.tsx
'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中使用它,您将看到一个错误:

app/page.tsx
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中包装依赖于客户端功能的第三方组件:

app/carousel.tsx
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

现在,您可以直接在Server Component中使用<Carousel />

app/page.tsx
import Carousel from './carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* 有效,因为Carousel是Client Component */}
<Carousel />
</div>
)
}

我们不希望您需要包装大多数第三方组件,因为您可能会在Client Components中使用它们。 但是,有一个例外是提供者,因为它们依赖于React状态和上下文,并且通常需要在应用程序的根部。

有关以下的第三方上下文提供程序的更多信息

使用上下文提供程序

上下文提供程序通常在应用程序的根部附近呈现,以共享全局关注点,如当前主题。 由于React context 不受服务器组件支持,因此在应用程序的根部创建上下文将导致错误:

app/layout.tsx
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内部呈现其提供者:

app/theme-provider.tsx
'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:

app/layout.tsx
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更容易优化服务器组件的静态部分。

库作者的建议

以类似的方式,创建用于被其他开发人员消耗的包的库作者可以使用"use client"指令标记其包的客户端入口点。 这允许包的用户直接将包组件导入其Server Components,而无需创建包装边界。

您可以通过在树的深处使用'use client'来优化包, 允许导入的模块成为Server Component模块图的一部分。

值得注意的是,一些捆绑工具可能会剥离"use client"指令。 您可以在React Wrap BalancerVercel Analytics 存储库中找到如何配置esbuild以在捆绑时包含"use client"指令的示例。

客户端组件

将客户端组件下移树

为了减小客户端JavaScript捆绑包的大小,我们建议将客户端组件下移组件树。

例如,您可能有一个包含静态元素(例如标志、链接等)和使用状态的交互式搜索栏的布局。

与其将整个布局作为客户端组件,不如将交互逻辑移到客户端组件(例如<SearchBar />),并将布局保留为服务器组件。 这意味着您不必将整个布局的组件JavaScript发送到客户端。

app/layout.tsx
// SearchBar是客户端组件
import SearchBar from './searchbar'
// Logo是服务器组件
import Logo from './logo'

// 默认情况下,布局是服务器组件
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}

从服务器传递给客户端组件的props(序列化)

如果在服务器组件中获取数据,您可能希望将数据作为props传递给客户端组件。 从服务器传递给客户端组件的props需要由React进行 序列化

如果您的客户端组件依赖于不可序列化的数据, 您可以使用第三方库在客户端获取数据, 或者通过Route Handler在服务器上获取数据。

交错服务器和客户端组件

在交错客户端和服务器组件时,将UI视为组件树可能会很有帮助。 从根布局 开始,它是一个服务器组件,然后通过添加"use client"指令在客户端上呈现某些子树组件。

在这些客户端子树内,您仍然可以嵌套服务器组件或调用服务器操作,但需要记住一些事项:

  • 在请求-响应生命周期期间,您的代码从服务器移动到客户端。 如果在客户端上需要访问服务器上的数据或资源,您将发起对服务器的新请求-而不是在服务器和客户端之间切换。
  • 当向服务器发出新请求时,首先渲染所有服务器组件,包括嵌套在客户端组件内部的组件。 渲染的结果(RSC Payload)将包含对客户端组件位置的引用。 然后,在客户端上,React使用RSC Payload将服务器和客户端组件协调成一个单一的树。
  • 由于客户端组件在服务器组件之后渲染,因此您不能将服务器组件导入客户端组件模块中(因为这将需要向服务器发起新请求)。 相反,您可以将服务器组件作为props传递给客户端组件。请参见下面的 不受支持的模式受支持的模式部分。

不受支持的模式:将服务器组件导入到客户端组件中

不支持以下模式。您不能将服务器组件导入到客户端组件中:

app/client-component.tsx
'use client'

// 不能将服务器组件导入到客户端组件中。
import ServerComponent from './Server-Component'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>

<ServerComponent />
</>
)
}

支持的模式:将服务器组件作为props传递给客户端组件

支持以下模式。您可以将服务器组件作为props传递给客户端组件。

一种常见的模式是使用React children prop在客户端组件中创建一个“插槽”。

在下面的示例中,<ClientComponent>接受一个children prop:

app/client-component.tsx
'use client'

import { useState } from 'react'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}

<ClientComponent>不知道children最终将由服务器组件的结果填充。 <ClientComponent>唯一的责任是决定children最终将放在何处。

在父服务器组件中, 您可以导入<ClientComponent><ServerComponent>, 并将<ServerComponent>作为<ClientComponent>的子组件传递:

app/page.tsx
// 此模式有效:
// 您可以将服务器组件作为客户端组件的子组件或prop。
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// 在Next.js中,默认情况下,页面是服务器组件
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}

使用这种方法,<ClientComponent><ServerComponent>是解耦的,可以独立呈现。 在这种情况下,子<ServerComponent>可以在服务器上渲染,远早于<ClientComponent>在客户端上渲染。

备注
  • 将内容提升模式已用于在父组件重新渲染时避免重新渲染嵌套的子组件。
  • 您不限于使用children prop。您可以使用任何prop传递JSX。

Edge 和 Node.js 运行时

在 Next.js 的上下文中,运行时指的是在执行过程中可用于代码的一组库、API 和一般功能。

在服务器上,有两种运行时可以渲染您的应用程序代码的部分:

  1. Node.js 运行时(默认)具有对所有 Node.js API 和生态系统中兼容的包的访问权限。
  2. Edge 运行时基于 Web API

运行时差异

在选择运行时时需要考虑许多因素。以下表格快速查看了主要差异。如果您想要更深入地分析差异,请查看下面的各节。

NodeServerlessEdge
Cold Boot/NormalLow
HTTP StreamingYesYesYes
IOAllAllfetch
Scalability/HighHighest
SecurityNormalHighHigh
LatencyNormalLowLowest
npm PackagesAllAllA smaller subset
Static RenderingYesYesNo
Dynamic RenderingYesYesYes
Data Revalidation w/ fetchYesYesYes

Edge 运行时

在 Next.js 中,轻量级的 Edge 运行时是可用 Node.js API 的一个子集。

Edge 运行时非常适合在低延迟下提供动态、个性化内容的情况,其函数简单且资源利用极小,因此速度快。 但在许多场景下,这可能是有限制的。

例如,在 Vercel 上执行的 Edge Runtime 中的代码不能超过 1 MB 到 4 MB 之间。 这个限制包括导入的包、字体和文件,具体取决于您的部署基础设施。

Node.js 运行时

使用 Node.js 运行时可以让您访问所有 Node.js API,以及依赖于这些 API 的所有 npm 包。 然而,与使用 Edge 运行时的路由相比,它启动速度不如后者快。

将 Next.js 应用程序部署到 Node.js 服务器将需要管理、扩展和配置基础设施。 或者,您可以考虑将 Next.js 应用程序部署到 Vercel 等无服务器平台,该平台将为您处理这些事务。

无服务器 Node.js

无服务器是理想的选择,如果您需要一个可处理比 Edge 运行时更复杂的计算负载的可扩展解决方案。 例如,在 Vercel 上使用无服务器函数,您的总代码大小为 50MB,包括导入的包、字体和文件。

与使用 Edge 的路由相比,不足之处在于 Serverless 函数在开始处理请求之前可能需要数百毫秒来启动。 根据您的站点流量,这可能是一个频繁发生的情况,因为这些函数不经常处于“热”状态。

示例

指定 Next.js 应用程序中个别路由段的运行时的选项。 为此,请声明一个名为 runtime 的变量并导出它。 该变量必须是一个字符串,其值必须是 'nodejs''edge' 运行时。

以下示例演示了导出运行时的页面路由段,其值为 'edge'

app/page.tsx
export const runtime = 'edge' // 'nodejs'(默认)| 'edge'

您还可以在布局级别上定义运行时,这将使布局下的所有路由在 edge 运行时上运行:

app/layout.tsx
export const runtime = 'edge' // 'nodejs'(默认)| 'edge'

如果未设置段运行时,则将使用默认的 nodejs 运行时。 如果不打算从 Node.js 运行时更改,则不需要使用运行时选项。

请参阅 Node.js 文档Edge 文档 获取可用 API 的完整列表。 这两个运行时还可以根据您的部署基础设施支持流式 处理。