渐进式渲染#
Important
渐进式渲染,或者称作流式渲染。这不是一个只能在 RSC 中可以享受到的特征,这种渲染模式和 Suspense
、 renderToPipeableStream
或 renderToReadableStream
有关。
但是在 Next.js 中你需要使用 App router RSC 才能享受此特征。所以本节讨论 Next.js 的渲染模式。
由于 RSC 组件支持异步,所以组件和组件平行关系之间的渲染并没有相互依赖性,并且可被拆分。多个组件可以谁先兑现谁先渲染。这在组件之间分别获取不同数据时非常好用。
例如一个页面上,存在两个组件,A 组件获取商品列表并渲染输出,B 组件获取商品分类并输出。两者都是独立的逻辑。
在传统 SSR 模式中,页面中组件的数据需要从页面顶层获取向下传递到组件,这样就会导致 A,B 组件的渲染都要等待页面数据获取完才能喜欢渲染。
假设我们的接口为:
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const fetchGoods = async () => {
await sleep(1000)
return [
{
name: 'iPhone 15',
variants: ['Blue'],
},
{
name: 'iPad Pro',
variant: 'Black',
},
]
}
const fetchCategories = async () => {
await sleep(3000)
return ['Electronics', 'Books']
}
export default (props: {
goods: { name: string; variants: string[] }[]
categories: string[]
}) => {
return (
<div>
<h1>Goods</h1>
<ul>
{props.goods.map((good) => (
<li key={good.name}>
{good.name} - {good.variants.join(', ')}
</li>
))}
</ul>
<h1>Categories</h1>
<ul>
{props.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
</div>
)
}
export const getServerSideProps = async () => {
const [goods, categories] = await Promise.all([
fetchGoods(),
fetchCategories(),
])
return {
props: {
goods,
categories,
},
}
}
上面的例子中,服务器响应浏览器至少需要 3s,之后才能在浏览器呈现数据。
如果在 RSC 中,两者之间可以谁先完成谁先渲染。
export default () => {
return (
<>
<Suspense>
<Goods />
</Suspense>
<hr className="my-8 h-1 bg-gray-100" />
<Suspense>
<Categories />
</Suspense>
</>
)
}
const Goods = async () => {
const goods = await fetchGoods()
return (
<div>
<h1>Goods</h1>
<ul>
{goods.map((good) => (
<li key={good.name}>
{good.name} - {good.variants.join(', ')}
</li>
))}
</ul>
</div>
)
}
const Categories = async () => {
const categories = await fetchCategories()
return (
<div>
<h1>Categories</h1>
<ul>
{categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
</div>
)
}
可以看到,等待 1s 后首先渲染出了 Goods,然后 2s 之后渲染出 Categories。这便是渐进式渲染的好处,最大程度提升了 First Meaningful Paint (FMP) 和 Largest Contentful Paint (LCP)。
Important
这种渲染方式虽然提升了首屏的性能,但是因为这个特征也会让页面布局的抖动更加明显,在开发过程中应该更加需要注意这点,尽量在 Supsense fallback 中填充一个和原始组件大小相同的占位。
灵活的服务器数据获取#
由于 RSC 中可以使用任何 Nodejs 方法,所以在数据获取上异常方便,我们不必单独编写一个 Server Api,然后只在 Client 去请求 API,也不必在 SSR 中请求接口,把数据水合到 Client 组件中。我们只需要编写到服务端获取数据的方法,然后直接在 RSC 中调用。
例如,我们现在做一个服务器的管理,其中有组件需要服务的状态信息。
在 RSC 之前我们一般这样去获取服务器的状态信息。
首先,在 SSR 时,使用 getServerSideProps 调用 getServerStatus()
把数据返回,然后在 Page 中接收这个 props。如果需要定时去刷新这个状态的话,我们还需要编写一个 API 接口包装这个方法,在 RCC 中轮询。
https://nextjs-book.innei.in/draw/12.server-status-in-ssr-and-csr.json
在 RSC 中,我们直接调用并渲染,然后使用 revalidatePath()
去做数据刷新,无需编写任何 API。
export default function Page() {
return (
<div className="flex gap-4">
<ServerStatus />
<Revalidate />
</div>
)
}
const ServerStatus = async () => {
// Accessing the server status
const status = await getServerStatus()
return <>{/* Render */}</>
}
'use server'
export const revalidateStatus = async () => {
revalidatePath('/status')
}
'use client'
export const Revalidate = () => {
useEffect(() => {
const timerId = setInterval(() => {
revalidateStatus()
}, 5000)
return () => {
clearInterval(timerId)
}
}, [])
return null
}
这里的 Revalidate 组件 + revalidateStatus
就是利用了 Server Action 的特征去做了页面的数据更新。
看似这里需要写三个文件,又要区分 RSC 和 RCC 好像挺复杂的,但是比起另写 API 和还有手动写 API 的类型定义并且无法做到 End-to-End type safe 的割裂感还是好太多了。
Server Action 的优势#
上一节其实已经利用了 Server Action 完成了页面的数据更新,其实 Server Action 还有其他的用法。
我们知道 Server Action 其实一个 POST 请求,我们编写一个异步的方法,并且标记为 'use server',那么在 RCC 中调用这个方法时,会自动帮你完成:向服务器发送 POST 请求获取这个方法的响应数据,这个数据可以是流式的,并且在此方法中可以调用 revalidate
等方法去触发页面的更新。
文档中一般会告诉你 Server action 来处理表单的提交,触发对数据的更新,最后反应到 UI 上。
不仅如此,其实我们可以利用 Server Action 去获取数据。
还是以上面的例子为例,只不过这次我们全部在 RCC 中实现数据获取和轮询。
```tsx filename="app/server-action/page.tsx" {5,8} 'use client'import useSWR from 'swr'
import { getLoadAvg } from './action'
export default function Page() {
const { data } = useSWR('get-load-avg', getLoadAvg, {
refreshInterval: 1000,
})
return <>Load Average: {data?.loadavg.join(', ')}</>
}
</Tab>
<Tab label="app/server-action/action.tsx">
```tsx filename="app/server-action/action.tsx" {1}
'use server'
import os from 'os'
export const getLoadAvg = async () => {
const avg_load = os.loadavg()
return {
loadavg: avg_load,
}
}
这样不仅少写了一个 API 接口,同时这样的写法也做到了 End-to-End type safe。
另外,推荐阅读:Server Action & Streamable UI
此文由 Mix Space 同步更新至 xLog
原始链接为 https://innei.in/posts/tech/why-react-server-component-2