现代渲染架构:流式 / 岛屿 / RSC

本节目标

从「整页水合」到「渐进增强」:传统 SSR 是服务端产出整页 HTML,浏览器再一次性「水合」整个应用——首屏快,但「可交互时间」晚(要等整包 JS 下载+执行完)。

流式渲染(Streaming SSR):服务端把 HTML 分块推送,被 <Suspense> 包住的慢组件先出占位,数据到了再流式补上。

// React:Suspense 让慢组件不阻塞首屏
<Suspense fallback={<Spinner />}>
  <SlowCommentSection />
</Suspense>

价值:TTFB 不变,但关键内容更早到达,对应 LCP/INP 优化。

岛屿架构(Islands Architecture):页面大部分是静态 HTML,只有若干「岛屿」(交互组件)独立水合,其余不加载 JS。

┌──────────────────────────────────────┐
│  静态头部(无 JS)                      │
│  ┌─────────┐        ┌─────────┐      │
│  │ 计数器   │        │ 搜索框   │      │
│  │ (岛屿A)  │        │ (岛屿B)  │      │
│  └─────────┘        └─────────┘      │
│  静态内容(无 JS)                      │
└──────────────────────────────────────┘
JS 只发「岛屿」部分 → 体积小、水合快(代表:Astro)

React Server Components(RSC):组件默认在服务端运行(不进客户端 bundle),只有 'use client' 的才进浏览器。

下面这段 Node 代码模拟流式 SSR 与岛屿水合的核心思想:

// 运行环境:Node.js 14+
// 步骤:node brw-l17.js
// 它会「分块输出」HTML(模拟流式),并演示「只有岛屿组件才发送水合脚本」

function streamHTML() {
  // ① 先立刻发送静态外壳 + 首屏关键内容(不等慢数据)
  process.stdout.write('[流式-第1块] <header>静态头部(无JS)</header>\n')
  process.stdout.write('[流式-第2块] <main>文章正文(直出, 立即可见)</main>\n')

  // ② 慢数据到了,再补发这块内容(不阻塞前面的首屏)
  setTimeout(() => {
    process.stdout.write('[流式-第3块] <section>评论区数据(晚到也补上)</section>\n')
    hydrateIslands()
    console.log('(观察:前两块立刻出现,第3块 0.5s 后才补上,符合流式+岛屿思想)')
  }, 500)

  // ③ 只给「岛屿」发水合脚本,静态部分不发
  function hydrateIslands() {
    const islands = ['计数器组件', '搜索框组件']
    console.log('[水合] 仅为岛屿发送 JS:', islands.join('、'))
    console.log('[水合] 静态头部/正文不发送 JS → 体积最小')
  }
}
streamHTML()

RSC 对模块化的启示:

① 每个模块要声明自己的「运行环境」(server / client / universal)
② 环境边界错乱(client 引了 server 代码)→ 报错或包体积失控
③ 这些架构本质在解决「代码如何被分发到不同执行环境」

工程选型:流式渲染注意水合匹配;岛屿架构适合内容站;RSC 适合数据密集应用——看交互密度与首屏诉求。

名词解释

流式渲染(Streaming SSR):服务端把 HTML 分块、边生成边发送给浏览器的渲染方式。方法:res.write(chunk) 多次发送、配合 <Suspense> 让慢组件延迟补发。它的价值是「首屏关键内容不等慢数据」,从而优化 LCP/INP。对比「整页渲染完才发」的传统 SSR,流式更早给到用户可见内容。

岛屿架构(Islands Architecture):一种页面组织思路——默认页面是「静态 HTML」,只把少数交互组件作为「岛屿」单独加载 JS 并水合,其余部分零 JS。方法:<Island component={...} /> 标记需要水合的岛屿,框架只对岛屿发脚本。代表框架 Astro。核心价值:JS 体积小、首屏快、可交互早。

水合(Hydration):浏览器拿到服务端生成的 HTML 后,再「绑定」JS 事件、恢复组件状态的过 程,让静态页面变「可交互」。方法:框架在客户端重跑组件逻辑并 attach 事件监听。痛点:整页水合要下载整包 JS;岛屿/RSC 的思路就是「只水合必要的部分」来缩减它。

课后练习

练习 1:一个以「内容展示为主、只有搜索框和主题切换两个交互」的官网,用整页 SSR + 整页水合,和用岛屿架构,哪个更合适?为什么?

答案:岛屿架构更合适。官网绝大多数是静态内容,整页水合会把「整包 JS」下载执行一遍,只为两个交互组件,纯属浪费;岛屿架构只给搜索框、主题切换这两个「岛屿」发 JS,其余静态 HTML 零 JS,体积最小、首屏最快、可交互也早。这正是岛屿架构(如 Astro)的主场。

练习 2:RSC 里,如果一个标了 'use client' 的组件,不小心 import 了一个只在服务端能跑的 db.query() 模块,会发生什么?

答案:会出问题——因为 'use client' 组件会被打包进客户端 bundle 发到浏览器,而 db.query() 依赖服务端环境(数据库连接等),浏览器里根本没有,要么打包报错(导入了不该进客户端的模块),要么运行时报「找不到连接/未定义」。这正说明 RSC 的核心纪律:明确模块的「运行环境边界」,server 代码不能泄漏进 client bundle。

总结

这一节是整门课的「视野升华」。前面 16 节你一直在钻「浏览器内部怎么跑」,到这里我把镜头拉远,看「今天的前端架构在解决什么更大的问题」——本质上都是**「如何把代码合理地分发到不同执行环境,并最小化用户要下载和执行的 JS」**。流式渲染让「首屏不等慢数据」,岛屿架构让「静态内容零 JS」,RSC 让「服务端逻辑不进客户端包」。它们的共同敌人是「整包水合带来的体积与延迟」。我的观点是:理解这些架构,不是为了背名词,而是理解「运行环境的边界」正在成为新的模块化维度——就像当年从全局脚本进化到模块系统一样,未来你会越来越多地思考「这段代码该在服务端跑、还是在客户端跑」。这门浏览器原理课,正是这一切的地基。