React 19 will officially introduce the concept of React Server Component (RSC), and Client Component and Server Component will be officially separated from now on. Next.js has supported Server Component since version 13. So why RSC? What are the advantages? In this chapter, we will discuss this issue.
Avoiding Hydration Errors#
The introduction of RSC reduces the occurrence of hydration errors. If you only use Server Component to describe all components, hydration errors will not occur.
First, let's review why hydration errors occur.
We know that in traditional SSR architecture, the code is isomorphic, which means that the server needs to render the page and return HTML to the browser for static rendering before the page is rendered. After the JS is loaded, the browser executes the JS code again to re-run this code and bind the state and event interactions to the UI. If the state at this step is inconsistent with the state during server rendering, hydration errors will occur.
Let's take a simple example - displaying the current server time. Suppose we need the UI to show the current time. We quickly write the following code:
import { useEffect, useState } from 'react'
export default function Home() {
return <div>{Date.now()}</div>
}
Due to hydration, the time on the browser is different from the time during server rendering, resulting in inconsistent data. This leads to a hydration error.
Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
In Next.js versions 14.2.x and above, you can have a clearer understanding of why this problem occurs.
In traditional SSR, data hydration needs to be handled manually. For example, in the above example, we need to display the server time. We need to use getServerSideProps
to ensure the consistency of the data.
import { useEffect, useState } from 'react'
export default function Home({ props: time }: { props: number }) {
return <div>{time}</div>
}
export const getServerSideProps = async () => {
return {
props: Date.now(),
}
}
This separation-based approach becomes very difficult to manage in the case of a large number of states, and the retrieval of server data must be centralized in the current page instead of in individual components, which makes the development experience more complex.
For example, when you need to retrieve more server data and the component depends on server data, you need to pass the server data from the top-level of the page to each component. If the component hierarchy is deep, you have to use Context or state management libraries to pass the data, even if the component logic is not strongly associated with the page. This approach limits the reusability of components because such components always need to retrieve server data from the top-level of the page instead of having independent logic to obtain the state:
https://nextjs-book.innei.in/draw/10.page-server-props.json
So, in the RSC mode, we can easily combine the required data with the components. For example, in the above example, we can quickly encapsulate it into a component.
```tsx filename="app/page.tsx" import { Servertime } from './components/server-time'export default function Home() {
return
}
</Tab>
<Tab label="app/components/server-time.tsx">
```tsx filename="app/components/server-time.tsx"
import { ServertimeClient } from './server-time.client'
export function Servertime() {
return <ServertimeClient time={Date.now()} />
}
import { useEffect, useState } from 'react'
export function ServertimeClient(props: { time: number }) {
const [currentTime, setCurrentTime] = useState(props.time)
useEffect(() => {
// Simple time handling, not reliable.
const id = setInterval(() => {
setCurrentTime(time => time + 1000)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
return
}
</Tab>
</Tabs>
In the above example, the `ServerTime` component can be used in any Server Component without the need to pass props.
```excalidraw
https://nextjs-book.innei.in/draw/11.app-server-props.json
Smaller Bundle Size#
Since Server Components only run on the server, the external libraries used in Server Components will not be loaded on the browser. This is more convenient for many tasks that require third-party libraries to process data or charts. Generally, these libraries have large file sizes, and these data can be processed only on the server side. By reducing the JS loaded on the browser, it reduces network load and improves the performance of the initial rendering.
Here is a simple example. For example, code highlighting is usually implemented using third-party libraries such as Prism or Shiki. These libraries generally have large file sizes, and if all languages need to be imported, the bundle size after packaging may increase by several megabytes.
Normally, we would use lazy
or dynamic
to split the code of components that use these libraries to prevent loading large JS files on the initial page and reduce the LCP metric.
const HighLighter = lazy(() =>
import('./components/shiki').then((mod) => ({
default: mod.HighLighter,
})),
)
export default function () {
return (
<div>
<Suspense fallback={'loading code block..'}>
<HighLighter content='const foo = "bar";' lang="ts" />
</Suspense>
</div>
)
}
However, since the HTML returned by the server already contains the highlighted DOM, it is unnecessary for the browser to download Shiki and perform highlighting again.
With Server Components, the logic of this component is completed on the server side, so there is no logic for rendering this component on the frontend, and naturally, Shiki will not be downloaded. Therefore, Shiki will not be included in the client's JS bundle.
import { bundledLanguages, getHighlighter } from 'shiki'
import type { FC } from 'react'
import type {
BundledLanguage,
BundledTheme,
CodeToHastOptions,
HighlighterCore,
} from 'shiki'
function codeHighlighter(
highlighter: HighlighterCore,
{
lang,
attrs,
code,
}: {
lang: string
attrs: string
code: string
},
) {
const codeOptions: CodeToHastOptions<BundledLanguage, BundledTheme> = {
lang,
meta: {
__raw: attrs,
},
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
return highlighter.codeToHtml(code, {
...codeOptions,
transformers: [...(codeOptions.transformers || [])],
})
}
export const HighLighter: FC<{
lang: string
content: string
}> = async (props) => {
const { lang: language, content: value } = props
const highlighter = await getHighlighter({
themes: [
import('shiki/themes/github-light.mjs'),
import('shiki/themes/github-dark.mjs'),
],
langs: Object.keys(bundledLanguages),
})
return (
<div
dangerouslySetInnerHTML={{
__html: codeHighlighter(highlighter, {
attrs: '',
code: value,
lang: language || '',
}),
}}
/>
)
}
export default () => {
return <HighLighter content='const foo = "bar"' lang="ts" />
}
The effect is significant.
This article is synchronized updated to xLog by Mix Space.
The original link is https://innei.in/posts/tech/why-react-server-component-1