技术篇:CSR、SSR、SSG、ISR 与客户端、服务端组件
Next.js实战教程 - 系列文章
本节内容是这个系列字数最长,概念最多的一节。请大家在阅读时多一些耐心。本节将基于 Next.js 详细介绍四种主要的渲染方式:客户端渲染(CSR)、服务端渲染(SSR)、静态站点生成(SSG)以及增量静态生成(ISR)。这些渲染模式将基于 Next.js 的 Pages Router 进行演示和讨论。服务端组件和客户端组件将基于 App Router 进行演示和讨论。
客户端渲染(CSR)、服务端渲染(SSR)、静态站点生成(SSG)以及增量静态生成(ISR)的示例代码为 example-render,服务端组件和客户端组件的示例代码为 example-rsc。模拟的 API 通过 https://reqres.in 调用。
在开始之前我们先创建示例项目如下:
example-render #
npx create-next-app@latest
✔ What is your project named? … example-render
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
example-rsc #
npx create-next-app@latest
✔ What is your project named? … example-rsc
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Client-side Rendering(CSR) #
概念 #
客户端渲染,即渲染在客户端执行。
执行过程 #
当用户访问一个网页时,服务器会发送一个相对空白的 HTML 文件,通常包含一个根 <div>
元素和一些必要的 JavaScript 文件。浏览器接收到 HTML 文件后,会下载并执行包含 React 应用逻辑的 JavaScript 文件。然后在浏览器中生成完整的页面内容。一旦页面加载并渲染完成,用户与页面的所有交互(如点击按钮、输入内容)都会通过 JavaScript 进行处理,React 会根据用户操作更新页面内容,而无需重新加载整个页面。
优点 #
由于在客户端处理,用户交互后的页面更新更快且更流畅。服务器只需提供静态的 HTML 和 JavaScript 文件,减少了服务器生成 HTML 的压力。
缺点 #
初始 JavaScript 文件较大,可能导致页面加载时间增加,尤其在网络较慢或设备性能较低的情况下。纯客户端渲染对 SEO 不利(目前已经好些)。
应用场景 #
单页应用(SPA, Single Page Application): SPA 通常加载一次页面,后续的页面切换和内容更新都在客户端完成,不需要每次都向服务器请求新页面。
例子 #
新建 csr.js 文件
import React, { useState, useEffect } from "react";
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://reqres.in/api/users/1");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setTimeout(() => {
setData(result.data);
}, 10000);
};
fetchData().catch((e) => {
// handle the error as needed
console.error("An error occurred while fetching the data: ", e);
});
}, []);
return (
<p className="p-10">{data ? `email : ${data.email}` : "Loading..."}</p>
);
}
解释:
可以看到,当访问页面后,会先显示 loading。当数据加载完毕会显示 data。 所有的操作包括网络请求都在客户端进行。
Server-side Rendering(SSR) #
概念 #
服务端渲染,即渲染在服务端执行。
执行过程 #
当用户请求一个网页时,这个请求会发送到服务器。服务器上运行的 React 应用会根据请求的数据和路径,生成完整的 HTML 内容。然后发送回客户端,这个 HTML 包含所有需要显示的内容,因此浏览器可以立即显示页面,而无需等待 JavaScript 的执行。页面加载后,客户端会加载 React 的 JavaScript 文件并将其应用于已经渲染的 HTML 元素上,这个过程称为“hydration”。完成之后页面变得可交互。
优点 #
更快的首屏加载时间,因为服务器直接返回完整的 HTML,用户可以更快地看到页面内容。搜索引擎爬虫可以直接抓取到完整的 HTML 内容,提高页面的可索引性,有利于 SEO。
缺点 #
服务器需要在每个请求上渲染页面内容,可能会增加服务器的负载,尤其是在高并发情况下。客户端接管(hydration)过程需要一些时间,因此在客户端完成 JavaScript 加载和执行之前,页面的交互可能会延迟。
应用场景 #
- 首屏加载时间至关重要的应用:需要确保用户能迅速看到页面内容。
- SEO 需求较高的网站:例如新闻站点等,需要确保页面内容对搜索引擎友好。
例子 #
import React, { useState, useEffect } from "react";
export default function Page({ data }) {
return <p className="p-10">email: {data.email}</p>;
}
export async function getServerSideProps() {
const res = await fetch(`https://reqres.in/api/users/1`);
const result = await res.json();
return { props: { data: result.data } };
}
服务端直接返回了渲染好的数据。
Static Site Generation(SSG) #
概念 #
静态站点生成,在构建时预先生成静态 HTML 页面,并在请求时直接提供这些页面的渲染方式。与客户端渲染(CSR)和服务器端渲染(SSR)不同,SSG 在构建过程中生成静态内容,而不是在用户请求时或客户端动态生成。
执行过程 #
在开发或部署阶段,会根据应用的路由和数据源预先生成所有页面的静态 HTML 文件。每个页面都会生成一个对应的 HTML 文件,连同 CSS、JavaScript 和其他静态资源一起被打包。这些文件不包含动态内容,所有页面内容在构建时已经确定。
优点 #
- 极快的加载速度:因为页面在构建时已经生成,用户请求时无需额外的处理,静态文件可以直接从 CDN 提供,速度非常快。
- 减少服务器负载:服务器无需动态生成页面,降低了服务器的计算资源消耗。
- 更好的安全性:因为不涉及服务器端的动态渲染,攻击面减少。
缺点 #
- 内容更新不及时:静态页面在构建时生成,因此如果数据频繁变化,需要重新构建站点才能反映最新内容。
- 不适合频繁更新的数据:对于需要实时更新的数据或交互性较强的应用,SSG 可能不合适。
应用场景 #
博客或文档站点:内容变化不频繁,且需要快速加载速度的站点,如技术博客、文档站点等。
例子 #
假设我们要创建一个用户详情页面,用户详情内容在构建时生成。我们可以使用Next.js 提供的 getStaticProps
方法,用于在构建时获取数据,并将这些数据作为 props 传递给页面组件。
在 ssg.js
中创建用户详情页面。
import React, { useState, useEffect } from "react";
export default function Page({ data }) {
return <p className="p-10">email: {data.email}</p>;
}
// This function gets called at build time
export async function getStaticProps() {
// Fetch data from external API
const res = await fetch(`https://reqres.in/api/users/1`);
const result = await res.json();
return { props: { data: result.data } };
}
例子2 #
在上面的例子中我们把 userid 固定为1,但在实际的场景中,我们需要通过 getStaticPaths
获取预渲染的路径。
在 pages/user/[id].js
中创建用户详情页面。
import React, { useState, useEffect } from "react";
// ssg example
export default function Page({ data }) {
return <p className="p-10">email: {data.email}</p>;
}
// 使用 getStaticProps 获取构建时的静态数据
export async function getStaticPaths() {
console.log("ssg example getStaticPaths");
const res = await fetch("https://reqres.in/api/users/");
const result = await res.json();
const users = result.data;
const paths = users.map((user) => ({
params: { id: String(user.id) },
}));
return { paths, fallback: false };
}
// 使用 getStaticPaths 为动态路由生成静态路径
export async function getStaticProps({ params }) {
console.log("ssg example getStaticProps ", params);
const res = await fetch(`https://reqres.in/api/users/${params.id}`);
const result = await res.json();
return { props: { data: result.data } };
}
getStaticPaths
:- 这个函数用于告诉 Next.js 在构建时需要预渲染哪些路径。它返回一个
paths
数组,数组中的每个对象代表一个静态页面的路径。
- 这个函数用于告诉 Next.js 在构建时需要预渲染哪些路径。它返回一个
getStaticProps
:- 这个函数会在构建时运行,它接受
params
参数,该参数包含动态路由的路径信息。在这个例子中,我们根据id
获取对应用户详情的数据,并将其作为 props 传递给组件。
- 这个函数会在构建时运行,它接受
然后执行 yarn build
yarn build
yarn run v1.22.18
$ next build
▲ Next.js 14.2.7
✓ Linting and checking validity of types
Creating an optimized production build ...
✓ Compiled successfully
Collecting page data ..ssg example getStaticPaths
✓ Collecting page data
Generating static pages (0/11) [= ]ssg example getStaticProps { id: '2' }
ssg example getStaticProps { id: '4' }
ssg example getStaticProps { id: '6' }
ssg example getStaticProps { id: '3' }
ssg example getStaticProps { id: '5' }
ssg example getStaticProps { id: '1' }
✓ Generating static pages (11/11)
✓ Collecting build traces
✓ Finalizing page optimization
Route (pages) Size First Load JS
┌ ○ / 5.32 kB 83.6 kB
├ └ css/628765f20b848f76.css 634 B
├ /_app 0 B 78.2 kB
├ ○ /404 182 B 78.4 kB
├ ƒ /api/hello 0 B 78.2 kB
├ ○ /csr 487 B 78.7 kB
├ ● /ssg (1190 ms) 310 B 78.6 kB
├ ƒ /ssr 309 B 78.6 kB
└ ● /user/[id] (7587 ms) 314 B 78.6 kB
├ /user/3 (1279 ms)
├ /user/5 (1273 ms)
├ /user/4 (1270 ms)
├ /user/6 (1270 ms)
├ /user/2 (1264 ms)
└ /user/1 (1231 ms)
+ First Load JS shared by all 81.6 kB
├ chunks/framework-ecc4130bc7a58a64.js 45.2 kB
├ chunks/main-51189aa2380b14da.js 32 kB
└ other shared chunks (total) 4.44 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses getStaticProps)
ƒ (Dynamic) server-rendered on demand
✨ Done in 18.02s.
详细说明 #
- 构建过程:
- 当你运行
next build
命令时,Next.js 会根据你的页面配置(如getStaticProps
和getStaticPaths
),在构建时生成静态 HTML 文件和相关的 JSON 数据文件。 - 这些文件会存储在
.next
文件夹中,具体的路径是.next/server/pages
。
- 当你运行
- 输出目录结构:
.next/server/pages
:这是生成的静态页面的存放位置。根据你的网站结构,Next.js 会在这个目录下生成与页面路径对应的 HTML 文件。.next/server/pages/[page].html
:每个页面的 HTML 文件。例如,如果你有一个about.js
页面文件,那么会生成一个about.html
文件。.next/server/pages/[page].json
:除了 HTML 文件,Next.js 还会生成对应的 JSON 文件,用于在客户端获取页面的静态数据。
- 部署到生产环境:
- 当你将应用程序部署到生产环境时,这些静态页面文件会被部署到你的服务器或 CDN 上,并通过这些文件来服务用户请求。
- 如果你使用 Vercel 或其他静态站点托管平台,这些静态页面将被自动部署,并存储在 CDN 上,从而在全球范围内快速响应用户请求。
静态页面在开发和构建过程中存储在 .next/server/pages
目录中,而在生产环境中,它们通常被部署到服务器或 CDN 上,供用户访问。
Incremental Static Regeneration(ISR) #
概念 #
增量静态再生,是一种在静态站点生成(SSG)的基础上,允许在构建后的特定条件下,逐步更新和再生成静态页面的技术。使得静态站点可以在保持快速加载速度的同时,能够灵活地更新内容。
执行过程 #
在应用构建阶段,部分或全部页面会预先生成静态 HTML 文件,并部署到服务器或内容分发网络(CDN)。当用户请求某个页面时,Next.js 会检查该页面是否已经过期。过期的定义是基于配置的 revalidate
时间(例如每 60 秒更新一次)。如果页面已经过期,Next.js 会在后台异步再生成这个页面。这意味着过期的页面仍然会立即返回给用户,而新的内容会在后台生成,并在生成完成后为下一次请求提供。这确保了页面内容的更新不会影响当前用户的体验。一旦后台生成的新页面完成,它会替换掉旧的静态页面。下次用户请求时,会直接获取到更新后的内容。
优点 #
- 快速加载:由于静态页面在构建时生成,用户请求时可以立即返回,加载速度极快。
- 灵活更新:无需重新部署整个站点,ISR 允许你在需要时自动更新部分页面,适用于内容变化较快的站点。
- SEO 友好:与 SSG 一样,ISR 提供的页面是完整的 HTML,对搜索引擎友好。
缺点 #
- 初次渲染延迟:如果页面需要再生成,而再生成时间较长,用户可能会在第一次请求时看到旧内容。
- 复杂性增加:相比于纯静态站点,ISR 增加了一定的复杂性,需要考虑页面缓存的有效期和再生成策略。
应用场景 #
- 内容变化频繁的站点:内容更新频繁但不需要实时更新。
- 组合 SSR 和 SSG 的场景:需要平衡快速加载和内容更新的应用,可以使用 ISR 来获取两者的优点。
例子 #
在上面例子的基础上,如果你的内容是定期更新的,可以通过 revalidate
属性设置增量静态生成(ISR, Incremental Static Regeneration),使页面在指定时间间隔后自动重新生成。
export async function getStaticProps({ params }) {
console.log("ssg example getStaticProps ", params);
const res = await fetch(`https://reqres.in/api/users/${params.id}`);
const result = await res.json();
return { props: { data: result.data }, revalidate: 60 };
}
通过这种方式,可以实现构建时生成静态页面,并根据需要在运行时定期更新。
以上就是基于 Pages Router 介绍的 CSR、SSR、SSG、ISR 但是在 App Router 中除 ISR 外已经没有了,取而代之的是关于 Server Component 和 Client Component 的介绍。
例如在 App Router 下调用 getServerSideProps
会直接报错
export async function getServerSideProps() {
const res = await fetch(`https://reqres.in/api/users/1`);
const result = await res.json();
return { props: { data: result.data } };
}
export default function Page({ data }) {
return <p className="p-10">email: {data.email}</p>;
}
上面代码会报错:
Error:
× "getServerSideProps" is not supported in app/. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching
接下来我们就看下什么是 Server Component 和 Client Component。
React Server Component(RSC) #
概念 #
React 团队引入的一种新概念,旨在提高 React 应用的性能和开发体验。它允许开发者在服务器端渲染 React 组件,而不必将这些组件的代码发送到客户端。
执行过程 #
服务端组件(RSC)的渲染服务端进行,React 将服务端组件(RSC)渲染为一种特殊的数据格式,称为 RSC Payload。Next.js 将 RSC Payload 和客户端组件 JavaScript 指令渲染成 HTML。在浏览器里立即显示生成的 HTML(初始显示非交互的 HTML)。RSC Payload 协调客户端与服务端组件树,并更新 DOM(RSC Payload 在服务端组件与客户端组件之间起到了桥梁作用。服务端组件是无状态的,它只负责生成静态的 HTML。但是,页面中往往不仅有服务端组件,还包含需要交互的客户端组件。为了使这些客户端组件能够理解和处理服务端组件的上下文,RSC Payload 传递了服务端渲染的元数据、组件树结构等)。最后通过客户端组件 JavaScript 指令水合客户端组件,使其可交互。(如果页面中仅有服务端组件,则直接显示)
How are Server Components rendered? #
On the server, Next.js uses React’s APIs to orchestrate rendering. The rendering work is split into chunks。
Each chunk is rendered in two steps:
- React renders Server Components into a special data format called the React Server Component Payload (RSC Payload).
- Next.js uses the RSC Payload and Client Component JavaScript instructions to render HTML on the server.
Then, on the client:
- The HTML is used to immediately show a fast non-interactive preview of the route - this is for the initial page load only.
- The React Server Components Payload is used to reconcile the Client and Server Component trees, and update the DOM.
- The JavaScript instructions are used to hydrate Client Components and make the application interactive.
https://nextjs.org/docs/app/building-your-application/rendering/server-components
优点 #
- 减少客户端负担:由于服务器组件不包含 JavaScript,客户端的 JavaScript 体积减少,页面加载速度更快。例如组件中使用一个大型库。 如果在客户端执行该组件,就意味着要将整个库发送到浏览器。 而使用服务器组件,只需静态 HTML 输出,无需向浏览器发送任何 JavaScript。 服务器组件是真正的静态组件,而且去除了整个水合步骤。
- 提高性能:通过在服务器端处理复杂的逻辑和数据获取,客户端可以只专注于 UI 的渲染,提升性能。
缺点 #
- 开发复杂度增加:需要在服务器组件和客户端组件之间进行明确的区分,开发者需要理解哪些逻辑应该放在哪一端。
- 生态系统的适应:现有的工具和库可能需要调整或更新才能与 RSC 配合使用。
应用场景 #
- 内容驱动的页面:页面内容较多且主要用于展示,使用 RSC 可以减少客户端的渲染压力。
- 服务器端数据处理:需要在服务器端获取和处理大量数据的场景,可以使用 RSC 将数据处理逻辑放在服务器端。
- 性能敏感的应用:希望在客户端减少 JavaScript 体积,提升首次加载速度的应用。
例子 #
export default async function Home() {
const res = await fetch("https://reqres.in/api/users/");
const result = await res.json();
console.log(result.data);
return (
<div className="flex flex-col p-10">
{result.data.map(({ id, email }) => {
return <div key={id}>{email}</div>;
})}
</div>
);
}
React Client Component(RCC) #
概念 #
浏览器端运行并渲染的 React 组件。客户端组件能够处理用户交互,并访问浏览器 API,如 localStorage 和 geolocation。
执行过程 #
你可能会认为客户端组件只在客户端呈现,但 Next.js 会在服务器上呈现客户端组件,生成初始 HTML。 因此,浏览器可以立即开始呈现它们,然后再执行水合。
优点 #
- 丰富的交互性:客户端组件可以充分利用 React 的状态和生命周期管理,提供丰富的用户交互体验。
- 浏览器 API 支持:客户端组件能够访问和利用浏览器提供的各种 API,实现更丰富的功能。
缺点 #
增加 JavaScript 体积:客户端组件需要在浏览器端执行,需要 下载JavaScript 文件和解析。
应用场景 #
需要交互的部分:例如表单、按钮、切换开关等,这些元素需要响应用户操作并更新页面内容。需要访问浏览器 API:例如处理文件上传、访问本地存储等功能,只能在客户端组件中实现。
在 Next.js 中,所有组件默认都是服务器组件。 这就是为什么我们需要使用 “use client “明确定义客户端组件。
Server-side Rendering(SSR)与React Server Components 区别 #
- SSR 侧重于在服务器上预渲染整个页面以便快速提供完整内容,但客户端仍需要加载和执行大量的 JavaScript 代码。所有的页面内容,包括 HTML 和 JavaScript 代码,都会发送给客户端。整个 React 应用在服务器端预渲染,然后在客户端加载并接管。
- RSC 则是一个更细粒度的优化工具,它允许开发者在服务器端渲染特定的组件,服务器组件可以直接使用服务器资源和数据库,减少了客户端的 JavaScript 体积,从而减少客户端的负担。
一种是渲染模式(SSR),一种是组件类型(RSC)
Client-side Rendering(CSR)与 React Client Components 区别 #
- CSR 是一种整体渲染策略,用户请求页面时,服务器只返回一个基础的 HTML 文件和相应的 JavaScript 文件。页面的内容和结构都是通过浏览器执行 JavaScript 来动态生成的。它涵盖了整个应用的渲染流程,包括初始加载、数据获取和交互处理。CSR 是构建 SPA 的常见方式,在这些应用中,页面之间的导航不需要重新加载整个页面,所有内容的加载和更新都通过 JavaScript 完成。
- React Client Components 是在客户端运行的具体 React 组件。它们处理与用户交互、动态更新、状态管理等相关的任务。React Client Components 是 React 应用中负责渲染和交互的基本构建块。
一种是渲染模式(CSR),一种是组件类型(RCC)
参考 #
https://nextjs.org/docs/pages/building-your-application/data-fetching
https://nextjs.org/docs/app/building-your-application/data-fetching
https://nextjs.org/docs/pages/building-your-application/rendering/client-side-rendering
https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering
https://nextjs.org/docs/app/building-your-application/rendering/server-components
https://www.smashingmagazine.com/2024/05/forensics-react-server-components/
https://weijunext.com/article/nextjs-v13-server-side-and-client-side-components-best-practices
https://edspencer.net/2024/7/1/decoding-react-server-component-payloads
https://vercel.com/blog/understanding-react-server-components