大多数团队以为上了 RSC 就能降 bundle size,结果 INP 反而涨了——这件事今天把 React Server Components 生产落地的真实账全算清楚了

大多数团队以为上了 RSC 就能降 bundle size,结果 INP 反而涨了——这件事今天把 React Server Components 生产落地的真实账全算清楚了

React Server Components 喊了两年,2026 年终于有一批团队把它跑上了生产。结果怎么样?

有人 bundle 掉了 40%,有人 INP 反而更差。有人删了三千行 React Query,有人缓存配错了被 CDN 返给了陌生人。

今天把这件事的生产数据全部拆开来看。

上了 RSC 之后,bundle 真的掉了多少

先说数字,不然没人信。

一个真实的电商列表页迁移数据(Devya Solutions):

  • 客户端 JS 减少 42%
  • TTFB 降低 180ms(开启流式渲染后)
  • INP 反而上升了 0.08s(某个拆分不当的页面)

另一份生产数据(诊所管理系统):

  • 客户端 bundle 减少约 40%
  • heavy 库(date-fns、markdown 解析、数据转换工具)全部留在服务端
  • 只有交互组件(表单、弹窗、下拉菜单)才下发 JavaScript

这是结构性收益,不是优化出来的。RSC 的原理是:不出交互的组件,浏览器永远收不到它的 JS 包。这不是压缩,是根本不发送。

流式渲染让 UX 体感变快,但前提是配对了

RSC 的第二个收益来源是流式渲染(Streaming)。

传统 SSR:等所有数据查完,再返回完整 HTML
RSC + Suspense:页面 shell 立刻返回,各区块数据查完就流进来

function DashboardPage() {
  return (
    <>
      <Suspense fallback={<ShellSkeleton />}>
        <UserPanel />
      </Suspense>
      <Suspense fallback={<MetricsSkeleton />}>
        <MetricsPanel />
      </Suspense>
    </>
  );
}

Shell 先出来,用户立刻看到页面框架。慢的部分自己加载,用户感知到的等待消失了。

但这里有个坑:某些代理会 buffer 流式响应,导致流式变成阻塞式。本地开发环境永远正常,上了生产才发现 TTFB 根本没改善。要在生产环境里实际验证,不是本地。

缓存是最多人踩的坑

RSC 的服务端输出默认不缓存,每次请求重新渲染。大多数团队加上缓存层的时候,会在某个层级出错。

四个缓存层级,踩坑顺序:

第一层:请求内 memoization

import { cache } from "react";

const getUser = cache(async (id) => {
  return db.users.findUnique({ where: { id } });
});

同一次请求里三个组件都要用户信息,只查一次数据库。安全,因为只存活于单次请求。

第二层:进程内存缓存

Map 或 LRU,跨请求共享。问题是 serverless 环境里函数实例重启会清空缓存,命中率飘忽不定。长时间运行的 Node 服务才适合。

第三层:Redis 等共享缓存

import { unstable_cache } from "next/cache";

const getProduct = unstable_cache(
  async (id) => db.products.findUnique({ where: { id } }),
  ["product"],
  { revalidate: 3600, tags: ["product"] }
);

这个层才真正跨实例、跨进程。但危险区也在这里:用户相关的数据、cookie 拼在 key 里的数据,上错了这个层就是安全事故。

第四层:CDN 缓存

这一层配错最常见。团队把含用户私人数据的页面设成了 public,CDN 把 A 用户的购物车页面返给了 B 用户。

底线:

  • 私有页面:Cache-Control: private, no-store
  • 公开页面:Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=600

INP 回归:从哪里来的

有团队上了 RSC 之后 INP 不降反升。根因不是 RSC 本身,是 Client Component 的拆分粒度。

Client Component 树hydrate 晚了,交互响应就慢了。

解法:把交互岛屿往上提一层,让服务端数据通过 props 传进去,而不是让整个子树都变成 Client Component。

// ❌ 错误:整个卡片都是 Client Component
'use client';
function ProductCard({ product }) {
  const [liked, setLiked] = useState(false);
  return <div>...</div>;
}

// ✅ 正确:只把交互部分拆成 Client Component
async function ProductCard({ product }) {
  // 服务端组件负责数据获取和渲染结构
  const reviews = await db.reviews.findMany({ productId: product.id });
  return (
    <div>
      <h3>{product.name}</h3>
      <LikeButton initialLiked={false} productId={product.id} />
    </div>
  );
}

'use client';
function LikeButton({ initialLiked, productId }) {
  const [liked, setLiked] = useState(initialLiked);
  return <button onClick={() => setLiked(!liked)}>...</button>;
}

use client 是打包入口点。随便引用一个图表库,就可能往每个页面里塞 80KB。

迁移策略:不要 big-bang

生产复盘一致的建议:不要一次性全量切换 RSC。

正确路径:

  1. 选一条路由,public 数据、bundle 最胖的页面先来
  2. 配好监控基线:TTFB、LCP、INP、缓存命中率、源头调用次数
  3. 灰度:1% → 10% → 50%
  4. 自动回滚:任何指标回归就停

这条路由稳定了,再迁下一条。逐条推进,出问题影响范围可控。

什么时候不该上 RSC

不是所有场景都适合:

  • 后端接口是高频轮询(chatty backend):每次请求都重新渲染,缓存收益为零
  • 页面含实时个性化定价:缓存层搞不定,最终还是全量动态
  • 团队对 React 渲染模型不熟:半吊子迁移比不迁移更差

硬规则:use client 的边界就是打包入口点。把这个边界往外推的代价,每个前端工程师都应该清楚。

结论

React Server Components 在 2026 年已经是生产级选项,不是实验品。但它的收益来自三个前提:客户端 JS 真的能降下来、缓存层配对了、流式渲染在生产环境验证过了。

任何一个前提没满足,收益就是零,甚至负收益。

选一条胖 bundle 路由先跑,配好监控基线,逐条推进。RSC 不是一个开关,是一条长征。

下一步:打开 Chrome DevTools Network,看一下当前页面首屏的 JS bundle 大小。那就是你的起点。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 14 阅读