渲染
服务器组件 (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 Functions | Data | Route |
---|---|---|
No | Cached | Statically Rendered |
Yes | Cached | Dynamically Rendered |
No | Not Cached | Dynamically Rendered |
Yes | Not Cached | Dynamically 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;
Feature | Node | Serverless | Edge |
---|---|---|---|
Cold Boot | / | Normal | Low |
HTTP Streaming | Yes | Yes | Yes |
IO | All | All | fetch |
Scalability | / | High | Highest |
Security | Normal | High | High |
Latency | Normal | Low | Lowest |
npm Packages | All | All | A smaller subset |
Static Rendering | Yes | Yes | No |
Dynamic Rendering | Yes | Yes | Yes |
Data Revalidation w/ fetch | Yes | Yes | Yes |
应用场景
- edge;
- 动态请求;
- 代码量小且简单的函数;
- node:大而全;
- serverless;
- 动态请求;
- 相较于 edge 更为复杂的函数;
自定义运行时
- 定义在 layout 或 page 文件中;
- layout:layout 层级下的所有子节点;
- page:对应节点;
export const runtime = "edge"; // 'nodejs' (default) | 'edge'
最佳实践
客户端组件
客户端层级
- 将客户端组件作为组件树的叶节点
- 避免客户端组件的传染性;
- 减少客户端 js bundle 的大小;
数据获取
- 尽量从服务器组件,使用属性传递数据;