跳到主要内容

渲染

服务器组件 (server component)

服务器组件

  • UI 渲染在服务端完成;
  • next 默认使用服务器组件;

优势

  • 提高服务端数据获取的性能;
  • 敏感数据和逻辑保留在服务器,提高安全性;
  • 具有缓存机制,提高性能;
  • 提高首屏加载性能;
  • SEO 优化;
  • 基于 stream 的数据传输;

渲染流程

  • 服务端;
    • react 渲染 server component 为特定的数据格式 (RSC payload);
    • next 将 RSC payload 和 client component 中的 js 渲染为 html;
  • 客户端;
    • 立刻接受预先渲染的无交互逻辑的 HTML,仅限于首屏加载;
    • 接受 RSC payload,构建组件树,并更新 DOM;
    • next 水合 client component 和 js 代码,使其具有交互性;

服务器渲染策略

静态渲染 (默认)

  • 构建时或者数据重新验证时渲染;
  • 渲染结果可被缓存至 CDN 中;
  • 适用于博客,首页等不具有用户个性化数据的页面;

动态渲染

动态渲染
  • 用户请求时渲染;
  • 适合于数据只有在请求时才会确定的场景,例如 cookie,url params;
切换至动态渲染
  • 使用动态函数;
  • 使用未被缓存的数据;
Dynamic FunctionsDataRoute
NoCachedStatically Rendered
YesCachedDynamically Rendered
NoNot CachedDynamically Rendered
YesNot CachedDynamically Rendered
常见动态函数
  • cookies();
  • headers();
  • useSearchParams();
  • searchParams 属性;

steaming (默认)

  • UI 分割成若干 chunk,逐块渲染 UI;
  • 提高首页加载性能;
  • next 默认使用;

客户端组件 (client component)

客户端组件

  • 编写具有交互性的 UI;
  • 请求时构建 UI;
  • next 可选;
  • 使用 use client 指令;
"use client";
import { useState } from "react";

export default function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

优势

  • 交互性:可使用 useState,useEffect 等 react API;
  • 允许使用浏览器 API;

传染性

  • 一旦将某组件设置为客户端组件;
  • 其所有子组件和被该组件导入的模块均视为客户端组件;

渲染流程

  • 获取数据;
  • 移除只能在服务端运行的代码;
    • 使用 server-only 库;
  • 导入第三方库并执行 react 客户端独有 api;
import "server-only";

export async function getData() {
const res = await fetch("https://external-service.com/data", {
headers: {
authorization: process.env.API_KEY,
},
});

return res.json();
}

第三方库标记问题

  • 若第三方库中使用 useState 等 API,但未标注为客户端组件;
  • next 视其为服务器组件从而报错;
  • 需要重导出第三方库并标记为客户端组件;
"use client";
import { Carousel } from "acme-carousel";
export default Carousel;

服务器组件和客户端组件

渲染顺序
  • next 首先渲染所有的服务器组件,包括嵌套在客户端组件中的服务器组件;
  • 然后 react 使用 RSC Payload 组织客户端组件和服务器组件;
导入组件
  • 由于服务器组件先于客户端组件渲染;
    • 客户端组件无法导入服务器组件;
    • 服务器组件可以导入客户端组件和服务器组件;
  • 但服务器组件可作为属性传递至客户端组件;
"use client";
// You cannot import a Server Component into a Client Component.
import ServerComponent from "./Server-Component";

export default function ClientComponent({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
);
}
"use client";
import { useState } from "react";

export default function ClientComponent({
children,
}: {
children: React.ReactNode;
}) {
const [count, setCount] = useState(0);

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
);
}

运行时

不同运行时
  • node 运行时;
    • 默认运行时;
    • 可使用 node API 以及其所有生态;
  • edge 运行时;
    • 基于 Web API;
FeatureNodeServerlessEdge
Cold Boot/NormalLow
HTTP StreamingYesYesYes
IOAllAllfetch
Scalability/HighHighest
SecurityNormalHighHigh
LatencyNormalLowLowest
npm PackagesAllAllA smaller subset
Static RenderingYesYesNo
Dynamic RenderingYesYesYes
Data Revalidation w/ fetchYesYesYes
应用场景
  • edge;
    • 动态请求;
    • 代码量小且简单的函数;
  • node:大而全;
  • serverless;
    • 动态请求;
    • 相较于 edge 更为复杂的函数;
自定义运行时
  • 定义在 layout 或 page 文件中;
    • layout:layout 层级下的所有子节点;
    • page:对应节点;
export const runtime = "edge"; // 'nodejs' (default) | 'edge'

最佳实践

客户端组件

客户端层级
  • 将客户端组件作为组件树的叶节点
  • 避免客户端组件的传染性;
  • 减少客户端 js bundle 的大小;
数据获取
  • 尽量从服务器组件,使用属性传递数据;