\n```\n\n**阶段二:客户端执行阶段**\n\n客户端加载完编译后的 HTML 后,全程无需执行全量应用 JS,无需重建组件树,无需绑定全局事件,仅通过极小体积的 Qwikloader 实现 “按需恢复交互”。以点击唤端按钮为例,整个流程分为三步:\n\n\n\n**1.Qwikloader 初始化**\n\n客户端加载 HTML 后,首先会执行 Qwikloader(Qwikloader 的体积极小,通常小于 1KB,不会对首屏渲染造成任何阻塞),Qwikloader 在 document 上注册全局事件监听器(如 click),用于捕获用户的交互行为,用户的每一次点击行为都会向上冒泡并由 Qwikloader 处理。此时浏览器只需完成 HTML 的解析与渲染,即可展示完整首屏,首屏渲染耗时≈HTML 加载耗时。\n\nQwik 的事件机制乍一看和 React 的事件委托很像。但 React 是为了适配虚拟 DOM 的事件系统,而 Qwik 是为了更好地管理“延迟加载” 。\n\n**2.交互触发**\n\n当用户点击 “App 内打开” 按钮时,点击事件会向上冒泡,被 Qwikloader 的全局事件监听器捕获。此时 Qwikloader 会读取按钮 on:click 属性中的 QRL 地址(./q-CyCA2y-K.js#s_pElXkJ1YLxE)。一般情况下,Qwik 可以通过 modulepreload 机制提前预加载对应的 JS,但当 JS 片段未下载时 Qwik 也会按需加载对应的 JS 片段(q-CyCA2y-K.js),这一片段体积往往仅几百字节。\n\n**3.逻辑执行**\n\nJS 片段加载完成后,Qwik 会直接执行其中的 s_pElXkJ1YLxE (QRL 中 # 后面的部分)方法,这一部分对应着唤端逻辑。\n\n如果涉及到状态变更,Qwik 会更新 JSON 快照中的对应状态,通过遍历 subs(订阅表),找到与该状态关联的 DOM 节点;调用 HTML 底部预编译的辅助函数,计算新的 DOM 内容并完成按需更新。\n\n整个阶段的流转(以点击唤端按钮为例)大致如下:\n\n\n\nQwik 之所以快速,并非因为它使用了巧妙的算法,而是因为其设计方式使得大部分 JavaScript 无需下载或执行。这套 Resumability 设计原理完全区别于传统 SSR 的技术体系,从根源上解决了传统 SSR 方案中首屏 JS 体积臃肿、水合过程阻塞主线程、交互恢复延迟等痛点。此外,这套无需水合的流程使得渲染性能得到提升,在低端设备上的内存压力得到缓解。为了更直观地对比这种技术差异,我们对传统 SSR 方案与 Qwik.js 方案进行详细的对比分析:\n\n\n\n### 3.3 极致细粒度的代码拆分:Optimizer(优化器)\n\nQwik 产物体积远小于传统框架的另一原因,是编译期的极致细粒度拆分。在 Qwik 中,所有内容都是可延迟加载的,无论是组件、方法、监听器还是样式。它相较传统框架(如 React/Vue)的按组件拆分,可以更精细到而是按事件/交互拆分,每个交互逻辑都是独立的极小 Chunk,且框架核心代码通过延迟加载且仅在必要时加载。\n\nQwik 对产物拆分的秘诀藏在这个我们熟悉的符号里:$。当开发者在 Qwik 中编写带 $ 标记的函数(如 onClick$ ),Qwik 的 Rust 优化器会自动将其转换为 QRL,这一过程完全无需开发者手动拆分。\n\n\n\nQwik 的架构充分利用现代工具来自动解决 entry point 生成的问题。优化器在打包过程中是作为 Vite 插件运行的,打包工作由我们熟悉的 Rollup 完成。在这里,每个带 $ 的函数被提取为独立的代码块(chunk);函数的闭包变量(如示例中的 count)被序列化并存储在 QRL 的索引数组中;HTML 中只保留 QRL 字符串,而非实际代码。\n\n上文提到的 Qwikloader 所做的贡献在这里需要再次被强调。浏览器加载 HTML 后,Qwikloader 注册单个全局事件监听器,用户点击按钮时,事件冒泡至全局监听器,Qwikloader 解析元素的 on:click 属性获取完整 QRL,使用 q:base(或文档 BaseURL)将相对路径转换为绝对 URL,在下载所需的极小代码块后,Qwikloader 从 chunk 中提取指定符号,使用索引数组从 HTML 中恢复闭包变量,最后执行目标函数,完成交互逻辑。QRL 能自动序列化并恢复函数的外部作用域,这是传统动态 import() 无法实现的突破。\n\n以页面信息组件为例,该组件在 M 站旧版架构中的编译后产物 ≈ 150KB(含运行时和全量逻辑),而 Qwik 首屏仅加载 1KB 的 JS,交互时按需加载几百字节的片段,提供了近乎即时的启动性能。大众点评 M 站重构后,核心 POI 详情页的首屏核心 JS 体积从 2MB 降至不足 10KB(仅框架核心和必要元数据),这是产物体积骤降的主要原因。\n\n但这里我们仍需解答两个问题:懒加载脚本会影响用户交互体验吗?延迟加载会带来 bundle 数量的上升吗?\n\n第一个问题,可能会。Qwik 既然选择在触发用户行为时再惰性加载并执行响应的 JS 脚本,那么难免需要在用户触发交互时动态生成对应的事件处理函数进行执行,但 Qwik 的事件绑定机制保障了交互不会丢失,运行时会在捕获事件的同时异步静默加载对应的事件处理器代码。其次, Qwik 引入了智能预加载模块(PreloadModule)对这一场景进行了优化。预加载模块允许应用在用户实际需要之前就开始在后台下载必要的代码,当用户鼠标悬停在可交互元素上、或页面滚动到某个元素可见区域时,Qwik 会提前静默加载该元素对应的交互代码。只有在网络条件极差的场景可能出现交互延迟,而这种情况对于主流的 React.lazy 来说也同样不可避免。\n\n第二个问题,会。但 Qwik 推崇的延迟加载其实已经是一项非常成熟的构建技术了,无论是使用 webpack、rollup 又或是其他任何构建工具都存在延迟加载和代码分割的技术。而 Qwik 的目标并非要减少 JS Bundle 的总量,而是根据用户交互逐步下载 JS,所以这个问题的答案并不重要。\n\n随着业务的迭代,应用变得更加复杂,代码变得更加臃肿。但得益于极致的代码块拆分,使用 Qwik 开发的应用的性能(可能首屏需要加载更多的 DOM 结构,但不会增加 JS 下载量)并不容易随着网站复杂度的提升而劣化。\n\n### 3.4 Qwik 的编译期优化\n\nQwik 在编译期还做了三大核心优化,进一步降低产物体积与运行时开销:\n\n**1.预编译**:传统框架(如 React、Vue)的响应式依赖是在客户端运行时完成的。Qwik 在编译期就会分析 useSignal、useStore (对应 React Hooks 中的 useState)等响应式 API 的依赖关系,明确 “哪个状态对应哪个 DOM 节点或哪个更新逻辑”,仅需执行编译期预生成的更新指令即可完成状态与 DOM 的同步,大幅减少运行时计算开销。\n\n\n\n**2.虚拟 DOM 规避**:Qwik 在大部分场景下规避了 React 所使用的虚拟 DOM,默认场景下无需虚拟 DOM,编译期会预分析响应式状态与真实 DOM 的绑定关系,生成组件边界和 DOM 自动化更新指令,直接操作真实 DOM 完成同步;仅在响应式状态发生变更(由交互或异步操作触发)时,进行细粒度的真实 DOM 更新,避免传统框架的虚拟 DOM 创建、对比与补丁开销。仅在大规模动态节点批量更新等特殊场景下,会临时使用局部轻量级虚拟 DOM 作为辅助工具,且更新完成后立即回收,不产生长期运行时开销。\n\n**3.Tree Shaking 极致化**:作为现代化打包构建工具 Vite(Rollup)的杰作,Qwik 的 API 设计(结合 QRL 资源定位机制)天然支持接近极致的 Tree Shaking,未使用的逻辑(如未触发的交互)不会被打包进入初始加载产物,仅保留轻量级引用标记,初始产物无冗余核心代码;未触发的交互逻辑会被打包为独立分包,等待客户端按需加载,进一步降低初始产物体积。\n\n### 3.5 语法易上手,精通需深入理解其设计哲学\n\nQwik 作为新兴 Web 框架,极低的学习成本与 React 几乎一致的开发范式是我们在 M 站敏捷落地的关键因素。M 站首个启动的重构项目留给研发的窗口只有两周时间,产品对技术探索的鼓励并不意味着排期能更加宽松,我们仍需从工程层面去审视其落地的成本。\n\n**较低的学习准入门槛**\n\nQwik 的设计初衷就是兼容 React 开发者的开发习惯,其核心 API 与 React 保持高度同源,仅在响应式状态、组件声明、事件方法上有微小的语法差异,所有差异均为增加标记符而非重构写法,React 开发者几乎可以做到零学习成本直接上手开发。此外,得益于 AI Coding 工具对 Qwik 的支持度相当高,我们在 H5 业务中沉淀的 AI 提效经验可以无缝复制到 Qwik 项目中,进一步抹平了语法差异带来的阻力。\n\n我们来简单看一下 Qwik 和 React 核心写法的对比:\n\n\n\nQwik 项目在目录结构上和 React 也完全没有差别。尽管它们在渲染原理上大相径庭,研发只需掌握 “组件用 component$、 方 法 用$、状态用 useSignal”这三条核心规则,就能直接上手开发业务代码,开发体验与 React 无任何割裂感。 如果你对 React 和 Qwik 的 API 差异感兴趣,可以阅读 Qwik 官网的 [React Cheat Sheet](https://qwik.dev/docs/guides/react-cheat-sheet/)。\n\n虽然 AI 编程助手(如 GitHub Copilot、Cursor 等)能有效辅助 Qwik 的语法编写,但由于 Qwik 的社区生态和训练数据量目前仍显著少于 React 等成熟框架,AI 在理解 Qwik 特定模式、最佳实践和复杂场景下的代码生成准确率可能仍有差距。这意味着一部分在成熟生态中可由 AI 高效承接的探索成本,在 Qwik 开发中可能仍需开发者人工介入和调试。\n\n**从组件思维到序列化思维**\n\nQwik 在语法层面(特别是对于熟悉 React 的开发者)确实具有较低的上手门槛,但语法易上手并不等同于工程易精通。要充分发挥其性能优势并避免常见陷阱,开发者需要深入理解其 “可恢复性、序列化边界、延迟加载” 等核心运行机理。因此,其学习曲线更接近于 “入门容易,精通需深入理解其设计哲学”。\n\n这里我们举两个简单的例子,在 React 中,闭包随处可见且无成本,但在 Qwik 中任何跨越边界的变量都必须是可序列化的,这意味着我们不能随意传递复杂的类实例;此外,如果开发者不理解 Qwik 延迟加载的特性,滥用 useVisibleTask(进入视口钩子,时机类似于 React 的 useEffect )可能导致错误的依赖追踪,极易引发网络瀑布流,框架带来的 TTI[2] 优化可能会被抵消。\n\n对底层运行机制的深度理解,以及对序列化成本的敏感度,是研发团队从写出代码进阶到写出高性能代码的必经之路。\n\n**Qwik with React,渐进式迁移的兜底**\n\n退一万步讲,你一定会想“我有模块已经在 React 上实现了迁移过来是不是有成本”、“Qwik 没有自己常用的组件库,重新造轮子是不是很浪费”,这个问题完全不用担心,Qwik 提供了官方插件 Qwik React,可以将现有的 React 组件封装在一个 qwikify$ 函数中。该组件可以在 Qwik 内部使用并将 React 组件转换为一个独立单元,主流的 UI 库经测试均能以这种方式在 Qwik 项目里引用,这也是对 Qwik 生态缺失的快速弥补方案。\n\n尽管这种混合架构提供了快速迁移路径,但每个封装组件本质上仍是一个独立的 React 运行时实例,每个 React 应用内部仍在发生着水合(但可以控制补水策略和时机),局部水合成本依旧是存在的。对于渐进式迁移的项目来说,仍需在 Qwik 原生写法带来的性能收益和直接复用 React 代码带来的工程效率上找到平衡。在 M 站商详页重构过程中,我们原本也计划将评价等外部业务团队维护的模块继续保持 React 技术栈,但我们很快就借助 AI Coding 快速抹平了 API,仅用几十分钟就原先的代码重构至 Qwik 的原生写法,这也可以给后续计划重构的业务提供参考。\n\n## 四、落地:面向首屏最优的架构设计\n\n结合 M 站的业务特性与站外场景的技术约束,本次 M 站商详页重构的架构设计,并非单一技术框架的落地,而是围绕“首屏最优”的目标打造的一套系统化工程方案。此外,为了让商详页的重构经验能在后续重构的其他页面中复用,我们深度融合了公司现有基建和技术体系,设计了基于 Qwik 的全套工具链和工程化能力。\n\n### 4.1 平衡 TTFB 与内容完整性的混合加载策略\n\n| | | |\n| --- | --- | --- |\n|  |  |  |\n\n以美食详情页重构为例,为了平衡 TTFB(首字节时间)[3]与内容完整性,我们对每类信息针对模块在页面中的视觉优先级、服务端获取耗时及业务依赖关系,梳理了加载优先级,并设计了分层的加载策略。\n\n**SSR 首屏直出层(关键内容优先)**\n\nPOI 基础信息(店名、星级、地址、相册)位于页面最顶部,定义为 L0 级数据;推荐菜、菜单可能在部分无货架供给的商户中进入首屏视口内,虽作为 L1 级数据仍需高优先级加载;对于这类数据,我们和服务端协同对所有信息进行了接口聚合,在 Node 层利用内部协议(内网接口)并行拉取这些数据,直接渲染进 HTML 文档,确保用户打开即见,最大化提升首屏感知速度。\n\n**CSR 渐进加载层(非关键信息渐进)**\n\n商家券、团购等信息(L1 级数据)依赖到餐链路且对数据新鲜度要求较高,获取耗时相对较长;评价列表、问答及商户推荐等信息(L1 级数据)位于页面首屏范围外,部分属于强依赖用户状态的个性化推荐。对于这类数据,我们在客户端侧通过 useVisibleTask$ (类似 React hooks 中的 useEffect 且第二个参数为空数组)的钩子,实现相关接口渐进式加载,配合组件资源、样式的懒加载,避免阻塞首屏关键路径。\n\n在 CSR 模块未完成加载前,服务端预渲染高度精确的 CSS 骨架屏占位,提升用户等待体验,有效降低布局抖动(CLS)。\n\n### 4.2 部署架构和网络层优化\n\n鉴于公司内部成熟的 Node.js 运维体系,我们选择公司 Serverless 平台作为 BFF(Backend for Frontend)宿主,复用其请求拦截、日志监控、容灾、负载均衡等基建能力。但由于框架兼容性与运行时环境约束,Qwik 无法直接接入现有 Serverless 平台运维体系,我们团队通过自研适配方案,攻克这一难题,进行了一系列的架构适配及增强优化:\n\n* **自定义 Adapter 层**:我们团队针对性开发了三种自定义 Adapter 方案,分别适配 Express、Fastify、Node Server 三种容器,通过对比渲染稳定性、性能损耗、常驻服务开销,最终选定更轻量级的 Node Server 版本,实现 Qwik City(Qwik 服务端套件,含路由、渲染、中间件等能力)与 Serverless 平台运行时的无缝桥接。Adapter 层对 MockLogger(日志服务)、请求处理、Runtime 上下文注入、通信分发和桥接、错误处理机制、流式输出等能力都做了统一适配,适配器的设计本身就是可复用的,可为后续业务屏蔽底层差异。\n\n\n\n* **构建和发布流程重构**: 为了提升自定义 Adapter 下的工程化能力和效率,我们重构了构建脚本与流水线服务。我们在构建脚本中完成对自定义 Adaptor 的对接适配,将构建过程中提取的 CSS 等静态资源从本地相对引用改为上传至内容分发网络,重新编排覆盖“测试 - 预发布 - 发布”流程的流水线,实现发布流程自动化,对齐 Next.js 等成熟 Web 框架的发布体验。\n\n* **路由层裁剪与 I/O 优化**:无论是 Koa Router、Express Router 还是 Qwik City,由于路由大多由文件系统驱动,Node 框架在路由的匹配和处理上都会产生一定的耗时。我们在 Qwik City 的路由层做了一些链路的裁剪,使得桥接 Serverless Runtime 的 Adapter 能定向指向固定的路由,减少了 I/O 的成本,一个云函数只处理一个页面的 SSR 加载。整个项目我们部署了多个 Serverless 云函数,页面粒度的部署架构不仅实现了容灾和高峰期扩容策略的隔离管理,还减少了一层 Qwik 路由匹配开销,进一步压缩 TTFB。\n\n* **连接池复用优化**:服务层的性能优化核心是降低 BFF 层与后端服务的跨服务调用耗时。BFF 层通过内网通信拉取服务端数据,我们设置了合理的 maxSockets 并开启 keep-alive 复用连接,采用 Node 18+ 内置的 HTTP1.1 客户端 undici (相比较传统的 HTTP 客户端 axios/fetch 有显著性能优势)最大化复用连接池。相比传统的 HTTP 接口,大幅减少了 TCP 建连耗时与报文解析开销,跨服务请求耗时平均降低 20%+,有效提升应用吞吐量。\n\n* **流式输出 + Gzip 压缩实现资源的渐进式加载**:通过渐进式资源传输缩短首屏加载耗时。初期启用了 Qwik 的流式输出能力,服务端将页面的 HTML 内容按模块拆分,分批次输出至客户端,客户端无需等待完整 HTML 加载完成即可提前解析 Head 标签内的资源(JS/CSS),进一步缩短首屏时间。同时,对所有 HTML、JS、CSS 资源开启 Gzip 压缩,资源体积再压缩 60% 以上。但从实际效果来看,由于大部分用户属于无缓存用户,即使提前 CSS 下载时机但加载完成时间仍晚于 CSS 内联方案,且流式输出能力必须搭配 Node 层的 Gzip 压缩,相比较下 Nginx 层压缩是更成熟、更节省服务器负载的方案,因此在二期版本中我们关闭了流式输出,改为将首屏核心 CSS 内联至 HTML 中。\n\n### 4.3 容错与降级机制设计\n\n对性能的追求,必须以绝对稳定和完整可用性为前提。新的技术框架和新的部署在公司内没有现成可复用的容错和降级机制,于是我们针对业务场景与流量特征设计了覆盖服务端渲染异常、浏览器兼容性过低的多层级、可配置、可观测的降级熔断机制,同时配套建设了全链路监控体系,确保在接口超时、渲染失败、低版本浏览器等极端场景下,用户仍能获得可用的基础访问体验。\n\n我们设计了以下三套可弹性自愈的降级链路,可在用户命中各类异常场景时灵活承接:\n\n**第一道防线:SSR 接口超时降级**\n\n在 SSR 渲染链路中植入主动熔断逻辑,实时监控接口的响应状态,触发超时阈值时主动切断 SSR 渲染流程,避免用户流程阻塞和请求积压。同时在客户端链路重试数据准备过程,页面从全局骨架屏开始完成从接口请求到完整渲染的全生命周期。\n\n**第二道防线:Serverless 渲染失败降级至 SSG 静态产物**\n\n针对新项目,我们在离线构建阶段同时构建了可用于 Node 持久化部署的 SSR 产物和可用于 CDN 分发的静态生成式站点(SSG)产物。该 SSG 产物在离线构建流水线中就被同步上传至内部 CDN。当 Serverless 云函数内部发生渲染失败(如模板解析异常、接口返回报错)或是上下游链路发生故障时,自动触发弹性容器平台的被动降级策略,直接从 CDN 返回预构建的静态产物,保障页面基础内容的可用性;\n\n**第三道防线:兼容性问题下的工程权衡**\n\nC 端页面为覆盖低端设备用户常引入大量 polyfill,导致打包产物冗余、JS 解析和运行开销增加。为了平衡现代浏览器下的极致体验和存量用户覆盖,我们通过 UA 嗅探,主动识别不支持 ES Modules 或 Proxy 等现代特性的极低版本浏览器(如 iOS 9),对于此类终端设备,在网关层直接执行重定向,将流量分发至功能完备的旧版 M 站,确保低端机型下用户的基本访问需求不受影响。目前线上仍保留部分节点维护旧版服务,作为平滑过渡与兜底方案,实现新旧架构的平稳共存与渐进式升级。\n\n\n\n此外,我们针对涉及 Qwik 框架的页面统一配置了技术指标看板及监控告警体系,保障服务的稳定性。\n\n如果从 0 开始搭建这套容灾架构,我们需要考虑正常 SSR 渲染链路、CSR 降级链路、静态降级页中的 CSR 链路以及正常渲染链路的二次刷新等四套不同的数据获取链路,同一个接口我们难道要各写一套逻辑去兼容这四种链路?当然不是。封装一个通用的同构请求拦截器是我们能实现这一降级机制的关键。AB 实验接入、微信鉴权、超时控制、错误处理等逻辑在双端的差异被我们在拦截器内部抹平,使得研发在接入新接口时无需关注这些兼容问题。\n\n\n\n## 五、优化:从可用到极致的毫秒级打磨\n\n无论是在重构版本交付阶段,还是在上线后,我们以最高标准为导向,持续分析线上慢请求,诊断性能短板,在多轮性能优化中寻找性能的增长点,将首屏性能指标大幅提升的基础上,进一步挖掘提升空间,对齐业内标杆水位。\n\n### 5.1 CSS 内联和按需加载策略\n\n在常规 SPA 中,CSS 通常作为外部 资源加载,直接阻塞浏览器的渲染流水线,常规的 Qwik 项目也会将所有样式文件独立打包,我们在离线构建阶段将这类静态产物同步至 CDN 。浏览器必须等待外部 CSS 下载并构建完成 CSSOM 树后,才能执行后续的 Layout 与 Paint,此过程在弱网或无缓存场景下会显著拉长首屏白屏时间,即使我们配置了长时效的 CDN 缓存策略,在流式渲染下多一次 CDN 请求所需的端到端耗时仍是个不可控的数字。\n\n重构之初我们对 M 站的缓存命中率并没有概念。但经过 AB 实验验证,在 M 站 “无缓存用户占比稳定在 50%+” 的场景下,CSS 内联方案的首屏渲染速度显著优于流式渲染下的 CDN 加载方案:我们将页面的公共样式资源(全局样式、骨架屏样式和全局响应式方案)以