想象你在网上订了一套精装房。开发商效率惊人,在工厂里把墙、地板、沙发、床全部装好,整房打包运到你面前——这就是服务端渲染(SSR):浏览器收到一段完整 HTML,立刻就能看到文字、图片和排版,完全不用等装修队现场施工。可住了几天你会郁闷地发现:这套房没通电。开关按不动、门铃不响,屋内的一切都不会回应你的操作。于是你请来电工师傅接线,把每个开关、门铃连到对应房间——这个“通电”过程,就是 Hydration(注水)。本篇文章围绕“精装房交付”展开:先看开发商如何把房子又快又好地交到手里(SSR),再看电工如何安全接线(Hydration 与事件绑定),然后处理“图纸和现场对不上”的纠纷(mismatch 警告),最后学会两招高级玩法——选择性注水和流式交付,让大户人家也能“先亮客厅、逐层入住”。

岗位关联

掌握 SSR 与 Hydration,是你在真实项目中做首屏性能优化、排查交互失效问题的硬核底牌。无论是 Next.js 全栈开发还是 React 架构设计,这套知识都会直接决定线上体验与稳定性。

目录
  1. 可视化演示
  2. 一、SSR渲染原理:开发商直接交付精装房
  3. 二、Hydration注水机制:电工上门接通电路
  4. 三、事件绑定与交互:一根总线点亮全屋
  5. 四、Mismatch告警处理:图纸与现场对不上
  6. 五、Selective注水优化:优先点亮客厅
  7. 六、Streaming流式渲染:装好一层交付一层
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

通过SSR渲染流水线、Hydration注水与事件绑定、流式渲染与选择性注水三个模式,展示服务端渲染到客户端可交互的完整过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

SSR服务端渲染流程 服务器执行renderToString生成完整HTML字符串用户无需等待JS即可看到内容 SSR提升首屏FCP和SEO但增加服务器负载需平衡渲染开销 Server: renderToString(Component) → HTML String → Client SSR核心价值是快速首屏和SEO友好

一、SSR渲染原理:开发商直接交付精装房

传统网页开发(CSR,客户端渲染)像“先交空地,再等装修队进场”。浏览器先收到一个近乎空白的 HTML 文件,就像一块只有地基的空地;随后 JavaScript 装修队扛着成百上千个组件上门,现场浇筑墙体、刷漆、摆家具,折腾完你才看到内容。如果装修队路上堵车(网络慢、JS 包大),这段时间你只能对着白屏干等。

服务端渲染(SSR)的思路是换个交付方式:开发商(服务器)在远程工厂里就把整套精装房装好——React 组件在服务器上用 renderToString 输出为一段完整的 HTML 字符串,随响应直接“快递”到浏览器。浏览器解析 HTML 立刻看到文字、图片与样式,像拿到一套拎包入住、无需现场施工的现房。

首屏时间的关键差异在“第一次有内容的绘制点”。CSR 必须经历 JS 下载、解析执行、再创建 DOM 的漫长链路;SSR 只需等服务器返回 HTML,浏览器逐字节解析即可呈现。SEO 方面,爬虫直接读 HTML 就能提取内容,不必执行 JS。用公式对比两种方案的首屏耗时:

可是住进精装房的你很快会发现:家里虽好看,却死气沉沉——按开关灯不亮、点按钮没反应。因为真正的“电器”(JavaScript 事件逻辑)还没接进来。这便引出 Hydration 注水。

TFCPSSR=TTTFB+Tparse,TFCPCSR=TTTFB+TJSdownload+TJSexec+Trender\displaystyle T_{FCP}^{SSR} = T_{TTFB} + T_{parse}, \qquad T_{FCP}^{CSR} = T_{TTFB} + T_{JS}^{download} + T_{JS}^{exec} + T_{render}
通俗地说:精装房到手就能住(SSR 只欠网络时间),毛坯房要等装修队回来施工完才能住(CSR 要等 JS 下载完、执行完、渲染完)。
  1. 首次内容绘制 T_FCPFirst Contentful Paint,指用户第一次看到页面内容的时刻。SSR 公式把它拆成“网络等待 + 解析时间”两段。
    TFCPSSRT_{FCP}^{SSR}
  2. 首字节时间 T_TTFB从发起请求到收到 HTML 第一个字节的耗时,对应精装房从下单到货车开到门口的时间。
    TTTFBT_{TTFB}
  3. JS 下载与执行 T_JSCSR 独有成本,浏览器要先下载并执行整包 JS 才能创建内容,相当于等装修队运材料、现场施工。
    TJSdownload+TJSexecT_{JS}^{download} + T_{JS}^{exec}
  4. 解析与渲染 T_renderHTML 到达后浏览器逐字节解析构建 DOM 树并绘制的收尾过程。
    Tparse+TrenderT_{parse} + T_{render}

二、Hydration注水机制:电工上门接通电路

SSR 交付的静态 HTML 是一座“硬件齐全但没通电”的精装房。要让房间响应人的动作——按下开关灯亮、点击按钮弹窗,必须有电工(客户端 JS)进场:接线、装开关。这一步在 React 里叫 Hydration,字面意思是“给干涸的骨架注入水分,让它活过来”。

React 18 用 hydrateRoot(domNode, <App />) 启动注水。它并不是把服务器给的 HTML 拆掉重建,而是做两件事:第一,在客户端再跑一遍组件逻辑,生成一棵虚拟 DOM(拿到电工图纸);第二,拿着图纸和真实 DOM 逐一对账(reconcile),在匹配的节点上挂事件监听器。

对账建立在“同构渲染”前提上——服务器和客户端用同一套组件代码渲染,结构应当一致。注水成本不能忽略:它等于又执行了一次组件渲染,外加遍历全部 DOM 节点的调和。页面越大,注水越慢;注水完成前,页面虽然“看得见”但点击按钮没反应。量化一下:

注水完成,整屋通电,点击、输入、滚动全部“活”了。但这里有个烧脑问题:电工接线靠图纸——如果图纸和实际装修对不上呢?这正是第四节的 mismatch。不过在此之前,先看电工怎么用最少的线搞定全屋开关。

Thydrate=∑i=1Ntreconcile(i),Ctotal=Crenderserver+Crenderclient+Cattach\displaystyle T_{hydrate} = \sum_{i=1}^{N} t_{reconcile}^{(i)}, \qquad C_{total} = C_{render}^{server} + C_{render}^{client} + C_{attach}
通俗地说:注水是“对账 + 接线”,每一盏灯、每一个开关都要核对图纸再连上线。灯越多(DOM 节点越多),电工耗时越长。
  1. 单节点调和 t_reconcile(i)注水时对每个 DOM 节点执行“客户端 VDOM 与真实 DOM 是否匹配”的检查。
    treconcile(i)t_{reconcile}^{(i)}
  2. 总和 Σ把 N 个节点的调和时间加总,得到总注水耗时——节点越多,电工要核对的插座越多。
    ∑i=1Ntreconcile(i)\sum_{i=1}^{N} t_{reconcile}^{(i)}
  3. 服务端渲染成本 C_server服务器把组件跑一遍生成 HTML 字符串的开销(工厂预制成本)。
    CrenderserverC_{render}^{server}
  4. 客户端渲染成本 C_client注水时客户端再跑一遍组件生成 VDOM 的开销(现场复勘成本)。
    CrenderclientC_{render}^{client}
  5. 事件挂载成本 C_attach把监听器绑定到对应 DOM 节点的布线开销。
    CattachC_{attach}

三、事件绑定与交互:一根总线点亮全屋

如果每个开关都单独拉一根线到配电箱,60 个房间就要 60 根线。聪明电工用一根“总线”(事件委托),把所有开关串到同一条线上;按下任何一个开关,信号顺着总线传到 root 配电箱,配电箱根据房间编号分发给对应房间的逻辑。React 就是这么干的。

React 17 之前把事件监听器挂在 document 上;React 18 改挂到根容器(root container)。无论页面有多少个可交互节点,程序只在 root 上绑定一次监听器,而不是每个节点绑一个。事件发生时,通过冒泡(bubble)从目标节点一路传到 root,React 再根据 Fiber 树找到对应组件,触发其事件处理器。

这种设计让注水时的事件挂载成本从“与节点数成正比”降到常数级别。不过事件真正触发时,冒泡路径上有多少层祖先节点,就要经过多少层分发,这部分成本是动态的。公式如下:

总线方案让注水“布线”极快,但总线也好、分户线也罢,都依赖一个前提:电工图纸和实际房间严格对应。一旦对不上,接错线的风险就出现了——这正是 hydration mismatch 警告的由来。

Cattachdelegate=O(1),Cattachnaive=O(N),Cdispatch=O(dbubble)\displaystyle C_{attach}^{delegate} = O(1), \qquad C_{attach}^{naive} = O(N), \qquad C_{dispatch} = O(d_{bubble})
通俗地说:一根总线(root 一次绑定)就能服务全屋开关;按开关后信号沿总线冒泡,楼层越深(DOM 层次越深),信号跑的路越长。
  1. O(1) 委托挂载事件委托后只在 root 上绑一次监听,成本与节点数无关,是常量。
    Cattachdelegate=O(1)C_{attach}^{delegate} = O(1)
  2. O(N) 朴素挂载若每个节点单独绑监听,成本随节点数线性增长,房间多了线就会很乱。
    Cattachnaive=O(N)C_{attach}^{naive} = O(N)
  3. O(d) 冒泡分发事件触发时从目标节点冒泡到 root,d 是树的深度;楼层越深信号跑得越久。
    Cdispatch=O(dbubble)C_{dispatch} = O(d_{bubble})

四、Mismatch告警处理:图纸与现场对不上

水电工进场第一件事是拿图纸核对现场。如果图纸画着“沙发靠左墙”,现场却是“沙发靠右墙”,专业电工绝不会硬接,而是立刻标红报警。React 的 hydration 过程也是如此:客户端注水时会把 VDOM(图纸)与真实 DOM(现场)逐棵比对,一旦发现同一位置的标签、文本或属性不一致,就抛出 hydration mismatch 警告。

为什么会不一致?最常见的原因是“服务端和客户端渲染时,看到的变量值不同”。比如组件里直接渲染 new Date().toLocaleString(),服务器在东八区输出“8:00“,浏览器在本地时区输出”6:00”;或者组件用了 Math.random()、typeof window 这类客户端专属 API。数据源不同(服务器从 MySQL 读,客户端从 localStorage 读)也会导致结构漂移。

修复思路有三条。第一,把“不确定的值”延后到客户端再定:放在 useEffect 或事件回调里更新,让首次客户端渲染与服务端一致。第二,保证两端用同一个数据源:把服务器数据序列化内嵌到 HTML(如 script 标签里的 window.__DATA__)。第三,对确实无法避免的细节差异(比如某些自动生成的 id),用 suppressHydrationWarning 让 React 跳过该节点的对账——相当于电工在图纸上盖一个“以现场为准”的章。用公式描述警告的触发条件:

处理完对账,精装房可以正式点亮。但整屋同时通电有个问题:如果这套房有 200 个房间,电工一根一根接完要耗很久,住户在客厅干等着。聪明的物业想了个办法——选择性注水,先通客厅。

∃ i: fi(Dserver)≠fi(Dclient)  ⟹  MismatchWarning\displaystyle \exists\, i:\ f_i(D_{server}) \neq f_i(D_{client}) \;\Longrightarrow\; \text{MismatchWarning}
通俗地说:服务器和客户端各拿一份数据“装修同一间房”,只要有一个节点的装修结果不一样,电工就会报警。
  1. 节点渲染函数 f_i渲染树中第 i 个节点的输出,可以看作数据 D 到 HTML 片段的一个函数。
    fi(D)f_i(D)
  2. 两端数据 D_server 与 D_client服务器与客户端各自持有的数据;两者不一致是 mismatch 的根源。
    Dserver≠DclientD_{server} \neq D_{client}
  3. 存在量词 ∃只要存在任意一个 i 使两端渲染结果不同,警告就会冒出——注意是“任一”而非“全部”。
    ∃ i:fi(Dserver)≠fi(Dclient)\exists\, i: f_i(D_{server}) \neq f_i(D_{client})
  4. 逻辑蕴含 ⇒推导出 mismatch 警告必然出现,意味着该节点的事件监听可能挂错位置。
    ⇒MismatchWarning\Rightarrow \text{MismatchWarning}

五、Selective注水优化:优先点亮客厅

想象一栋 60 层的公寓,物业宣布“整栋楼同一晚通电”。电工逐层挨家接线,顶层住户苦苦等了一夜。更聪明的做法是“分层分户送电”:先给今晚就要入住的 1 楼客厅通电,其他楼层贴着封条,等住户真正进来(或靠近、或按门铃)再补电。这就是 Selective Hydration(选择性注水),React 18 的核心特性之一。

Selective Hydration 以 Suspense 边界为“分户单元”。页面首帧中,只有处于屏幕内、属于关键路径的组件会立即注水;其余被 <Suspense> 包裹的区域,React 会记录其“未注水”状态。若用户与未注水区域交互(比如点击了折叠面板),React 拦截该事件,立刻对该边界执行补注水,然后重放事件——用户感觉几乎无延迟。

底层依赖 React Scheduler 的优先级调度。注水不是按 DOM 顺序遍历,而是按优先级队列调度:紧急交互 > 关键 UI > 可见区域 > 屏幕外低优。收益是首屏交互时间从“全量注水完成”提前到“关键部分注水完成”。对比公式如下:

按需通电解决了“等全屋”问题,但还有一个老毛病没解决:开发商坚持整栋楼装修完才交房,首层住户还是要等顶层刷完漆。要彻底缩短“看到房子”的时间,得让开发商改流水线——这就是 Streaming SSR。

Tinteractiveselective=Tcritical+τschedule,Tinteractivefull=Tcritical+Tlazy\displaystyle T_{interactive}^{selective} = T_{critical} + \tau_{schedule}, \qquad T_{interactive}^{full} = T_{critical} + T_{lazy}
通俗地说:全屋通电要等“客厅+卧室+厨房”全部接完(T_full);选择性注水只需等客厅接完(T_critical),剩下房间随用随接。
  1. 关键注水时间 T_critical首屏高优先级组件完成注水所需的时间(客厅通电时间)。
    TcriticalT_{critical}
  2. 调度开销 τ调度器为注水任务排队分配的时间,一般极小可忽略。
    τschedule\tau_{schedule}
  3. 惰性注水时间 T_lazy非关键组件若一起注水所需的时间(卧室和厨房的接线时间)。
    TlazyT_{lazy}
  4. 全量 vs 选择式全量注水要等 T_critical 加 T_lazy,选择式只需等 T_critical,交互时间显著提前。
    Tinteractivefull=Tcritical+TlazyT_{interactive}^{full} = T_{critical} + T_{lazy}

六、Streaming流式渲染:装好一层交付一层

传统 SSR 像开发商“整栋交付”:一栋 30 层的楼必须全部施工完毕才通知你入住,浏览器作为住户,要眼巴巴等到整个 HTML 响应传完才能开始解析。Streaming SSR 则像“边建边交”:开发商(服务器)把 HTML 流式输出,先交已装修好的一层楼(HTML 头部的 shell 和首屏内容),你立刻开始布置;二三层还在刷漆,装好一层就顺着管道送一层。

在 React 中通过 renderToPipeableStream(Node 服务端)实现。页面被拆成“外壳 + 若干 Suspense 边界”。外壳(导航栏、页面框架、首屏主体)不需要等任何异步数据;服务器撞上 <Suspense> 边界时,先输出占位符(如 loading 图标),继续发送已完成部分的 HTML;该边界数据就绪后,再将真正的 HTML 片段以“补丁”形式继续流出。浏览器收到多少就渲染多少。

好处是双重的:首字节(TTFB)大幅提前,因为不再需要等全部组件完成;外壳就绪后可以立刻开始注水,内层边界到达后按 Selective Hydration 策略各自注水,实现“边到边湿”。关键公式:

至此,整条链路串起来了:SSR 交精装房(静态 HTML 秒到)→ Hydration 通电(对账+挂事件)→ 事件总线支撑交互 → mismatch 保证图纸与现场一致 → Selective Hydration 让客厅先亮 → Streaming SSR 让一层层交付成为可能。开发商、电工、物业三层协作,最终让住户“快看到、快入住、快互动”。

TFCPstreaming=TfirstChunk+ε,Tcomplete=∑k=1mSkRk\displaystyle T_{FCP}^{streaming} = T_{firstChunk} + \varepsilon, \qquad T_{complete} = \sum_{k=1}^{m} \frac{S_k}{R_k}
通俗地说:第一层油漆一干就交钥匙(首块到达即可见);整栋楼全部交付的时间 = 每层装修耗时之和,每层耗时 = 该层工作量除以施工速度。
  1. 首块到达时间 T_firstChunk第一个 HTML 分块到达浏览器的时间,流式渲染中首屏可见由它决定。
    TfirstChunkT_{firstChunk}
  2. 解析小量 ε浏览器解析并绘制第一个分块所需的时间,通常极小。
    ε\varepsilon
  3. 分块大小 S_k第 k 个 HTML 分块的字节数,相当于第 k 层楼的工程量。
    SkS_k
  4. 传输速率 R_k该分块在链路上的传输速度,相当于施工队的速度。
    RkR_k
  5. 累加求和 Σ所有分块“大小/速率”之和就是全部 HTML 送完的时间;减小分块或提高带宽都能提速。
    ∑k=1mSkRk\sum_{k=1}^{m} \frac{S_k}{R_k}

AI 应用场景

场景一:电商大促商品详情页提速

阶段:前端渲染链路优化(SSR/Hydration 性能调优阶段)

解决的问题:大促时商品页首屏 TTFB 慢、用户等白屏流失。传统 CSR 首屏要等全套 JS 加载完才开始渲染,购物车、推荐位等次要模块又拖慢注水。用 Streaming SSR 让外壳和价格信息优先流出,购物车与推荐区走 Selective Hydration,用户先看到价格、能点“加入购物车”,次要模块随后“偷偷”注水。

具体案例:某电商改造双 11 详情页后,TTFB 从 1.8s 降到 0.6s,LCP 从 3.2s 降到 1.4s,转化率提升约 9%。

场景二:技术文档站兼顾 SEO 与评论区交互

阶段:内容渲染与 SEO 适配阶段

解决的问题:技术文档、博客必须让爬虫直接读到正文,同时保留评论区、目录折叠等交互能力。常见坑是页面顶部显示“更新时间”时直接渲染 new Date(),导致服务端与客户端时区不同而触发 mismatch。方案:服务端流入统一 ISO 时间字符串,客户端在 useEffect 中再格式化为本地时间;评论区用 Suspense 包裹,滚动接近时才按需注水。

具体案例:Next.js 官方文档站就是这种模式,SEO 评分与交互体验兼得,页面滚动到评论区附近才完成该区域注水。

场景三:低代码报表大屏首屏优化

阶段:数据可视化应用的交付与交互优化阶段

解决的问题:大屏页面由几十个图表组件组成,一次性全量注水会让首屏交互迟迟不可用。利用 Selective Hydration:首屏表格高优先注水,图表组件包在 Suspense 里,滚动到可视区才注水;配合 Streaming SSR 让“看到外壳”的时间大幅提前,数据绑定完成一块就点亮一块。

具体案例:某低代码平台改造后,大屏首屏可交互时间从 4.5s 降到 2.1s,图表区域无感懒注水,CPU 空闲时长显著提升。

Python 演示

python95 行
"""用 Python 模拟 SSR 与 Hydration 的核心流程"""import time  class Node:    """极简虚拟 DOM 节点"""     def __init__(self, tag, text="", key=""):        self.tag = tag        self.text = text        self.key = key        self.children = []     def to_html(self):        inner = "".join(c.to_html() for c in self.children)        return "<%s>%s%s</%s>" % (self.tag, self.text, inner, self.tag)  def ssr_render(root):    """服务端渲染 组件树转静态 HTML 字符串"""    return root.to_html()  def hydrate(server_html, client_tree):    """注水 核对服务端 HTML 与客户端树并统计事件绑定"""    mismatches = []    listeners = 0     def walk(node, path, parent_html):        nonlocal listeners        expect = "<%s>%s" % (node.tag, node.text)        if expect not in parent_html:            mismatches.append("/".join(path + [node.tag]))            return        if node.key == "button":            listeners += 1        for child in node.children:            walk(child, path + [node.tag], node.to_html())     walk(client_tree, [], server_html)    return mismatches, listeners  def selective_hydrate(client_tree, priority_keys):    """选择性注水 按优先级决定本次注水的节点"""    ready = []    lazy = []     def walk(node, path):        target = "/".join(path + [node.tag])        if node.key in priority_keys:            ready.append(target)        else:            lazy.append(target)        for child in node.children:            walk(child, path + [node.tag])     walk(client_tree, [])    return ready, lazy  def main():    page = Node("div")    page.children = [        Node("header", "商品详情页", key="shell"),        Node("main", "价格与优惠", key="price"),        Node("section", "评论区", key="comment"),    ]    page.children[1].children = [Node("button", "加入购物车", key="button")]     server_html = ssr_render(page)    print("== SSR 阶段 服务器生成的静态 HTML ==")    print(server_html)     server_html = server_html.replace(        "<section>评论区</section>", "<section>最新评论</section>"    )    mismatches, listeners = hydrate(server_html, page)    print("== Hydration 阶段 核对 DOM 并挂载事件 ==")    print("mismatch 数量:", len(mismatches), "| 绑定事件数:", listeners)    print("mismatch 位置:", mismatches)     ready, lazy = selective_hydrate(page, {"price", "button"})    print("== Selective Hydration 按需注水 ==")    print("本次注水:", ready)    print("暂缓注水:", lazy)     start = time.time()    for _ in range(1000):        hydrate(server_html, page)    print("注水 1000 次耗时:", round(time.time() - start, 4), "秒")  if __name__ == "__main__":    main()
目的:用一棵极简的组件树完整演示“SSR 生成 HTML → Hydration 对账挂事件 → Selective Hydration 按优先级注水”三个核心环节,并统计 mismatch 与事件绑定数。
  1. server_html = ssr_render(page)调用服务端渲染,得到整段静态 HTML,相当于开发商交付精装房。
  2. expect = "<%s>%s" % (node.tag, node.text)客户端按树节点生成“期望标记”,把它当作电工手里的对账图纸。
  3. if expect not in parent_html:图纸内容在服务端产物里找不到,说明前后端不一致,记录为一处 mismatch。
  4. if node.key == "button": listeners += 1只有带关键 key 的可交互节点才算“挂载事件”,用来统计注水工作量。
  5. def selective_hydrate(client_tree, priority_keys):选择性注水的入口:按优先级集合挑选本次要通电的节点。
  6. if node.key in priority_keys:key 命中高优先级集合的节点先注水,其余进入 lazy 暂缓列表,对应“先亮客厅”。
  7. server_html = server_html.replace(人为改一处服务端文本,模拟线上“两端数据不一致”,制造一处 mismatch 供演示。

常见误区

误区我以为做完 SSR,浏览器拿到完整 HTML 能直接显示内容,那就不用加载 JS 了,Hydration 是不是多余的?

正解SSR 只解决“看到内容”,不解决“可以交互”。没有注水,页面就像没通电的精装房——画面完美但按钮无效。浏览器仍要下载包含 Hydration 逻辑的 JS bundle,用它给 DOM 挂事件。SSR 与 Hydration 是成对出现的,缺一个都不完整。

为何常见:因为 SSR 返回的 HTML 肉眼可见地“完整”,小白容易误以为它就是最终产物,忽略了交互层依赖 JS。

误区我以为注水是浏览器拿到 HTML 后用 JS 再渲染一次,生成新的 DOM 替换旧的。

正解注水不会重建 DOM,React 会复用服务端已有的 DOM 节点(这是它与客户端渲染的本质区别)。它只做两件事:生成 VDOM 与现有 DOM 对账、挂事件监听器。重新渲染会白白浪费首屏成果,甚至导致闪烁和布局抖动。

为何常见:“渲染”这个词让人以为 hydrate 也是在渲染,实际上它更接近“激活”——给静态标记注入行为。

误区控制台报 hydration mismatch 但页面照常显示,应该没什么大问题,先上线再说。

正解mismatch 意味着某节点的事件监听可能挂到了错误位置,用户点击可能触发错误组件逻辑或毫无反应。警告是“电工发现图纸和现场对不上”的红色标记,必须找到时间、环境或数据差异并修复,而不是无视。

为何常见:警告不像报错那样阻断运行,页面视觉正常,容易让人低估它对交互的破坏力。

误区这些高级特性只有 Next.js 才有,我用 React 自己搭 SSR 用不上。

正解两者都是 React 18 原生能力:hydrateRoot 配合并发特性支持选择性注水,renderToPipeableStream 提供流式渲染。Next.js 只是把它们封装进框架、开箱即用。手写 React SSR 同样可以使用。

为何常见:大家最先在 Next.js 的博客和教程里见到 streaming 与 selective 的实战,容易误以为它们是框架绑定功能。

误区流式渲染听起来更高端,应该比一次性渲染更省服务器资源。

正解Streaming 不减少渲染总量,它只是把一整块 HTML 切分成多个分块按序输出,让浏览器更早拿到“能看的部分”。服务器总 CPU 时间基本不变(甚至因分块调度略有增加)。收益在体验侧:TTFB 提前、首屏更快,而不是成本侧。

为何常见:“流式”与“分片”容易让人联想到省资源,忽略了它只是改变了产出节奏,并没有减少工作量。

面试题

Q1. SSR 相比传统 CSR 最主要的好处是什么?

单选题 · easy

  • A.大幅降低服务器内存占用
  • B.加快首屏内容呈现与SEO← 正确答案
  • C.彻底取消前端打包环节
  • D.让页面无需加载任何JS

答案:B。SSR 在服务器生成完整 HTML,浏览器一拿到就能渲染,爬虫也能直接读内容,因此首屏与 SEO 是核心收益。它不降低服务器开销,也不免除 JS(交互还需 hydration),更不会取消打包。

Q2. Hydration(注水)的核心任务是什么?

单选题 · easy

  • A.用 JS 重建全部 DOM 节点
  • B.为已有 DOM 对账并挂事件← 正确答案
  • C.把 CSS 样式全部内联化
  • D.将图片资源全部预加载

答案:B。注水不是重建 DOM,而是复用服务端 HTML,用 VDOM 对账后在已有节点上绑定事件监听,让页面可交互。A 是“重渲染”误区,C 和 D 与注水无关。

Q3. 下列哪种实现最容易触发 hydration mismatch?

单选题 · mid

  • A.组件内部使用纯函数计算
  • B.直接在渲染中读取 Date← 正确答案
  • C.使用 CSS Modules 样式
  • D.调用 useState 保存状态

答案:B。new Date() 在服务端与客户端运行的时区或时刻可能不同,渲染结果不一致从而触发 mismatch。纯函数与 useState 不依赖环境,CSS Modules 只影响样式不影响 DOM 文本。

Q4. 修复 hydration mismatch 的推荐做法是?

单选题 · mid

  • A.对所有组件使用 suppress
  • B.锁定两端渲染数据一致← 正确答案
  • C.把渲染全部改成同步
  • D.禁用服务端渲染功能

答案:B。最稳妥是让服务端与客户端首帧渲染数据一致:把浏览器专属值放进 useEffect 更新,或把服务器数据内嵌到 HTML 由客户端复用。suppress 只应局部使用,禁用 SSR 则因噎废食。

Q5. Selective Hydration 的注水单位是什么?

单选题 · mid

  • A.单个函数组件文件
  • B.Suspense 边界区域← 正确答案
  • C.整个路由页面层级
  • D.一个 React Context

答案:B。React 以 <Suspense> 包裹的区域为边界决定何时注水;未就绪的边界可暂缓注水,交互发生时再补注水。函数文件、路由、Context 都不是注水的调度单位。

Q6. React 18 的事件委托将监听器挂在哪个位置?

单选题 · hard

  • A.每个 DOM 节点自身
  • B.根容器 root 之上← 正确答案
  • C.window 全局对象上
  • D.服务端节点之上

答案:B。React 18 把事件监听统一挂在 hydrateRoot 创建的根容器上,通过事件冒泡统一分发(React 17 前挂在 document 上)。这样能以 O(1) 成本完成事件挂载。

Q7. Streaming SSR 中用户最早能看见内容取决于?

单选题 · hard

  • A.整个页面渲染完成耗时
  • B.首个 HTML 分块到达时间← 正确答案
  • C.全部 JS 下载完成时间
  • D.最大图片加载完成时间

答案:B。流式渲染把 HTML 分为多个 chunk,第一个 chunk(外壳与首屏)一到浏览器就能解析展示,所以首屏时间取决于首块到达时间,不必等全量生成或 JS 下载。

Q8. 下列哪些情况会引发 hydration mismatch?

多选题 · mid

  • A.渲染时使用随机数结果← 正确答案
  • B.两端采用不同的数据源← 正确答案
  • C.组件内使用 useEffect
  • D.直接读取 window 尺寸← 正确答案

答案:ABD。随机数会让两端文本不同,不同数据源让结构漂移,window 尺寸只在客户端存在(服务端为 undefined)。useEffect 在渲染之后执行,反而是规避 mismatch 的常用手段。

Q9. 关于 Streaming SSR 与 Selective Hydration 正确的有?

多选题 · hard

  • A.两者都能缩短首屏可见时间← 正确答案
  • B.两者都依赖 Suspense 边界← 正确答案
  • C.两者都能减少总渲染量
  • D.两者都无需客户端注水

答案:AB。Streaming 通过分块输出让首块更早到达,Selective 通过优先级调度让关键部分更早可交互,二者都围绕 Suspense 边界工作。它们不减少服务端渲染总量,也都不免客户端注水。

Q10. 关于注水前后页面状态正确的描述有?

多选题 · easy

  • A.注水前按钮点击无响应← 正确答案
  • B.注水需要执行客户端渲染← 正确答案
  • C.注水后服务端不再返回HTML
  • D.注水只发生在浏览器端← 正确答案

答案:ABD。注水前只有静态 HTML,事件未挂载所以点击无响应;注水过程中客户端会再生成一次 VDOM 用于对账;注水动作发生在浏览器。服务端是否继续返回 HTML 取决于后续请求,与本次注水无关。