NextJs 组件
服务端组件 vs 客户端组件
Next.js 中默认是服务端组件,使用 'use client' 声明客户端组件。
服务端组件
tsx
// 默认就是服务端组件,无需声明
export default async function Address() {
const res = await fetch('https://api.example.com/posts')
const data = (await res.json()).slice(0, 10)
return (
<ul>
{data.map(({ title, id }) => (
<li key={id}>{title}</li>
))}
</ul>
)
}// 默认就是服务端组件,无需声明
export default async function Address() {
const res = await fetch('https://api.example.com/posts')
const data = (await res.json()).slice(0, 10)
return (
<ul>
{data.map(({ title, id }) => (
<li key={id}>{title}</li>
))}
</ul>
)
}- 数据获取在服务端执行
- 不会打包到客户端 bundle 中(代码更小)
- 不支持 useState、useEffect 等交互 API
- 可直接 async/await
客户端组件
tsx
'use client'
import { useEffect, useState } from 'react'
export default function Address() {
const [list, setList] = useState([])
useEffect(() => {
fetch('https://api.example.com/todos')
.then(res => res.json())
.then(setList)
}, [])
return <ul>{list.map(item => <li key={item.id}>{item.title}</li>)}</ul>
}'use client'
import { useEffect, useState } from 'react'
export default function Address() {
const [list, setList] = useState([])
useEffect(() => {
fetch('https://api.example.com/todos')
.then(res => res.json())
.then(setList)
}, [])
return <ul>{list.map(item => <li key={item.id}>{item.title}</li>)}</ul>
}- 支持交互 API 和浏览器 API
- 代码会打包到客户端 bundle 中
对比
| 服务端组件 | 客户端组件 | |
|---|---|---|
| 数据获取 | 更快(服务端直连) | 需要客户端请求 |
| 安全性 | 逻辑不暴露给前端 | 代码对前端可见 |
| 缓存 | 渲染结果可缓存 | 无 |
| Bundle | 不会打包到客户端 | 会打包到客户端 |
| FCP | 更短 | 较长 |
| 交互 | 不支持(无 state/effect) | 完整支持 React 交互 |
| 运行时机 | 构建时 + 服务端 | 构建时 + 服务端 + 客户端 |
水合(Hydration)
SSR 将组件渲染为 HTML,但 HTML 没有交互性。客户端下载 JS 后为 HTML 赋予交互性的过程称为水合。
传统 SSR 的问题:
- 数据的获取必须在组件渲染之前
- 所有组件的 JS 必须先加载到客户端才能开始水合
- 所有组件都必须水合完毕才能交互
Suspense 与 Streaming
Suspense 允许推迟渲染部分内容,直到数据加载完毕:
tsx
import { Suspense } from 'react'
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function Component1() {
await sleep(2000)
return <h1>Hello Component1</h1>
}
async function Component2() {
await sleep(3000)
return <h1>Hello Component2</h1>
}
export default function Page() {
return (
<section>
<Suspense fallback={<p>Loading Component1</p>}>
<Component1 />
</Suspense>
<Suspense fallback={<p>Loading Component2</p>}>
<Component2 />
</Suspense>
</section>
)
}import { Suspense } from 'react'
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function Component1() {
await sleep(2000)
return <h1>Hello Component1</h1>
}
async function Component2() {
await sleep(3000)
return <h1>Hello Component2</h1>
}
export default function Page() {
return (
<section>
<Suspense fallback={<p>Loading Component1</p>}>
<Component1 />
</Suspense>
<Suspense fallback={<p>Loading Component2</p>}>
<Component2 />
</Suspense>
</section>
)
}Streaming 是 Suspense 背后的实现技术,将 HTML 拆分为多个 chunk 逐步发送:
- 提前发送的组件可提前水合
- 减少 TTFB、FCP、TTI
在 Next.js 中实现 Streaming 的两种方式:
- 组件级别:使用
Suspense - 页面级别:使用
loading.tsx
RSC vs SSR
| RSC(React Server Components) | SSR(Server-Side Rendering) | |
|---|---|---|
| 重点 | 组件级别的细粒度渲染控制 | 页面级别的服务端渲染 |
| 代码打包 | 组件代码不打包到 bundle | 所有依赖都打包到 bundle |
| 数据获取 | 组件中直接获取 | 需通过 getServerSideProps |
| 交互性 | 只适用于静态内容 | 水合后完整交互 |
RSC 解决了 SSR 的核心缺陷:用户需要下载的 JS 代码量不会减少。使用 RSC(服务端组件)可以将不必要的代码隐藏在服务端。
Cao Note