想象你在网上订了一套精装房。开发商效率惊人,在工厂里把墙、地板、沙发、床全部装好,整房打包运到你面前——这就是服务端渲染(SSR):浏览器收到一段完整 HTML,立刻就能看到文字、图片和排版,完全不用等装修队现场施工。可住了几天你会郁闷地发现:这套房没通电。开关按不动、门铃不响,屋内的一切都不会回应你的操作。于是你请来电工师傅接线,把每个开关、门铃连到对应房间——这个“通电”过程,就是 Hydration(注水)。本篇文章围绕“精装房交付”展开:先看开发商如何把房子又快又好地交到手里(SSR),再看电工如何安全接线(Hydration 与事件绑定),然后处理“图纸和现场对不上”的纠纷(mismatch 警告),最后学会两招高级玩法——选择性注水和流式交付,让大户人家也能“先亮客厅、逐层入住”。
掌握 SSR 与 Hydration,是你在真实项目中做首屏性能优化、排查交互失效问题的硬核底牌。无论是 Next.js 全栈开发还是 React 架构设计,这套知识都会直接决定线上体验与稳定性。
目录
可视化演示
通过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 注水。
- 首次内容绘制 T_FCPFirst Contentful Paint,指用户第一次看到页面内容的时刻。SSR 公式把它拆成“网络等待 + 解析时间”两段。
- 首字节时间 T_TTFB从发起请求到收到 HTML 第一个字节的耗时,对应精装房从下单到货车开到门口的时间。
- JS 下载与执行 T_JSCSR 独有成本,浏览器要先下载并执行整包 JS 才能创建内容,相当于等装修队运材料、现场施工。
- 解析与渲染 T_renderHTML 到达后浏览器逐字节解析构建 DOM 树并绘制的收尾过程。
二、Hydration注水机制:电工上门接通电路
SSR 交付的静态 HTML 是一座“硬件齐全但没通电”的精装房。要让房间响应人的动作——按下开关灯亮、点击按钮弹窗,必须有电工(客户端 JS)进场:接线、装开关。这一步在 React 里叫 Hydration,字面意思是“给干涸的骨架注入水分,让它活过来”。
React 18 用 hydrateRoot(domNode, <App />) 启动注水。它并不是把服务器给的 HTML 拆掉重建,而是做两件事:第一,在客户端再跑一遍组件逻辑,生成一棵虚拟 DOM(拿到电工图纸);第二,拿着图纸和真实 DOM 逐一对账(reconcile),在匹配的节点上挂事件监听器。
对账建立在“同构渲染”前提上——服务器和客户端用同一套组件代码渲染,结构应当一致。注水成本不能忽略:它等于又执行了一次组件渲染,外加遍历全部 DOM 节点的调和。页面越大,注水越慢;注水完成前,页面虽然“看得见”但点击按钮没反应。量化一下:
注水完成,整屋通电,点击、输入、滚动全部“活”了。但这里有个烧脑问题:电工接线靠图纸——如果图纸和实际装修对不上呢?这正是第四节的 mismatch。不过在此之前,先看电工怎么用最少的线搞定全屋开关。
- 单节点调和 t_reconcile(i)注水时对每个 DOM 节点执行“客户端 VDOM 与真实 DOM 是否匹配”的检查。
- 总和 Σ把 N 个节点的调和时间加总,得到总注水耗时——节点越多,电工要核对的插座越多。
- 服务端渲染成本 C_server服务器把组件跑一遍生成 HTML 字符串的开销(工厂预制成本)。
- 客户端渲染成本 C_client注水时客户端再跑一遍组件生成 VDOM 的开销(现场复勘成本)。
- 事件挂载成本 C_attach把监听器绑定到对应 DOM 节点的布线开销。
三、事件绑定与交互:一根总线点亮全屋
如果每个开关都单独拉一根线到配电箱,60 个房间就要 60 根线。聪明电工用一根“总线”(事件委托),把所有开关串到同一条线上;按下任何一个开关,信号顺着总线传到 root 配电箱,配电箱根据房间编号分发给对应房间的逻辑。React 就是这么干的。
React 17 之前把事件监听器挂在 document 上;React 18 改挂到根容器(root container)。无论页面有多少个可交互节点,程序只在 root 上绑定一次监听器,而不是每个节点绑一个。事件发生时,通过冒泡(bubble)从目标节点一路传到 root,React 再根据 Fiber 树找到对应组件,触发其事件处理器。
这种设计让注水时的事件挂载成本从“与节点数成正比”降到常数级别。不过事件真正触发时,冒泡路径上有多少层祖先节点,就要经过多少层分发,这部分成本是动态的。公式如下:
总线方案让注水“布线”极快,但总线也好、分户线也罢,都依赖一个前提:电工图纸和实际房间严格对应。一旦对不上,接错线的风险就出现了——这正是 hydration mismatch 警告的由来。
- O(1) 委托挂载事件委托后只在 root 上绑一次监听,成本与节点数无关,是常量。
- O(N) 朴素挂载若每个节点单独绑监听,成本随节点数线性增长,房间多了线就会很乱。
- O(d) 冒泡分发事件触发时从目标节点冒泡到 root,d 是树的深度;楼层越深信号跑得越久。
四、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 个房间,电工一根一根接完要耗很久,住户在客厅干等着。聪明的物业想了个办法——选择性注水,先通客厅。
- 节点渲染函数 f_i渲染树中第 i 个节点的输出,可以看作数据 D 到 HTML 片段的一个函数。
- 两端数据 D_server 与 D_client服务器与客户端各自持有的数据;两者不一致是 mismatch 的根源。
- 存在量词 ∃只要存在任意一个 i 使两端渲染结果不同,警告就会冒出——注意是“任一”而非“全部”。
- 逻辑蕴含 ⇒推导出 mismatch 警告必然出现,意味着该节点的事件监听可能挂错位置。
五、Selective注水优化:优先点亮客厅
想象一栋 60 层的公寓,物业宣布“整栋楼同一晚通电”。电工逐层挨家接线,顶层住户苦苦等了一夜。更聪明的做法是“分层分户送电”:先给今晚就要入住的 1 楼客厅通电,其他楼层贴着封条,等住户真正进来(或靠近、或按门铃)再补电。这就是 Selective Hydration(选择性注水),React 18 的核心特性之一。
Selective Hydration 以 Suspense 边界为“分户单元”。页面首帧中,只有处于屏幕内、属于关键路径的组件会立即注水;其余被 <Suspense> 包裹的区域,React 会记录其“未注水”状态。若用户与未注水区域交互(比如点击了折叠面板),React 拦截该事件,立刻对该边界执行补注水,然后重放事件——用户感觉几乎无延迟。
底层依赖 React Scheduler 的优先级调度。注水不是按 DOM 顺序遍历,而是按优先级队列调度:紧急交互 > 关键 UI > 可见区域 > 屏幕外低优。收益是首屏交互时间从“全量注水完成”提前到“关键部分注水完成”。对比公式如下:
按需通电解决了“等全屋”问题,但还有一个老毛病没解决:开发商坚持整栋楼装修完才交房,首层住户还是要等顶层刷完漆。要彻底缩短“看到房子”的时间,得让开发商改流水线——这就是 Streaming SSR。
- 关键注水时间 T_critical首屏高优先级组件完成注水所需的时间(客厅通电时间)。
- 调度开销 τ调度器为注水任务排队分配的时间,一般极小可忽略。
- 惰性注水时间 T_lazy非关键组件若一起注水所需的时间(卧室和厨房的接线时间)。
- 全量 vs 选择式全量注水要等 T_critical 加 T_lazy,选择式只需等 T_critical,交互时间显著提前。
六、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 让一层层交付成为可能。开发商、电工、物业三层协作,最终让住户“快看到、快入住、快互动”。
- 首块到达时间 T_firstChunk第一个 HTML 分块到达浏览器的时间,流式渲染中首屏可见由它决定。
- 解析小量 ε浏览器解析并绘制第一个分块所需的时间,通常极小。
- 分块大小 S_k第 k 个 HTML 分块的字节数,相当于第 k 层楼的工程量。
- 传输速率 R_k该分块在链路上的传输速度,相当于施工队的速度。
- 累加求和 Σ所有分块“大小/速率”之和就是全部 HTML 送完的时间;减小分块或提高带宽都能提速。
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 演示
"""用 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()
server_html = ssr_render(page)调用服务端渲染,得到整段静态 HTML,相当于开发商交付精装房。expect = "<%s>%s" % (node.tag, node.text)客户端按树节点生成“期望标记”,把它当作电工手里的对账图纸。if expect not in parent_html:图纸内容在服务端产物里找不到,说明前后端不一致,记录为一处 mismatch。if node.key == "button": listeners += 1只有带关键 key 的可交互节点才算“挂载事件”,用来统计注水工作量。def selective_hydrate(client_tree, priority_keys):选择性注水的入口:按优先级集合挑选本次要通电的节点。if node.key in priority_keys:key 命中高优先级集合的节点先注水,其余进入 lazy 暂缓列表,对应“先亮客厅”。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 最主要的好处是什么?
- A.大幅降低服务器内存占用
- B.加快首屏内容呈现与SEO← 正确答案
- C.彻底取消前端打包环节
- D.让页面无需加载任何JS
答案:B。SSR 在服务器生成完整 HTML,浏览器一拿到就能渲染,爬虫也能直接读内容,因此首屏与 SEO 是核心收益。它不降低服务器开销,也不免除 JS(交互还需 hydration),更不会取消打包。
Q2. Hydration(注水)的核心任务是什么?
- A.用 JS 重建全部 DOM 节点
- B.为已有 DOM 对账并挂事件← 正确答案
- C.把 CSS 样式全部内联化
- D.将图片资源全部预加载
答案:B。注水不是重建 DOM,而是复用服务端 HTML,用 VDOM 对账后在已有节点上绑定事件监听,让页面可交互。A 是“重渲染”误区,C 和 D 与注水无关。
Q3. 下列哪种实现最容易触发 hydration mismatch?
- A.组件内部使用纯函数计算
- B.直接在渲染中读取 Date← 正确答案
- C.使用 CSS Modules 样式
- D.调用 useState 保存状态
答案:B。new Date() 在服务端与客户端运行的时区或时刻可能不同,渲染结果不一致从而触发 mismatch。纯函数与 useState 不依赖环境,CSS Modules 只影响样式不影响 DOM 文本。
Q4. 修复 hydration mismatch 的推荐做法是?
- A.对所有组件使用 suppress
- B.锁定两端渲染数据一致← 正确答案
- C.把渲染全部改成同步
- D.禁用服务端渲染功能
答案:B。最稳妥是让服务端与客户端首帧渲染数据一致:把浏览器专属值放进 useEffect 更新,或把服务器数据内嵌到 HTML 由客户端复用。suppress 只应局部使用,禁用 SSR 则因噎废食。
Q5. Selective Hydration 的注水单位是什么?
- A.单个函数组件文件
- B.Suspense 边界区域← 正确答案
- C.整个路由页面层级
- D.一个 React Context
答案:B。React 以 <Suspense> 包裹的区域为边界决定何时注水;未就绪的边界可暂缓注水,交互发生时再补注水。函数文件、路由、Context 都不是注水的调度单位。
Q6. React 18 的事件委托将监听器挂在哪个位置?
- A.每个 DOM 节点自身
- B.根容器 root 之上← 正确答案
- C.window 全局对象上
- D.服务端节点之上
答案:B。React 18 把事件监听统一挂在 hydrateRoot 创建的根容器上,通过事件冒泡统一分发(React 17 前挂在 document 上)。这样能以 O(1) 成本完成事件挂载。
Q7. Streaming SSR 中用户最早能看见内容取决于?
- A.整个页面渲染完成耗时
- B.首个 HTML 分块到达时间← 正确答案
- C.全部 JS 下载完成时间
- D.最大图片加载完成时间
答案:B。流式渲染把 HTML 分为多个 chunk,第一个 chunk(外壳与首屏)一到浏览器就能解析展示,所以首屏时间取决于首块到达时间,不必等全量生成或 JS 下载。
Q8. 下列哪些情况会引发 hydration mismatch?
- A.渲染时使用随机数结果← 正确答案
- B.两端采用不同的数据源← 正确答案
- C.组件内使用 useEffect
- D.直接读取 window 尺寸← 正确答案
答案:ABD。随机数会让两端文本不同,不同数据源让结构漂移,window 尺寸只在客户端存在(服务端为 undefined)。useEffect 在渲染之后执行,反而是规避 mismatch 的常用手段。
Q9. 关于 Streaming SSR 与 Selective Hydration 正确的有?
- A.两者都能缩短首屏可见时间← 正确答案
- B.两者都依赖 Suspense 边界← 正确答案
- C.两者都能减少总渲染量
- D.两者都无需客户端注水
答案:AB。Streaming 通过分块输出让首块更早到达,Selective 通过优先级调度让关键部分更早可交互,二者都围绕 Suspense 边界工作。它们不减少服务端渲染总量,也都不免客户端注水。
Q10. 关于注水前后页面状态正确的描述有?
- A.注水前按钮点击无响应← 正确答案
- B.注水需要执行客户端渲染← 正确答案
- C.注水后服务端不再返回HTML
- D.注水只发生在浏览器端← 正确答案
答案:ABD。注水前只有静态 HTML,事件未挂载所以点击无响应;注水过程中客户端会再生成一次 VDOM 用于对账;注水动作发生在浏览器。服务端是否继续返回 HTML 取决于后续请求,与本次注水无关。