昨天白天我几乎没写产品代码,干了一件像解剖课的事:把 remix.run 和 leptos.dev 两个官网抓回来逐层拆,再和本站 zjchina.net 摆在一起对比。curl 是最诚实的客户端——它不执行 JS,不偏袒任何框架,服务端到底给没给内容,一拉便知。
上两篇(《当今最前沿的全栈架构》和《“最前沿”续篇》)交代了双进程 SSR 怎么建、怎么修;这篇回答另一个问题:它在 2026 年的同代框架里,到底站在哪儿。
一句话结论先放这里:三者 curl 都拿到了完整正文,同属 SSR 阵营,但站在水合光谱的三个位置——
Leptos 全页 WASM 水合 → Remix 选择性岛屿增强 → 我们 SSR 零 WASM(WASM 只做降级备胎)。
实测证据:三家摆上同一张桌子
2026-09-13 抓取,三个站点的首页级 HTML,全部同一台机器、同一条网络:
| 指标 | remix.run | leptos.dev | zjchina.net |
|---|---|---|---|
| HTML 体积 | 116 KB | 27 KB | 42 KB |
| 正文(字节/字数) | 4.6KB / 4,630 | 4.3KB / 4,316 | 13.5KB / 5,872 汉字 |
| 首屏 JS 框架 | 按需 ESM(importmap) | 整包 wasm 水合 | 无(3 个手写小脚本) |
| 服务端注水数据 | rmx-data JSON 岛 ×3 | __RESOLVED_RESOURCES 等 | 无(不水合,无需注水) |
| 运行时版本 | Remix 3.0.0-rc.2(Vite 重写版) | Leptos + Trunk | Dioxus SSR 侧车 |
HTML 体积这行单独看会误事:remix 的 116KB 里大头是文本和内联 SVG,不是脚本;leptos 页面本身简洁,27KB 不奇怪;我们 42KB 里装着 5,872 个汉字的完整正文。
真正拉开差距的是第三行——首屏要为框架付多少 JS。
这一行决定的不是服务器吐字多快,而是手机摸到链接之后,要先下载、解析、执行多少东西才能看见页面活过来。
共同信仰:对纯 CSR 空壳的集体反叛
拆完之后我的感受是,这三家在根上是一伙的。共同信仰有三条:
- 服务端先跑一遍组件,输出成品 HTML。curl、搜索引擎、残疾网络一律拿到完整内容。这是对“纯 CSR 空壳”的集体反叛——先给一个
<div id="root"></div>再靠几百 KB JS 现场拼装的年代,三家都不回去了。 - 同一份组件代码,两种形态。服务器产出 HTML,浏览器按需复活交互。组件只写一遍,是这一代 SSR 框架共同的工程承诺。
- 都在 HTML 里埋水合数据——除了我们。原因下面单说:公开页我们根本不水合,无数据可埋。
水合光谱上的三个流派
信仰相同,复活页面的手法却是三个极端。
Leptos:全页水合,最“重”的复活
27KB 的 HTML 里全文俱在,但它同时引用 /pkg/leptos_website.js 和 .wasm,并把服务端 Resource 的状态序列化进 __RESOLVED_RESOURCES、__PENDING_RESOURCES、__INCOMPLETE_CHUNKS 三个全局槽位。浏览器加载完成后,整个应用在 WASM 里重建,接管全部 DOM。
这是水合光谱上最“厚”的一端,但代价买到的是真东西——前后端一种语言、一套组件模型,全 Rust 同构。顺带一提,Leptos 的机制和 Dioxus 最像——整应用 WASM 重建接管 DOM,正是本站 CSR 降级路径的做法。这套东西我们也有,只是不让它在首屏上场。
Remix 3:岛屿选择性增强,精准复活
抓包时有个意外发现:整个 116KB 页面里,字符串 “react” 出现 0 次。Remix 3 的 RC 版本已经和 React 解耦,是 Vite 重写过的新内核。
它用 importmap 把组件拆成按需加载的 ESM,HTML 里只有 3 个 rmx-data JSON 岛:订阅表单、主题同步、落地页增强。
只有这三个交互组件会各自加载对应的 .ts 模块,纯展示内容一字节 JS 都不付。
这是“整页水合”和“零 JS”之间最精细的中间路线——复活这一枪,只对真正会动的地方开。
我们:SSR 零水合 + WASM 备胎,最激进的减法
本站首页和文章页的 SSR HTML 里,脚本只有三个:nav.js(导航/下拉/回顶)、particles.js(粒子背景)、register-sw.js(Service Worker 注册)。全部手写 vanilla JS,无框架、无水合、无注水,合计约 531 行、gzip 后 7.7KB,而且全部 defer,不挡首绘。
同构的 Dioxus WASM 产物确实存在(876KB,wasm-opt 优化后),但它只在 SSR 侧车挂掉、回退 CSR 薄壳时才上场;desktop/mobile 打包端则天然走它。它不是错误页,是一个功能完整的底线形态。
独一份的进程拓扑:为什么我们不需要注水
水合数据(注水)的本质,是“渲染时的数据,挖个坑埋进 HTML,留给浏览器复活时挖出来用”。我们不用埋,因为拓扑不一样:
别家: 浏览器 ⇄ 全栈运行时(Remix server / Leptos SSR,一体)
我们: 浏览器 ⇄ Nginx ⇄ Drogon(C++ 门卫) ⇄ SSR 侧车(Rust 纯渲染器)
└─挂了/超时 → CSR 薄壳兜底
Remix 和 Leptos 默认自己的运行时永远活着;我们按 ROS 2 的故障域纪律设计——这个话题在第一篇里详细讲过——侧车是一个可以随时死掉的无状态节点。它不持数据,文章、统计、会话全部在 TrailBase,侧车渲染时回环请求 Drogon 的 API 现取。既然浏览器端复活的只有三个不读应用状态的 vanilla 脚本,自然无坑可埋、无水可注。
代价也明码标价:别家单进程,我们双进程,还要养着降级链路、熔断和侧车健康。这不是免费的午餐,是一笔主动选择的贷款。
一张图总结取舍
首屏 JS 成本 零 ◀──────────────────────────────▶ 整包
zjchina Remix 3 岛 Leptos
(vanilla 增强) (3 个交互组件) (全页 wasm 水合)
- 架构复杂度:我们高(双进程 + 降级)/Remix 中(单体 + importmap)/Leptos 低(单体)
- 同构语言:Leptos 是(全 Rust)/Remix 否(TS 生态)/我们否(C++ 门卫 + Rust 渲染器混编)
- 首屏 JS:我们零框架 ◀ Remix 3 个交互岛 ◀ Leptos 整包 wasm
最有意思的发现是:方向上我们反而和最新的 Remix 3 最投机——服务端给足内容,客户端只为真实交互付 JS 成本。只是 Remix 用水合的 TS 岛实现,我们用更土但更省的手写 JS 实现,把同构 WASM 留给了“底线形态”。
信仰的账单:当晚五场移动端手术
架构是最容易拿来吹牛的部分,但“零水合”只回答了“首屏 JS 为零”,没回答“零了之后,真机上到底好不好用”。解剖完三家的同一天晚上,我把攒着的五个移动端问题一次清完。它们恰好全是零水合路线必须自己还的债——没有框架替你处理启动图、安全区和触摸合成。
第一刀:全屏安装中心。 新建 deploy-guide.js(32KB),自动识别 iOS/Android/Windows/macOS/Linux 五平台并智能排序。iOS 26 真机走查安装路径四步:右下角“···”→“共享”→“添加到主屏幕”→“添加”,配五幕 SVG 引导动画;二维码用手写的 qrcode-lite.js,不引第三方库;旧的部署下拉菜单移除。
第二刀:冷启动白屏。 Service Worker 从 v8 升到 v9,导航请求由 network-first 改为 stale-while-revalidate:缓存立即首绘,后台静默 revalidate,断网也能打开。CSR 薄壳加了品牌启动动画(旋转光环 + 流光进度条),兜底层不再是黑屏等待。另外补齐 8 张 iOS 竖屏启动图:750×1334、828×1792、1125×2436、1170×2532(iPhone 13)、1179×2556、1284×2778、1290×2796、1320×2868,统一 #050510 底 + icon + 字标,8 组带 media 查询的 apple-touch-startup-image 同时接进 SSR 模板和 CSR 薄壳,从 iPhone SE 到 16 Pro Max 全覆盖。
第三刀:灵动岛与安全区。 site.css 全站补 env(safe-area-inset-*),每一处都用 max() 带桌面兜底值:SSR 内页页眉页脚、文章详情页、安装中心、admin、阅读进度条、skip-link,加上横屏左右安全区和底部 home indicator。刘海屏和灵动岛不再咬住标题。
第四刀:点击零延迟。 一条 CSS 规则覆盖所有可交互元素——a[href]、button、summary、label、各类输入控件、[role="button"]、[tabindex]:touch-action: manipulation 关掉老内核的 300ms 点击合成等待,-webkit-tap-highlight-color: rgba(0,240,255,.16) 给指尖即时反馈。正文区域保持 touch-action: auto,双击放大照常保留。iOS Safari、Android WebView、微信/抖音内核一并覆盖。
第五刀:后端配套。 sw.js 和 register-sw.js 的响应头改为 no-cache。这条债上一篇文章刚被反咬过——immutable 缓存让新 SW 卡在 HTTP 缓存里最长 24 小时,这次从规则源头钉死。
数字验收:iPhone 13 模拟 + 真机 standalone
| 指标(iPhone 13 模拟) | 首轮 | 二次访问(SW 交付) |
|---|---|---|
| TTFB | 99ms | 31ms |
| FCP | 620ms | 76ms |
| LCP | 620ms | 76ms |
| CLS | 0.000 | 0.000 |
| INP | <16ms | <16ms |
安装中心是全站交互最重的页面,测得最长 INP 128ms,仍在 Google“良好”阈值(200ms)之内。真机 standalone PWA 60fps 录屏的结果:手指触摸标签到整页首绘 83ms,单帧切换、无白屏;Service Worker 直接交付耗时 19–42ms。
作为对照,这一切的首屏框架 JS 成本是 0 字节——不是 0MB,是 0。531 行 vanilla JS 摊在导航、粒子、SW 注册三件事上,gzip 后 7.7KB,并且不挡首绘。水合光谱上“最省”的那个位置,数字站得住。
收尾:双进程是高配形态,单进程是底线形态
上一篇我写过一句话:
双进程是高配形态,单进程是底线形态。
这一天的三家解剖,算是给它补齐了技术注脚。Remix 用水合的 TS 岛,Leptos 用全页 WASM,我们用更土但更省的手写 JS,把同构 WASM 留给底线形态——侧车活着时它不上场,侧车死了它上场即是完整应用。
三条路线没有标准答案。Leptos 用最厚的客户端买下全 Rust 同构;Remix 3 把“只为真实交互付 JS”工程化到框架级别;我们把同一件事做到零框架,代价是双进程拓扑、手写脚本,和一个随时可能死掉、但死了用户也不疼的渲染侧车。
所以选型问题从来不是“哪种架构最先进”,而是“你愿意把复杂度付在哪一层”。我们把复杂度付在进程拓扑和故障降级上,换客户端可以一直穷下去。待办依然挂着:SW 版本自动版本化、线上错误监控。机器继续进化,数字继续实测——每篇文章里的每个数字,都能在 zjchina.net 上找到对应的实体。