技术篇:CSR、SSR、SSG、ISR 与客户端、服务端组件

·1450 字·7 分钟
独立开发 Next.js
Next.js实战教程 - 系列文章
Part 7: 当前文章

本节内容是这个系列字数最长,概念最多的一节。请大家在阅读时多一些耐心。本节将基于 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

image.png

✔ 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

image <em>1</em>.png

✔ 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>
  );
}

image <em>2</em>.png

image <em>3</em>.png

解释:

可以看到,当访问页面后,会先显示 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 } };
}

服务端直接返回了渲染好的数据。

image <em>4</em>.png

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 } };
}

image <em>5</em>.png

例子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 数组,数组中的每个对象代表一个静态页面的路径。
  • 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.

image <em>6</em>.png

详细说明 #

  1. 构建过程
    • 当你运行 next build 命令时,Next.js 会根据你的页面配置(如 getStaticPropsgetStaticPaths),在构建时生成静态 HTML 文件和相关的 JSON 数据文件。
    • 这些文件会存储在 .next 文件夹中,具体的路径是 .next/server/pages
  2. 输出目录结构
    • .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 文件,用于在客户端获取页面的静态数据。
  3. 部署到生产环境
    • 当你将应用程序部署到生产环境时,这些静态页面文件会被部署到你的服务器或 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 的介绍。

image <em>7</em>.png

image <em>8</em>.png

例如在 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:

  1. React renders Server Components into a special data format called the React Server Component Payload (RSC Payload).
  2. Next.js uses the RSC Payload and Client Component JavaScript instructions to render HTML on the server.

Then, on the client:

  1. The HTML is used to immediately show a fast non-interactive preview of the route - this is for the initial page load only.
  2. The React Server Components Payload is used to reconcile the Client and Server Component trees, and update the DOM.
  3. 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>
);
}

image <em>9</em>.png

image <em>10</em>.png

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

Next.js实战教程 - 系列文章
Part 7: 当前文章