各位同仁,下午好!
今天,我们将深入探讨 React Server Components(RSC)中一个既强大又复杂的话题:缓存机制。特别是,我们将聚焦于一个许多开发者都曾感到困惑的现象——为什么在 RSC 应用中,执行一次浏览器刷新(Browser Refresh)与执行一次客户端导航(Client Navigation)时,数据和组件的缓存表现会截然不同?
RSC 的出现,旨在融合服务器端渲染(SSR)的性能优势与客户端渲染(CSR)的交互性,将数据获取和部分渲染逻辑推向服务器端,从而减少客户端 Bundle 大小,提升首次加载速度。然而,要充分发挥 RSC 的潜力,我们必须深刻理解其背后的多层缓存策略。正是这些策略,决定了我们的应用在不同交互场景下的响应速度和资源消耗。
RSC 核心机制回顾:为何缓存如此关键?
在深入缓存细节之前,让我们快速回顾一下 RSC 的基本工作原理及其对性能的意义。
什么是 RSC?
React Server Components 是在服务器上渲染的 React 组件。它们不包含任何客户端 JavaScript 代码,因此不会被打包进客户端 Bundle。R RSC 的输出不是 HTML,而是一种特殊的、可序列化的 React 树结构,它包含了组件的 Props、Children 以及对客户端组件的引用。
RSC 如何工作?
当一个请求到达服务器时,React 会在服务器上渲染 RSC 树。这个渲染过程可以包含异步操作,例如直接在组件内部进行数据查询(await fetch(...))。渲染完成后,序列化的 RSC 负载(Payload)会被流式传输到客户端。客户端的 React 运行时会接收这个负载,并将其与现有的 DOM 结构进行协调(Reconciliation),最终呈现出完整的 UI。
RSC 中的关键概念:
- 服务器组件(Server Components):默认类型,不带
"use client"指令,在服务器上渲染。 - 客户端组件(Client Components):带
"use client"指令,在服务器上预渲染(SSR),但其交互逻辑最终在客户端水合(Hydration)。 - 服务器动作(Server Actions):通过
"use server"指令定义的函数,可以在客户端组件中调用,但在服务器上执行,通常用于数据修改。
为什么缓存对 RSC 如此关键?
想象一下,每次用户访问页面或进行导航时,我们都重新从数据库或外部 API 获取所有数据,重新执行所有昂贵的计算。这无疑会带来巨大的性能开销,包括:
- 增加服务器负载: 频繁的数据获取和计算会耗尽服务器资源。
- 延长响应时间: 数据获取通常是网络瓶颈,导致用户等待时间变长。
- 增加网络传输: 尽管 RSC 已经优化了客户端 Bundle,但如果每次都传输全新的数据,网络负载依然不小。
因此,有效地利用缓存是 RSC 性能优化的核心。
RSC 环境下的多层缓存机制
在现代的 React/Next.js 应用(尤其是 App Router 架构)中,RSC 的缓存是一个多层协同工作的复杂系统。理解这些层次是理解其行为差异的基础。
-
HTTP 缓存 (HTTP Caching)
- 作用对象: 主要是静态资源(JS/CSS/图片文件),也包括初始的 HTML 文档。
- 机制: 依赖于标准的 HTTP 头,如
Cache-Control、ETag、Last-Modified。浏览器、CDN 和反向代理服务器会根据这些头来决定是否缓存资源,以及何时重新验证。 - 与 RSC 的关系: 初始的 HTML 文档(其中可能包含了 RSC 首次渲染的输出)可以被 HTTP 缓存。然而,后续的 RSC 数据负载通常是动态生成的,其
Cache-Control头通常设置为no-cache或private,因此很少直接被浏览器 HTTP 缓存。
-
React 缓存 (React’s In-Memory Cache)
- API:
React.cache() - 作用对象: 纯函数或异步函数的调用结果。
- 机制:
React.cache是 React 18 引入的一个 API,用于在单个 React 渲染周期内对函数调用结果进行记忆化(Memoization)。这意味着,如果在一个服务器请求中,同一个React.cache包装的函数被多次调用且参数相同,它只会执行一次实际的逻辑,后续调用会直接返回缓存结果。 - 生命周期: 每个独立的服务器请求都会有一个全新的
React.cache上下文。 一旦请求完成,这个缓存就会被销毁。 -
代码示例:
import React from 'react'; // 这个函数会在单个请求中,对于相同的 productId 只执行一次 const getProductDetails = React.cache(async (productId) => { console.log(`[Server] Fetching product details for ${productId}...`); const res = await fetch(`https://api.example.com/products/${productId}`); return res.json(); }); async function ProductDescription({ productId }) { const product = await getProductDetails(productId); return <p>{product.description}</p>; } async function ProductReviews({ productId }) { const product = await getProductDetails(productId); // 这里会命中缓存 return ( <div> <h2>Reviews for {product.name}</h2> {/* ... render reviews ... */} </div> ); } export default async function ProductPage({ params }) { const productId = params.id; const product = await getProductDetails(productId); // 第一次调用 return ( <div> <h1>{product.name}</h1> <ProductDescription productId={productId} /> <ProductReviews productId={productId} /> </div> ); }在上述例子中,
getProductDetails(productId)在同一个请求内被调用了三次,但服务器日志只会输出一次[Server] Fetching product details for ${productId}...。
- API:
-
框架级数据缓存 (Next.js Data Cache)
- 作用对象: 通过 Next.js 扩展的
fetch()API 进行的数据请求。 - 机制: 在 Next.js App Router 中,
fetch()被自动扩展,具备了强大的缓存能力。它可以在服务器端缓存数据,并且通过配置revalidate选项(时间或no-store)或tags来控制缓存的生命周期和粒度。revalidate: number:数据将在指定秒数后变为陈旧(stale),下次请求时会尝试重新获取。revalidate: 0/no-store:不缓存数据,每次请求都重新获取。tags: string[]:为缓存项打上标签,可以通过revalidateTag()函数精确地失效缓存。
- 生命周期: 这种缓存是持久的,它存在于服务器的生命周期内(或者在生产环境中,可以配置为跨越多个 serverless 实例的共享缓存,例如 Vercel 平台会使用 Redis)。 这意味着,即使是不同的用户请求,如果命中同一个服务器进程,且请求的数据在缓存中且未过期,就可能直接从缓存中返回。
-
代码示例:
// app/products/page.js import Link from 'next/link'; import { revalidatePath, revalidateTag } from 'next/cache'; async function getProducts() { console.log('[Server] Fetching all products from API...'); const res = await fetch('https://api.example.com/products', { next: { revalidate: 60, tags: ['products'] }, // 缓存60秒,并打上'products'标签 }); if (!res.ok) throw new Error('Failed to fetch products'); return res.json(); } export default async function ProductsPage() { const products = await getProducts(); return ( <div> <h1>Our Products</h1> <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} </ul> <Link href="/dashboard">Go to Dashboard</Link> <form action={async () => { 'use server'; console.log('[Server Action] Invalidating products cache...'); revalidateTag('products'); // 手动失效 'products' 标签的缓存 // 或者 revalidatePath('/products'); // 失效特定路径的缓存 }}> <button type="submit">Refresh Product Data (Force Revalidate)</button> </form> </div> ); }这个
getProducts函数在服务器端获取数据。如果 60 秒内有多个请求(无论是来自同一个用户的客户端导航,还是来自不同用户的请求),它很可能只实际调用一次外部 API。
- 作用对象: 通过 Next.js 扩展的
-
客户端数据缓存 (Client-Side Data Caching Libraries)
- 作用对象: 由客户端组件通过
useEffect或事件处理器触发的数据获取。 - 机制: 像 SWR、React Query (TanStack Query)、Apollo Client 等库,在客户端管理数据状态。它们提供了强大的缓存、去重、后台刷新、乐观更新等功能。
- 生命周期: 存在于客户端的内存中,只要浏览器标签页不关闭或不刷新,缓存就会一直存在。 它们通常与客户端组件的生命周期绑定,并在组件卸载时清理相关观察者。
- 与 RSC 的关系: 虽然 RSC 主要在服务器端运行,但客户端组件仍然可以自行获取数据。这些库的缓存行为与 RSC 的服务器端缓存是正交的,它们共同构成了完整的数据管理策略。
-
代码示例 (SWR):
// app/components/ClientProductList.js (Marked with "use client") 'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then((res) => res.json()); export default function ClientProductList() { const { data, error, isLoading } = useSWR('/api/products-client', fetcher); if (error) return <div>Failed to load products</div>; if (isLoading) return <div>Loading client products...</div>; return ( <div> <h2>Products (Client-Fetched)</h2> <ul> {data.map(p => <li key={p.id}>{p.name}</li>)} </ul> </div> ); }这个组件在客户端获取数据。如果用户在不同的客户端页面之间导航,只要这个组件被渲染,SWR 会在内存中缓存数据,并根据其配置(如
stale-while-revalidate)决定何时重新验证。
- 作用对象: 由客户端组件通过
核心差异解析:浏览器刷新 vs. 客户端导航
现在,让我们来详细剖析为什么这两种交互方式会导致不同的缓存表现。
场景一:浏览器刷新 (Browser Refresh / Full Page Reload)
当用户在地址栏输入 URL、点击刷新按钮、或通过外部链接直接访问页面时,会发生浏览器刷新。
发生过程:
- 完整 HTTP 请求: 浏览器发起一个全新的、完整的 HTTP GET 请求,目标是整个 HTML 文档。这个请求没有上下文,它对服务器来说,就如同一个首次访问的用户。
- 客户端状态销毁: 浏览器会清除当前标签页的所有客户端 JavaScript 状态、DOM 结构以及内存中的所有数据(包括 SWR/React Query 等库的缓存)。
- 服务器端全新处理:
- 服务器接收到请求,将其视为一个全新的、独立的请求。
- React 会从头开始渲染整个 RSC 树。
React.cache():由于是新请求,React.cache()的上下文是全新的,它不会继承上一个请求的缓存。但对于当前请求内部的重复调用,它仍然会进行去重。- Next.js Data Cache (
fetch()扩展):服务器会检查fetch()调用的缓存。- 如果缓存项存在且未过期(根据
revalidate时间),数据将从 Next.js 的服务器端缓存中返回,而不会再次调用外部 API。 - 如果缓存项已过期,或者被
revalidateTag/revalidatePath显式失效,Next.js 会重新发起外部 API 请求,并更新缓存。 - 如果
fetch()使用了no-store或revalidate: 0,则每次都会重新获取。
- 如果缓存项存在且未过期(根据
- HTML 及 RSC Payload 传输: 服务器将完整的 HTML 文档(包含初始 RSC 渲染结果)发送给浏览器,并可能通过流式传输后续的 RSC Payload。
- 客户端水合: 客户端的 React 运行时接收 HTML,进行水合,并渲染客户端组件。任何客户端数据缓存库(SWR, React Query)都会从空状态开始。
缓存表现总结:
- HTTP 缓存: 可能会命中针对 HTML 文档的 HTTP 缓存(如果配置允许)。
React.cache(): 每次刷新都是一个新请求,React.cache()重新初始化,只在当前请求内部有效。- Next.js Data Cache (
fetch()扩展): 会生效,但会严格遵守revalidate时间。 如果缓存过期,将重新获取数据。 - 客户端数据缓存: 完全失效,从零开始。
场景二:客户端导航 (Client Navigation / Soft Navigation)
当用户点击 next/link 组件、使用 router.push() 或 router.replace() 导航到应用内部的另一个页面时,会发生客户端导航。
发生过程:
- 拦截并发送特殊请求: 浏览器不会发起一个全新的完整 HTTP 请求。Next.js 客户端路由器会拦截导航事件,并通过
fetch或XHR向服务器发送一个特殊的请求,请求的不是完整的 HTML,而是目标路由的 RSC Payload。这个请求会包含当前页面的上下文信息。 - 客户端状态保持: 客户端的 JavaScript 状态、DOM 结构以及内存中的数据(包括 SWR/React Query 等库的缓存)会尽力保留。React 会在新旧组件树之间进行高效的协调。
- 服务器端处理:
- 服务器接收到这个特殊的 RSC Payload 请求。
- React 渲染目标路由的 RSC 树。
React.cache():同样,React.cache()的上下文是针对这个特定导航请求的,它会在该请求内部进行去重。- Next.js Data Cache (
fetch()扩展):这是关键的区别所在。 Next.js 的服务器端数据缓存会检查fetch()调用的缓存。- 由于服务器进程可能持续运行,即使在
revalidate时间内,对于同一个fetch调用(相同的 URL 和选项),它很可能会直接命中服务器端的持久化缓存。 这意味着,如果用户在/products和/dashboard之间来回导航,并且两个页面都依赖于同一个fetch调用获取数据(或不同的fetch调用但其缓存都在有效期内),那么这些数据可能不需要再次从外部 API 获取。 - 只有当缓存项真正过期,或者通过
revalidateTag/revalidatePath显式失效后,Next.js 才会重新发起外部 API 请求。
- 由于服务器进程可能持续运行,即使在
- RSC Payload 传输: 服务器将目标路由的 RSC Payload 流式传输到客户端。
- 客户端协调: 客户端的 React 运行时接收到 Payload,并与现有 UI 进行协调,更新页面内容。客户端数据缓存库(SWR, React Query)会继续保持其状态,并可能根据其策略进行数据重新验证。
缓存表现总结:
- HTTP 缓存: 不涉及完整的 HTML 请求,因此不直接应用。
React.cache(): 每次导航都是一个新请求,React.cache()重新初始化,只在当前请求内部有效。- Next.js Data Cache (
fetch()扩展): 高效且持久。 在revalidate时间内,即使是跨多个客户端导航请求,只要服务器进程还在运行且缓存未过期,就能直接命中缓存,显著减少外部 API 调用。 - 客户端数据缓存: 保持活跃。 SWR/React Query 等库会维持其缓存状态,并可能在后台重新验证数据。
差异总结表格
| 特性/场景 | 浏览器刷新 (Browser Refresh) | 客户端导航 (Client Navigation) |
|---|---|---|
| 请求类型 | 全新的完整 HTTP GET 请求(获取 HTML) | 特殊的 XHR/Fetch 请求(获取 RSC Payload) |
| 客户端状态 | 完全销毁 | 尽力保留(DOM、JS 状态、客户端缓存) |
| 服务器端请求上下文 | 全新的请求上下文 | 全新的请求上下文 |
React.cache() |
每次刷新都重置,只在当前请求内部去重 | 每次导航都重置,只在当前请求内部去重 |
Next.js fetch() Data Cache |
严格遵守 revalidate 时间。 过期则重新获取。 |
更高效地利用持久化缓存。 在 revalidate 时间内,即使跨多个导航,也很可能直接命中服务器缓存。 |
| HTTP 缓存 | 针对 HTML 文档可能生效 | 不直接涉及 |
| 客户端数据缓存 | 完全失效(SWR/React Query 等),从零开始 | 保持活跃,继续管理其缓存数据 |
| 主要影响 | 确保获取最新数据,但可能导致外部 API 调用增多 | 显著减少外部 API 调用,提升导航速度和用户体验 |
实践中的代码演示
让我们通过具体的代码示例来进一步体会这些差异。
我们将模拟一个简单的 Next.js 应用,其中包含产品列表页和仪表盘页,两者都从模拟 API 获取数据。
// 模拟一个简单的外部 API 服务,用于观察服务器端的实际请求
// 可以在项目根目录创建一个 `api.js` 文件来运行
// node api.js
const express = require('express');
const app = express();
const port = 3001;
let productsData = [
{ id: 1, name: 'Laptop Pro', price: 1200, description: 'Powerful laptop' },
{ id: 2, name: 'Mouse Wireless', price: 25, description: 'Ergonomic mouse' },
];
let dashboardData = {
stats: { users: 1500, sales: 25000 },
message: 'Welcome to your dashboard!'
};
app.get('/products', (req, res) => {
console.log(`[Mock API] GET /products - ${new Date().toLocaleTimeString()}`);
res.json(productsData);
});
app.get('/dashboard', (req, res) => {
console.log(`[Mock API] GET /dashboard - ${new Date().toLocaleTimeString()}`);
res.json(dashboardData);
});
app.post('/products', express.json(), (req, res) => {
console.log(`[Mock API] POST /products - ${new Date().toLocaleTimeString()}`);
const newProduct = { id: productsData.length + 1, ...req.body };
productsData.push(newProduct);
res.status(201).json(newProduct);
});
app.listen(port, () => {
console.log(`Mock API listening at http://localhost:${port}`);
});
请先启动这个模拟 API:node api.js
接下来是 Next.js 应用代码(假设是 App Router 结构):
app/page.js (产品列表页 – RSC)
// app/page.js
import Link from 'next/link';
import { revalidatePath, revalidateTag } from 'next/cache';
// 这个函数会利用 Next.js 的 fetch 缓存
async function getProducts() {
console.log('[RSC] Fetching all products from API...'); // 服务器端日志
const res = await fetch('http://localhost:3001/products', {
next: {
revalidate: 10, // 缓存10秒
tags: ['products'],
},
});
if (!res.ok) throw new Error('Failed to fetch products');
return res.json();
}
export default async function HomePage() {
const products = await getProducts();
// 服务器动作:添加产品并重新验证缓存
async function addProduct(formData) {
'use server';
const name = formData.get('name');
const price = parseFloat(formData.get('price'));
const description = formData.get('description');
console.log('[Server Action] Adding new product...');
await fetch('http://localhost:3001/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price, description }),
});
revalidateTag('products'); // 失效 'products' 标签的缓存
revalidatePath('/'); // 失效当前路径的缓存
// 这里通常会重定向,但为了演示,我们只刷新数据
}
return (
<div>
<h1>Product List (RSC)</h1>
<p>Data fetched at: {new Date().toLocaleTimeString()}</p>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} - ${p.price}
</li>
))}
</ul>
<nav style={{ marginTop: '20px' }}>
<Link href="/dashboard" style={{ marginRight: '15px' }}>
Go to Dashboard (Client Nav)
</Link>
<a href="/about" style={{ marginRight: '15px' }}>
Go to About (Full Refresh)
</a>
</nav>
<div style={{ marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}>
<h2>Add New Product (Server Action)</h2>
<form action={addProduct}>
<input type="text" name="name" placeholder="Product Name" required style={{ display: 'block', marginBottom: '10px' }} />
<input type="number" name="price" placeholder="Price" required style={{ display: 'block', marginBottom: '10px' }} />
<textarea name="description" placeholder="Description" style={{ display: 'block', marginBottom: '10px' }}></textarea>
<button type="submit">Add Product & Revalidate</button>
</form>
</div>
</div>
);
}
app/dashboard/page.js (仪表盘页 – RSC)
// app/dashboard/page.js
import Link from 'next/link';
async function getDashboardData() {
console.log('[RSC] Fetching dashboard data...'); // 服务器端日志
const res = await fetch('http://localhost:3001/dashboard', {
next: {
revalidate: 30, // 缓存30秒
tags: ['dashboard'],
},
});
if (!res.ok) throw new Error('Failed to fetch dashboard data');
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<div>
<h1>Dashboard (RSC)</h1>
<p>Data fetched at: {new Date().toLocaleTimeString()}</p>
<p>Users: {data.stats.users}</p>
<p>Sales: {data.stats.sales}</p>
<p>{data.message}</p>
<Link href="/">Back to Home (Client Nav)</Link>
</div>
);
}
实验步骤和观察:
- 启动 Next.js 应用:
npm run dev或yarn dev - 打开浏览器: 访问
http://localhost:3000/
观察 1:初始加载 / (Home Page)
- Next.js Dev Server Console: 你会看到
[RSC] Fetching all products from API...。 - Mock API Console: 你会看到
[Mock API] GET /products - ...。- 这说明服务器端确实进行了数据获取。
观察 2:浏览器刷新 / (Home Page)
- 立即刷新: 再次刷新
http://localhost:3000/。- Next.js Dev Server Console: 你可能不会立即看到
[RSC] Fetching all products from API...。 - Mock API Console: 你不会看到
[Mock API] GET /products - ...。 - 结论: Next.js Data Cache 命中了。因为上次获取的数据在 10 秒的
revalidate期限内。
- Next.js Dev Server Console: 你可能不会立即看到
- 等待 10 秒后刷新: 等待 10 秒以上,再次刷新
http://localhost:3000/。- Next.js Dev Server Console: 你会再次看到
[RSC] Fetching all products from API...。 - Mock API Console: 你会再次看到
[Mock API] GET /products - ...。 - 结论: Next.js Data Cache 已过期,数据被重新获取。
- Next.js Dev Server Console: 你会再次看到
观察 3:客户端导航 / -> /dashboard -> /
- 从 Home 页点击 "Go to Dashboard (Client Nav)"。
- Next.js Dev Server Console: 你会看到
[RSC] Fetching dashboard data...。 - Mock API Console: 你会看到
[Mock API] GET /dashboard - ...。 - 结论: Dashboard 数据被获取。
- Next.js Dev Server Console: 你会看到
- 从 Dashboard 页点击 "Back to Home (Client Nav)"。
- Next.js Dev Server Console: 你不会看到
[RSC] Fetching all products from API...(假设距离上次获取产品数据未超过 10 秒)。 - Mock API Console: 你不会看到
[Mock API] GET /products - ...。 - 结论: Next.js Data Cache 命中了。即使是从
/dashboard导航回/,Next.js 的服务器端进程仍然保留了/products的缓存数据,并且它尚未过期。
- Next.js Dev Server Console: 你不会看到
观察 4:使用 Server Action 强制重新验证
- 在 Home 页,不刷新浏览器,点击 "Add Product & Revalidate" 按钮。输入一些产品信息。
- 表单提交后:
- Next.js Dev Server Console: 你会看到
[Server Action] Adding new product...和[RSC] Fetching all products from API...。 - Mock API Console: 你会看到
[Mock API] POST /products - ...和[Mock API] GET /products - ...。 - 结论:
revalidateTag('products')和revalidatePath('/')生效了。尽管产品数据可能尚未过期,但我们通过 Server Action 显式地使其失效,导致 Next.js 重新获取了最新的产品列表,页面也随之更新。这个操作对后续的浏览器刷新和客户端导航都有效。
- Next.js Dev Server Console: 你会看到
通过这些实验,我们可以清晰地看到:
- 浏览器刷新总是要求服务器检查 Next.js Data Cache 是否过期,过期则重新获取。
- 客户端导航则能更充分地利用 Next.js Data Cache 的持久性,只要缓存未过期,即使是不同的导航请求,也可能直接命中缓存,避免重复的外部 API 调用。
revalidateTag和revalidatePath提供了强大的控制力,允许我们根据业务逻辑精确地失效缓存,确保数据新鲜度。
最佳实践与设计考量
理解 RSC 的缓存机制后,我们可以采取以下最佳实践来构建高性能、高效率的应用:
-
充分利用
fetch()扩展:- 在 RSC 中,优先使用 Next.js 扩展的
fetch()进行数据获取。 - 为
fetch()配置合适的revalidate时间,平衡数据新鲜度和性能。对于不经常变化的数据,可以设置较长的revalidate时间;对于实时性要求高的数据,设置为0或no-store。 - 使用
tags为缓存项打上标签,以便通过revalidateTag()进行精细化控制。
- 在 RSC 中,优先使用 Next.js 扩展的
-
合理使用
React.cache():- 主要用于在单个请求内去重昂贵的计算或数据获取。
- 适用于组件树中多个子组件需要相同数据时,避免重复调用同一个函数。
- 不要依赖
React.cache()进行跨请求的持久化缓存。
-
策略性地使用
revalidatePath和revalidateTag:- 在数据发生变更(如通过 Server Actions)后,显式地使相关缓存失效。
revalidateTag()提供了更细粒度的控制,推荐在可能的情况下使用标签。
-
理解
no-store的含义:- 当数据需要绝对实时、不容许任何缓存时,使用
fetch(..., { cache: 'no-store' })。 - 这会跳过所有 Next.js 的服务器端缓存,每次都从源头获取最新数据。
- 当数据需要绝对实时、不容许任何缓存时,使用
-
结合客户端数据缓存库:
- 对于需要高度交互性、实时更新或复杂本地状态管理的客户端组件,仍然可以引入 SWR、React Query 等库。
- 它们与 RSC 的服务器端缓存是互补的,共同提供无缝的用户体验。
-
监控和分析:
- 在开发和生产环境中,观察服务器日志以了解数据何时被实际获取,何时命中缓存。
- 利用 Next.js 提供的性能分析工具来识别缓存瓶颈。
结语
RSC 的缓存机制是其性能优势的基石,也是其复杂性的体现。通过深入理解 HTTP 缓存、React 自身的 React.cache、Next.js 强大的 fetch() 数据缓存,以及客户端数据缓存库的协同作用,我们才能驾驭这股力量。浏览器刷新和客户端导航之间的缓存表现差异,本质上反映了它们对服务器请求生命周期和客户端状态保留的不同处理方式。掌握这些细微之处,并将其融入我们的设计决策中,将使我们能够构建出既高效又响应迅速的现代 Web 应用。感谢各位。