Progressive Rendering#
Important
Progressive rendering, also known as streaming rendering. This is not a feature that can only be enjoyed in RSC. This rendering mode is related to Suspense
, renderToPipeableStream
, or renderToReadableStream
.
However, in Next.js, you need to use the App router RSC to enjoy this feature. So this section discusses the rendering mode of Next.js.
Because RSC components support asynchronous rendering, there is no interdependence between the rendering of components and their parallel relationships, and they can be split. Multiple components can be rendered in the order of completion. This is very useful when different data is obtained between components.
For example, on a page, there are two components, Component A retrieves and renders a list of goods, and Component B retrieves and outputs the categories of goods. Both are independent logic.
In traditional SSR mode, the data of components on the page needs to be obtained from the top of the page and passed down to the components, which means that the rendering of components A and B needs to wait for the page data to be obtained before they can be rendered.
Assuming our interface is:
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,
},
}
}
In the above example, it takes at least 3 seconds for the server to respond to the browser before the data can be rendered in the browser.
If in RSC, whichever component completes first will be rendered first.
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>
)
}
As you can see, after waiting for 1 second, Goods is rendered first, and then Categories is rendered after 2 seconds. This is the advantage of progressive rendering, which maximizes the First Meaningful Paint (FMP) and Largest Contentful Paint (LCP).
Important
Although this rendering method improves the performance of the first screen, it also makes the layout jitter more obvious. In the development process, you should pay more attention to this point and try to fill the fallback of Suspense with a placeholder of the same size as the original component.
Flexible Server Data Fetching#
Because any Node.js method can be used in RSC, data fetching is extremely convenient. We don't need to write a separate server API and only request the API in the client, nor do we need to request the interface in SSR and hydrate the data into the client component. We just need to write a method to get the data on the server side and call it directly in RSC.
For example, let's say we are building a server management system where a component needs to retrieve the status information of the server.
Before RSC, we usually retrieve the server status information like this.
First, in SSR, use getServerSideProps
to call getServerStatus()
and return the data, then receive this props in the Page component. If you need to refresh this status regularly, we also need to write an API to wrap this method and poll it in RCC.
https://nextjs-book.innei.in/draw/12.server-status-in-ssr-and-csr.json
In RSC, we can directly call and render it, and use revalidatePath()
to refresh the data, without writing any 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
}
The Revalidate component + revalidateStatus
here uses the Server Action feature to update the page data.
Although it seems that three files need to be written here, and it seems complicated to distinguish between RSC and RCC, it is much better than writing another API, manually writing the API type definition, and unable to achieve End-to-End type safety.
Advantages of Server Action#
In the previous section, we have actually used Server Action to update the page data, but Server Action has other uses.
We know that Server Action is actually a POST request. We write an asynchronous method and mark it as 'use server'. When this method is called in RCC, it will automatically send a POST request to the server to obtain the response data of this method. This data can be streamed, and methods like revalidate
can be called in this method to trigger page updates.
The documentation usually tells you to use Server Action to handle form submissions, trigger data updates, and reflect them in the UI.
Not only that, but we can also use Server Action to retrieve data.
Using the previous example, but this time we implement data fetching and polling entirely in 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,
}
}
This not only reduces the need to write an API interface, but also achieves End-to-End type safety.
In addition, it is recommended to read: Server Action & Streamable UI
Why RSC - A Different Perspective on Next.js
This article is synchronized to xLog by Mix Space
The original link is https://innei.in/posts/tech/why-react-server-component-2