为突破传统 Web 框架的性能瓶颈,大众点评增长团队引入 Qwik.js 重构 M 站核心页面架构,解决了重构前页面加载慢、维护成本高的难题。借助“可恢复性”能力,我们甩掉了传统水合的性能损耗,搭配全链路优化与工程化适配,让各个页面的性能指标都得到了明显提升。本文将拆解本次重构的技术选型、原理与落地细节,沉淀前沿框架在站外场景的落地经验。
一、背景与挑战:流量转化与用户体验的困境
什么是 M 站?M 即 Mobile,对大众点评而言,M 站是面向公域的流量引流入口,经近年 M 站与 PC 站形态融合、交互链路剥离后,定位进一步明确为“信息展示 + 点击唤端”的极简触点。我们所维护的大众点评 M 站,也由此聚焦至商户详情页、内容详情页、首页这三大高流量页面。在移动互联网流量进入精细化运营的背景下,主流互联网厂商均将交易、内容生产等高用户价值链路从站外转移至 App 站内,而 M 站则成为全域流量的核心收口与高效转化载体,承担着将搜索引擎、社交生态、厂商合作等渠道的免费或低成本公域精准流量,转化为 App 日活用户(DAU)的最后核心节点。作为大众点评对外流量的第一触点,这类页面无复杂的业务交互链路,只聚焦于“让用户看到信息 → 觉得有价值 → 点击唤起 App” 的极简转化逻辑。
对于这部分以“目的性极强且无粘性”为特征的访客而言,“第一印象”直接决定承接效果,用户的注意力 100% 集中在“页面能不能打开、打开快不快、信息清不清晰”上。因此,M 站的性能表现并非单纯的前端体验指标,而是无容错空间的流量转化生命线:页面白屏过长会导致用户因等不下去跳出,前期所有流量运营动作都会前功尽弃;同时,M 站页面性能也直接决定了下沉市场、增量用户群体对平台的初始心智,这部分用户也是增长产品期望触达的群体。基于此,大众点评 M 站重构的核心技术目标十分明确:就是让最差的设备加最差的网络,也能流畅打开页面,最大化挖掘公域流量的转化价值。
大众点评 M 站核心页面在重构前性能表现不佳,首屏加载体验存在明显短板,流量转化效率受限于性能瓶颈,陷入“体验差 → 转化低”的恶性循环。性能短板已成为制约免费公域流量资源利用率提升的根本瓶颈。从 2025 年 Q3 开始,产品侧就核心页面提出完全对齐站内样式的诉求,通过视觉与信息展示逻辑的统一降低认知成本,同时点评技术部发起全渠道用户体验提升专项,聚焦站内外核心链路体验优化,主动攻克 M 站性能难题。我们于去年下半年启动了对 M 站核心页面的重构,覆盖商户详情页、内容详情页等站外流量最高的页面,并于下半年陆续完成上线。
二、困局:传统架构的性能天花板
M 站页面在过去几年陆陆续续进行过一些修修补补,但最核心的商详页模块复杂,涉及到餐综、境内外等多种类目和场景,一直停留在一种早已停止维护的技术栈,本质是由小程序 DSL 通过编译时构建生成 Vue 产物。它的局限性也是显而易见的:① 首屏渲染效率低下(在无缓存的首次访问场景中页面白屏时间较长);② 运行时存在性能瓶颈(跨端编译产物天然存在冗余,首屏核心渲染依赖的资源体积偏大,弱网环境放大体验问题);③ 开发维护成本高(框架早已停止维护,学习成本极高,与流行的 AI Coding 体系完全脱节,开发体验割裂)。
考虑重构的复杂度,继续基于这套停止维护的旧技术栈迭代已不具备可行性。我们聚焦自身业务场景,深入分析站外页面性能优化的核心要点,明确极致的首屏资源控制与按需加载的交互逻辑,是突破性能瓶颈的关键方向。
以最先重构的商详页为例,商详页的站外场景有四个不可突破的约束,直接框定了渲染方案的选择范围:① 无预优化能力,首屏资源必须 “从零加载”;② POI 数据需要保持实时性;③ 大众点评 POI 数量超千万级,预生成成本极高;④ 搜索引擎为商详页、笔记页带来巨量来源,有强 SEO 诉求。在主流的渲染方案中,服务端渲染是当前场景下的唯一解法。
在技术选型上,我们对前端社区较为流行的服务端渲染方案做了全面的调研与评估:目前 Next.js、Nuxt.js 已成为行业内 SSR 的主流解决方案,不少头部厂商的 Web 基建以及公司内部部分团队的自研 SSR 框架,均基于这类成熟框架的二次封装与能力扩展;同时,SvelteKit、Qwik 等新兴的高性能 SSR 解决方案,凭借其创新的编译与运行时设计以及亮眼的数据,也逐步成为前端社区的讨论热点。
完全以 React 为模板的 Next.js 作为公司站外生态的成熟基建方案,具备稳定、易落地、学习成本低的优势,但在站外纯 H5 场景下受限于无容器预请求、无 JS 预加载而无法“借力”,其性能表现存在明显上限,此前已使用 Next.js 重构的 M 站 UGC 详情页(非同构)的首屏体验仍有提升空间。Next.js 是稳中求进的选择,但要实现性能的破局式提升,需要探索一些更具挑战性、前沿性的技术选型;SvelteKit 的 DSL 不太主流,学习成本稍高;Qwik 作为前年发布的新兴框架,生态尚不完善,在公司内甚至是国内主流互联网厂商内都没有公开落地的案例,只能依赖官方文档解决问题,框架的认知门槛与工程化落地成本也相对更高。
选型并非盲目追新,而是基于场景匹配度与收益成本比的理性决策,否则可能一时拍脑袋最后还得灰溜溜换方案。
首先,Qwik 是三种方案中首屏渲染所需资源体积最小的,其零水化特性从根源上解决了传统 SSR 的水合性能损耗,也天然对弱网和低端机型友好,能直接支撑首屏秒开提升的目标,Qwik 框架通过其独特的可恢复性设计,显著提升了应用的性能基线,这意味着在同等业务复杂度下,Qwik 能为应用提供更高的性能起点和更强的抗压能力,从而在一定程度上缓冲因业务逻辑实现不理想而带来的性能损耗;其次,C 端标准化交付场景下视觉还原和 UAT 限制了交付产物的“个性”,组件库在 C 端样式高度定制的场景下重要程度不高且可以借助 AI Coding 补齐能力;对于无复杂交互且加载时请求全量数据的以展示为主的界面来说,在业务封装和适配公司基建上也不需要投入太多,确实可以追求更轻量化的框架;Qwik 虽然生态成熟度不足但核心缺失的工具链仍可通过自研补充,Qwik API 与 React 高度同源,对 React 开发者来说只需学习少量 API 便可上手。
我们还借助 AI Coding 前置实现最小 Demo(同时实现 Next.js、SvelteKit、Qwik 版本并对比各项指标),Qwik 虽然落地门槛高但我们已攻破大部分问题并在公司平台上跑通整套构建和部署流程,且性能指标也非常亮眼。相比性能提升带来的业务价值,这些落地成本显然是可接受的。
基于以上背景与思考,我们确立了本次 M 站重构的技术目标:跳出既有技术框架的性能桎梏,从零开始探索 Qwik 生态在站内的落地可行性,兼顾极致的用户体验、高效的流量转化与可持续的研发迭代,最终落地一套可复用、可沉淀的站外渲染架构。
三、破局:为何选择 Qwik 与 Resumability?
我们选定 Qwik 作为重构技术栈,是因为它相较于其他方案加载白屏时间短、达成可交互的所需资源少,这也正好是我们对新版页面的构想。想要理解 Qwik 的性能优势,我们先从传统 CSR/SSR 方案的水合讲起,再谈谈 Qwik 的 Resumability 原理,以及编译期优化带来的极致产物体积优势。这也是 Qwik 能在同类前沿框架中脱颖而出的核心竞争力。
3.1 传统 SSR/CSR 的性能瓶颈:Hydration(水合)
无论是纯客户端渲染(CSR)还是主流的服务端渲染(SSR),传统前端框架的交互能力恢复都离不开水合这个环节。水合的是客户端对服务端预渲染 HTML 的激活过程。服务端仅能输出无交互能力的静态 HTML,想要让页面具备点击、跳转、数据更新等交互能力,客户端必须完成三个核心动作:
- 下载全量的页面组件 JS 代码与框架 Runtime;
- 解析并执行所有 JS 代码,重新走一遍组件的渲染逻辑,生成虚拟 DOM;
- 将虚拟 DOM 与服务端输出的静态 HTML 做对比、绑定事件监听,生成应用状态(State),最终让静态页面具备交互能力。
这个过程需要全量加载、全量执行、全量绑定,缺一不可。页面想要完成就绪,必须等所有 JS 加载完成、所有逻辑执行完毕,哪怕用户仅需要一个“唤起 App”的点击动作,或是首屏注入唤起点位或者执行弹窗,也必须加载整个页面的 JS 资源。此外,水合不匹配、资源加载阻塞等情况都会直接导致页面白屏,每多等一毫秒都可以导致一部分用户直接关闭页面。
从我们的数据来看,M 站有 50% 以上的流量来自 30 天内首次访问,对 M 站这类站外纯 H5、无容器预热、无资源预加载、无接口预请求、无缓存加持的场景,水合的损耗被无限放大,形成不可逆的性能瓶颈。
高版本 Next.js 常使用选择性水合、启用 SWC 压缩等方式缩短水合时间,但依旧存在优化空间。
3.2 Qwik 的 Resumability 设计
Qwik 最核心的设计理念是“跳过水合,直接恢复交互”,其底层是 Resumability(可恢复性)。它指的是:服务端不仅渲染静态 HTML,还会序列化组件的状态、事件绑定等信息并嵌入 HTML;客户端无需重新执行组件逻辑,仅在用户触发交互时,按需加载极小的交互代码片段,从根源上消除水合过程。
Resumability 的核心是:HTML 不再只是“骨架”。
在传统 SSR 中,服务器返回的 HTML 只包含静态的 DOM 结构,所有的"灵魂"(状态、事件监听器、组件逻辑等)都丢失了,必须在客户端通过水合过程重新注入。Qwik 彻底颠覆了这一模式:HTML 不再只是视图的静态快照,而是应用状态的完整序列化存储。其他所有框架的水合都会在客户端重新执行所有应用程序逻辑,而 Qwik 则是在服务器上暂停执行,然后在客户端上恢复执行。
仍以大众点评 M 站的 POI 详情页核心交互为例,我们先来看一段使用 Qwik 实现的代码片段,这段代码抽象了商户信息和唤端按钮这两个逻辑:
阶段一:离线打包构建和服务端编译阶段
和任何 SSR 框架一样,Qwik 对 HTML 文档的组装分为离线打包构建和服务端编译两个阶段。前者通过 Rollup 生成 CDN 静态产物:碎片化、可按需加载的 JS 文件和 CSS 等静态文件;后者根据实时请求的数据生成 HTML 文档。这两个部分 Qwik 主要做了以下操作:
1.组件边界构建
框架与组件树协同工作,一般情况下框架需要完全理解组件树以知晓哪些组件需要重新渲染以及何时重新渲染。在传统框架中,组件结构仅存在于 JavaScript 运行时的堆栈或虚拟 DOM 中,为了重建组件树信息框架会重新执行组件模板并记忆组件边界的位置。但 Qwik 中并没有大规模启用虚拟 DOM。为了让 HTML 具备描述组件树的能力,Qwik 引入了组件边界的概念。Qwik 编译器会在组件周围包裹 <!--qv--> 虚拟宿主节点,这些注释并非无意义的占位符,<!--qv--> 和 <!--/qv--> 分别标志了组件节点的开始和结束,这些节点使得 Qwik 实现在扁平的 DOM 中重建组件树结构。这些节点携带了关键索引属性:q:id 作为组件实例的唯一标识,用于关联后续序列化状态中的数据索引;q:key 用于列表渲染;q:sref(State Reference)标记了该组件订阅了哪些响应式数据。这使得 Qwik 无需在内存中维护虚拟 DOM,仅凭 HTML 标记即可识别组件层级与更新范围。
<!--qv q:id=a q:key=tL5t:jQ_0-->
<div style="color:blue" q:key="jQ_3" /> <!-- 某一组件编译生成的 DOM 结构 -->
<!--/qv-->
备注:以上为Qwik生成的代码片段,下同
2.标签序列化
Qwik 框架会将事件处理器序列化为 QRL 信息[1](Qwik Resource Locator,例如: ./chunk.js#handler)。它指向一个将被延迟加载的 JS 代码块,用于告知 Qwik 代码处理器应从何处加载。这种结构被编码成字符串,直接写入 HTML 属性中。此时,DOM 节点不仅包含视觉结构,还包含了交互逻辑的“入口地址”:
<!-- ./q-CyCA2y-K.js#s_pElXkJ1YLxE 指向 q-CyCA2y-K.js 导出的 s_pElXkJ1YLxE 方法 -->
<button on:click="./q-CyCA2y-K.js#s_pElXkJ1YLxE" q:key="jQ_2">App内打开</button>
3.依赖收集与状态持久化
Qwik 自顶向下只收集事件监听器捕获到的变量及其依赖。通过递归收集和去重优化,所有需要持久化的状态对象会被编号组成一个对象池数组(<script type="qwik/json" /> 中)。HTML 中的 q:id 和 q:sref 只是索引,真正的状态数据存储在底部的 JSON 中。例如,在上述示例的 JSON 中,refs 表示引用映射表,关联标识与对象索引,用于快速查找;objs 存储实际数据,包含状态对象、原始值等实例;ctx 表示上下文容器,存放组件/应用的上下文相关数据(如路由信息、注入依赖); subs 则为响应式订阅配置,订阅信息记录了依赖关联,会告知 Qwik 哪些组件需要因状态变化而重新渲染。
<script type="qwik/json">
{"refs":{"9":"0!","d":"6!"},"ctx":{},"objs":[{"likes":"9","isAppOpen":"a"},"\u00110! @1","#8",{"state":"0!"},"\u00113! @2","#b",{"state":"0!"},"\u00116! @3","#e",100,false],"subs":[["_1","3 #8 1 #8 likes","3 #b 4 #b likes","3 #e 7 #e isAppOpen"]]}
</script>
阶段二:客户端执行阶段
客户端加载完编译后的 HTML 后,全程无需执行全量应用 JS,无需重建组件树,无需绑定全局事件,仅通过极小体积的 Qwikloader 实现 “按需恢复交互”。以点击唤端按钮为例,整个流程分为三步:
1.Qwikloader 初始化
客户端加载 HTML 后,首先会执行 Qwikloader(Qwikloader 的体积极小,通常小于 1KB,不会对首屏渲染造成任何阻塞),Qwikloader 在 document 上注册全局事件监听器(如 click),用于捕获用户的交互行为,用户的每一次点击行为都会向上冒泡并由 Qwikloader 处理。此时浏览器只需完成 HTML 的解析与渲染,即可展示完整首屏,首屏渲染耗时≈HTML 加载耗时。
Qwik 的事件机制乍一看和 React 的事件委托很像。但 React 是为了适配虚拟 DOM 的事件系统,而 Qwik 是为了更好地管理“延迟加载” 。
2.交互触发
当用户点击 “App 内打开” 按钮时,点击事件会向上冒泡,被 Qwikloader 的全局事件监听器捕获。此时 Qwikloader 会读取按钮 on:click 属性中的 QRL 地址(./q-CyCA2y-K.js#s_pElXkJ1YLxE)。一般情况下,Qwik 可以通过 modulepreload 机制提前预加载对应的 JS,但当 JS 片段未下载时 Qwik 也会按需加载对应的 JS 片段(q-CyCA2y-K.js),这一片段体积往往仅几百字节。
3.逻辑执行
JS 片段加载完成后,Qwik 会直接执行其中的 s_pElXkJ1YLxE (QRL 中 # 后面的部分)方法,这一部分对应着唤端逻辑。
如果涉及到状态变更,Qwik 会更新 JSON 快照中的对应状态,通过遍历 subs(订阅表),找到与该状态关联的 DOM 节点;调用 HTML 底部预编译的辅助函数,计算新的 DOM 内容并完成按需更新。
整个阶段的流转(以点击唤端按钮为例)大致如下:
Qwik 之所以快速,并非因为它使用了巧妙的算法,而是因为其设计方式使得大部分 JavaScript 无需下载或执行。这套 Resumability 设计原理完全区别于传统 SSR 的技术体系,从根源上解决了传统 SSR 方案中首屏 JS 体积臃肿、水合过程阻塞主线程、交互恢复延迟等痛点。此外,这套无需水合的流程使得渲染性能得到提升,在低端设备上的内存压力得到缓解。为了更直观地对比这种技术差异,我们对传统 SSR 方案与 Qwik.js 方案进行详细的对比分析:
3.3 极致细粒度的代码拆分:Optimizer(优化器)
Qwik 产物体积远小于传统框架的另一原因,是编译期的极致细粒度拆分。在 Qwik 中,所有内容都是可延迟加载的,无论是组件、方法、监听器还是样式。它相较传统框架(如 React/Vue)的按组件拆分,可以更精细到而是按事件/交互拆分,每个交互逻辑都是独立的极小 Chunk,且框架核心代码通过延迟加载且仅在必要时加载。
Qwik 对产物拆分的秘诀藏在这个我们熟悉的符号里:$。当开发者在 Qwik 中编写带 $ 标记的函数(如 onClick$ ),Qwik 的 Rust 优化器会自动将其转换为 QRL,这一过程完全无需开发者手动拆分。
Qwik 的架构充分利用现代工具来自动解决 entry point 生成的问题。优化器在打包过程中是作为 Vite 插件运行的,打包工作由我们熟悉的 Rollup 完成。在这里,每个带 $ 的函数被提取为独立的代码块(chunk);函数的闭包变量(如示例中的 count)被序列化并存储在 QRL 的索引数组中;HTML 中只保留 QRL 字符串,而非实际代码。
上文提到的 Qwikloader 所做的贡献在这里需要再次被强调。浏览器加载 HTML 后,Qwikloader 注册单个全局事件监听器,用户点击按钮时,事件冒泡至全局监听器,Qwikloader 解析元素的 on:click 属性获取完整 QRL,使用 q:base(或文档 BaseURL)将相对路径转换为绝对 URL,在下载所需的极小代码块后,Qwikloader 从 chunk 中提取指定符号,使用索引数组从 HTML 中恢复闭包变量,最后执行目标函数,完成交互逻辑。QRL 能自动序列化并恢复函数的外部作用域,这是传统动态 import() 无法实现的突破。
以页面信息组件为例,该组件在 M 站旧版架构中的编译后产物 ≈ 150KB(含运行时和全量逻辑),而 Qwik 首屏仅加载 1KB 的 JS,交互时按需加载几百字节的片段,提供了近乎即时的启动性能。大众点评 M 站重构后,核心 POI 详情页的首屏核心 JS 体积从 2MB 降至不足 10KB(仅框架核心和必要元数据),这是产物体积骤降的主要原因。
但这里我们仍需解答两个问题:懒加载脚本会影响用户交互体验吗?延迟加载会带来 bundle 数量的上升吗?
第一个问题,可能会。Qwik 既然选择在触发用户行为时再惰性加载并执行响应的 JS 脚本,那么难免需要在用户触发交互时动态生成对应的事件处理函数进行执行,但 Qwik 的事件绑定机制保障了交互不会丢失,运行时会在捕获事件的同时异步静默加载对应的事件处理器代码。其次, Qwik 引入了智能预加载模块(PreloadModule)对这一场景进行了优化。预加载模块允许应用在用户实际需要之前就开始在后台下载必要的代码,当用户鼠标悬停在可交互元素上、或页面滚动到某个元素可见区域时,Qwik 会提前静默加载该元素对应的交互代码。只有在网络条件极差的场景可能出现交互延迟,而这种情况对于主流的 React.lazy 来说也同样不可避免。
第二个问题,会。但 Qwik 推崇的延迟加载其实已经是一项非常成熟的构建技术了,无论是使用 webpack、rollup 又或是其他任何构建工具都存在延迟加载和代码分割的技术。而 Qwik 的目标并非要减少 JS Bundle 的总量,而是根据用户交互逐步下载 JS,所以这个问题的答案并不重要。
随着业务的迭代,应用变得更加复杂,代码变得更加臃肿。但得益于极致的代码块拆分,使用 Qwik 开发的应用的性能(可能首屏需要加载更多的 DOM 结构,但不会增加 JS 下载量)并不容易随着网站复杂度的提升而劣化。
3.4 Qwik 的编译期优化
Qwik 在编译期还做了三大核心优化,进一步降低产物体积与运行时开销:
1.预编译:传统框架(如 React、Vue)的响应式依赖是在客户端运行时完成的。Qwik 在编译期就会分析 useSignal、useStore (对应 React Hooks 中的 useState)等响应式 API 的依赖关系,明确 “哪个状态对应哪个 DOM 节点或哪个更新逻辑”,仅需执行编译期预生成的更新指令即可完成状态与 DOM 的同步,大幅减少运行时计算开销。
2.虚拟 DOM 规避:Qwik 在大部分场景下规避了 React 所使用的虚拟 DOM,默认场景下无需虚拟 DOM,编译期会预分析响应式状态与真实 DOM 的绑定关系,生成组件边界和 DOM 自动化更新指令,直接操作真实 DOM 完成同步;仅在响应式状态发生变更(由交互或异步操作触发)时,进行细粒度的真实 DOM 更新,避免传统框架的虚拟 DOM 创建、对比与补丁开销。仅在大规模动态节点批量更新等特殊场景下,会临时使用局部轻量级虚拟 DOM 作为辅助工具,且更新完成后立即回收,不产生长期运行时开销。
3.Tree Shaking 极致化:作为现代化打包构建工具 Vite(Rollup)的杰作,Qwik 的 API 设计(结合 QRL 资源定位机制)天然支持接近极致的 Tree Shaking,未使用的逻辑(如未触发的交互)不会被打包进入初始加载产物,仅保留轻量级引用标记,初始产物无冗余核心代码;未触发的交互逻辑会被打包为独立分包,等待客户端按需加载,进一步降低初始产物体积。
3.5 语法易上手,精通需深入理解其设计哲学
Qwik 作为新兴 Web 框架,极低的学习成本与 React 几乎一致的开发范式是我们在 M 站敏捷落地的关键因素。M 站首个启动的重构项目留给研发的窗口只有两周时间,产品对技术探索的鼓励并不意味着排期能更加宽松,我们仍需从工程层面去审视其落地的成本。
较低的学习准入门槛
Qwik 的设计初衷就是兼容 React 开发者的开发习惯,其核心 API 与 React 保持高度同源,仅在响应式状态、组件声明、事件方法上有微小的语法差异,所有差异均为增加标记符而非重构写法,React 开发者几乎可以做到零学习成本直接上手开发。此外,得益于 AI Coding 工具对 Qwik 的支持度相当高,我们在 H5 业务中沉淀的 AI 提效经验可以无缝复制到 Qwik 项目中,进一步抹平了语法差异带来的阻力。
我们来简单看一下 Qwik 和 React 核心写法的对比:
Qwik 项目在目录结构上和 React 也完全没有差别。尽管它们在渲染原理上大相径庭,研发只需掌握 “组件用 component、状态用 useSignal”这三条核心规则,就能直接上手开发业务代码,开发体验与 React 无任何割裂感。 如果你对 React 和 Qwik 的 API 差异感兴趣,可以阅读 Qwik 官网的 React Cheat Sheet。
虽然 AI 编程助手(如 GitHub Copilot、Cursor 等)能有效辅助 Qwik 的语法编写,但由于 Qwik 的社区生态和训练数据量目前仍显著少于 React 等成熟框架,AI 在理解 Qwik 特定模式、最佳实践和复杂场景下的代码生成准确率可能仍有差距。这意味着一部分在成熟生态中可由 AI 高效承接的探索成本,在 Qwik 开发中可能仍需开发者人工介入和调试。
从组件思维到序列化思维
Qwik 在语法层面(特别是对于熟悉 React 的开发者)确实具有较低的上手门槛,但语法易上手并不等同于工程易精通。要充分发挥其性能优势并避免常见陷阱,开发者需要深入理解其 “可恢复性、序列化边界、延迟加载” 等核心运行机理。因此,其学习曲线更接近于 “入门容易,精通需深入理解其设计哲学”。
这里我们举两个简单的例子,在 React 中,闭包随处可见且无成本,但在 Qwik 中任何跨越边界的变量都必须是可序列化的,这意味着我们不能随意传递复杂的类实例;此外,如果开发者不理解 Qwik 延迟加载的特性,滥用 useVisibleTask(进入视口钩子,时机类似于 React 的 useEffect )可能导致错误的依赖追踪,极易引发网络瀑布流,框架带来的 TTI[2] 优化可能会被抵消。
对底层运行机制的深度理解,以及对序列化成本的敏感度,是研发团队从写出代码进阶到写出高性能代码的必经之路。
Qwik with React,渐进式迁移的兜底
退一万步讲,你一定会想“我有模块已经在 React 上实现了迁移过来是不是有成本”、“Qwik 没有自己常用的组件库,重新造轮子是不是很浪费”,这个问题完全不用担心,Qwik 提供了官方插件 Qwik React,可以将现有的 React 组件封装在一个 qwikify$ 函数中。该组件可以在 Qwik 内部使用并将 React 组件转换为一个独立单元,主流的 UI 库经测试均能以这种方式在 Qwik 项目里引用,这也是对 Qwik 生态缺失的快速弥补方案。
尽管这种混合架构提供了快速迁移路径,但每个封装组件本质上仍是一个独立的 React 运行时实例,每个 React 应用内部仍在发生着水合(但可以控制补水策略和时机),局部水合成本依旧是存在的。对于渐进式迁移的项目来说,仍需在 Qwik 原生写法带来的性能收益和直接复用 React 代码带来的工程效率上找到平衡。在 M 站商详页重构过程中,我们原本也计划将评价等外部业务团队维护的模块继续保持 React 技术栈,但我们很快就借助 AI Coding 快速抹平了 API,仅用几十分钟就原先的代码重构至 Qwik 的原生写法,这也可以给后续计划重构的业务提供参考。
四、落地:面向首屏最优的架构设计
结合 M 站的业务特性与站外场景的技术约束,本次 M 站商详页重构的架构设计,并非单一技术框架的落地,而是围绕“首屏最优”的目标打造的一套系统化工程方案。此外,为了让商详页的重构经验能在后续重构的其他页面中复用,我们深度融合了公司现有基建和技术体系,设计了基于 Qwik 的全套工具链和工程化能力。
4.1 平衡 TTFB 与内容完整性的混合加载策略
以美食详情页重构为例,为了平衡 TTFB(首字节时间)[3]与内容完整性,我们对每类信息针对模块在页面中的视觉优先级、服务端获取耗时及业务依赖关系,梳理了加载优先级,并设计了分层的加载策略。
SSR 首屏直出层(关键内容优先)
POI 基础信息(店名、星级、地址、相册)位于页面最顶部,定义为 L0 级数据;推荐菜、菜单可能在部分无货架供给的商户中进入首屏视口内,虽作为 L1 级数据仍需高优先级加载;对于这类数据,我们和服务端协同对所有信息进行了接口聚合,在 Node 层利用内部协议(内网接口)并行拉取这些数据,直接渲染进 HTML 文档,确保用户打开即见,最大化提升首屏感知速度。
CSR 渐进加载层(非关键信息渐进)
商家券、团购等信息(L1 级数据)依赖到餐链路且对数据新鲜度要求较高,获取耗时相对较长;评价列表、问答及商户推荐等信息(L1 级数据)位于页面首屏范围外,部分属于强依赖用户状态的个性化推荐。对于这类数据,我们在客户端侧通过 useVisibleTask$ (类似 React hooks 中的 useEffect 且第二个参数为空数组)的钩子,实现相关接口渐进式加载,配合组件资源、样式的懒加载,避免阻塞首屏关键路径。
在 CSR 模块未完成加载前,服务端预渲染高度精确的 CSS 骨架屏占位,提升用户等待体验,有效降低布局抖动(CLS)。
4.2 部署架构和网络层优化
鉴于公司内部成熟的 Node.js 运维体系,我们选择公司 Serverless 平台作为 BFF(Backend for Frontend)宿主,复用其请求拦截、日志监控、容灾、负载均衡等基建能力。但由于框架兼容性与运行时环境约束,Qwik 无法直接接入现有 Serverless 平台运维体系,我们团队通过自研适配方案,攻克这一难题,进行了一系列的架构适配及增强优化:
- 自定义 Adapter 层:我们团队针对性开发了三种自定义 Adapter 方案,分别适配 Express、Fastify、Node Server 三种容器,通过对比渲染稳定性、性能损耗、常驻服务开销,最终选定更轻量级的 Node Server 版本,实现 Qwik City(Qwik 服务端套件,含路由、渲染、中间件等能力)与 Serverless 平台运行时的无缝桥接。Adapter 层对 MockLogger(日志服务)、请求处理、Runtime 上下文注入、通信分发和桥接、错误处理机制、流式输出等能力都做了统一适配,适配器的设计本身就是可复用的,可为后续业务屏蔽底层差异。
-
构建和发布流程重构: 为了提升自定义 Adapter 下的工程化能力和效率,我们重构了构建脚本与流水线服务。我们在构建脚本中完成对自定义 Adaptor 的对接适配,将构建过程中提取的 CSS 等静态资源从本地相对引用改为上传至内容分发网络,重新编排覆盖“测试 - 预发布 - 发布”流程的流水线,实现发布流程自动化,对齐 Next.js 等成熟 Web 框架的发布体验。
-
路由层裁剪与 I/O 优化:无论是 Koa Router、Express Router 还是 Qwik City,由于路由大多由文件系统驱动,Node 框架在路由的匹配和处理上都会产生一定的耗时。我们在 Qwik City 的路由层做了一些链路的裁剪,使得桥接 Serverless Runtime 的 Adapter 能定向指向固定的路由,减少了 I/O 的成本,一个云函数只处理一个页面的 SSR 加载。整个项目我们部署了多个 Serverless 云函数,页面粒度的部署架构不仅实现了容灾和高峰期扩容策略的隔离管理,还减少了一层 Qwik 路由匹配开销,进一步压缩 TTFB。
-
连接池复用优化:服务层的性能优化核心是降低 BFF 层与后端服务的跨服务调用耗时。BFF 层通过内网通信拉取服务端数据,我们设置了合理的 maxSockets 并开启 keep-alive 复用连接,采用 Node 18+ 内置的 HTTP1.1 客户端 undici (相比较传统的 HTTP 客户端 axios/fetch 有显著性能优势)最大化复用连接池。相比传统的 HTTP 接口,大幅减少了 TCP 建连耗时与报文解析开销,跨服务请求耗时平均降低 20%+,有效提升应用吞吐量。
-
流式输出 + Gzip 压缩实现资源的渐进式加载:通过渐进式资源传输缩短首屏加载耗时。初期启用了 Qwik 的流式输出能力,服务端将页面的 HTML 内容按模块拆分,分批次输出至客户端,客户端无需等待完整 HTML 加载完成即可提前解析 Head 标签内的资源(JS/CSS),进一步缩短首屏时间。同时,对所有 HTML、JS、CSS 资源开启 Gzip 压缩,资源体积再压缩 60% 以上。但从实际效果来看,由于大部分用户属于无缓存用户,即使提前 CSS 下载时机但加载完成时间仍晚于 CSS 内联方案,且流式输出能力必须搭配 Node 层的 Gzip 压缩,相比较下 Nginx 层压缩是更成熟、更节省服务器负载的方案,因此在二期版本中我们关闭了流式输出,改为将首屏核心 CSS 内联至 HTML 中。
4.3 容错与降级机制设计
对性能的追求,必须以绝对稳定和完整可用性为前提。新的技术框架和新的部署在公司内没有现成可复用的容错和降级机制,于是我们针对业务场景与流量特征设计了覆盖服务端渲染异常、浏览器兼容性过低的多层级、可配置、可观测的降级熔断机制,同时配套建设了全链路监控体系,确保在接口超时、渲染失败、低版本浏览器等极端场景下,用户仍能获得可用的基础访问体验。
我们设计了以下三套可弹性自愈的降级链路,可在用户命中各类异常场景时灵活承接:
第一道防线:SSR 接口超时降级
在 SSR 渲染链路中植入主动熔断逻辑,实时监控接口的响应状态,触发超时阈值时主动切断 SSR 渲染流程,避免用户流程阻塞和请求积压。同时在客户端链路重试数据准备过程,页面从全局骨架屏开始完成从接口请求到完整渲染的全生命周期。
第二道防线:Serverless 渲染失败降级至 SSG 静态产物
针对新项目,我们在离线构建阶段同时构建了可用于 Node 持久化部署的 SSR 产物和可用于 CDN 分发的静态生成式站点(SSG)产物。该 SSG 产物在离线构建流水线中就被同步上传至内部 CDN。当 Serverless 云函数内部发生渲染失败(如模板解析异常、接口返回报错)或是上下游链路发生故障时,自动触发弹性容器平台的被动降级策略,直接从 CDN 返回预构建的静态产物,保障页面基础内容的可用性;
第三道防线:兼容性问题下的工程权衡
C 端页面为覆盖低端设备用户常引入大量 polyfill,导致打包产物冗余、JS 解析和运行开销增加。为了平衡现代浏览器下的极致体验和存量用户覆盖,我们通过 UA 嗅探,主动识别不支持 ES Modules 或 Proxy 等现代特性的极低版本浏览器(如 iOS 9),对于此类终端设备,在网关层直接执行重定向,将流量分发至功能完备的旧版 M 站,确保低端机型下用户的基本访问需求不受影响。目前线上仍保留部分节点维护旧版服务,作为平滑过渡与兜底方案,实现新旧架构的平稳共存与渐进式升级。
此外,我们针对涉及 Qwik 框架的页面统一配置了技术指标看板及监控告警体系,保障服务的稳定性。
如果从 0 开始搭建这套容灾架构,我们需要考虑正常 SSR 渲染链路、CSR 降级链路、静态降级页中的 CSR 链路以及正常渲染链路的二次刷新等四套不同的数据获取链路,同一个接口我们难道要各写一套逻辑去兼容这四种链路?当然不是。封装一个通用的同构请求拦截器是我们能实现这一降级机制的关键。AB 实验接入、微信鉴权、超时控制、错误处理等逻辑在双端的差异被我们在拦截器内部抹平,使得研发在接入新接口时无需关注这些兼容问题。
五、优化:从可用到极致的毫秒级打磨
无论是在重构版本交付阶段,还是在上线后,我们以最高标准为导向,持续分析线上慢请求,诊断性能短板,在多轮性能优化中寻找性能的增长点,将首屏性能指标大幅提升的基础上,进一步挖掘提升空间,对齐业内标杆水位。
5.1 CSS 内联和按需加载策略
在常规 SPA 中,CSS 通常作为外部 <link> 资源加载,直接阻塞浏览器的渲染流水线,常规的 Qwik 项目也会将所有样式文件独立打包,我们在离线构建阶段将这类静态产物同步至 CDN 。浏览器必须等待外部 CSS 下载并构建完成 CSSOM 树后,才能执行后续的 Layout 与 Paint,此过程在弱网或无缓存场景下会显著拉长首屏白屏时间,即使我们配置了长时效的 CDN 缓存策略,在流式渲染下多一次 CDN 请求所需的端到端耗时仍是个不可控的数字。
重构之初我们对 M 站的缓存命中率并没有概念。但经过 AB 实验验证,在 M 站 “无缓存用户占比稳定在 50%+” 的场景下,CSS 内联方案的首屏渲染速度显著优于流式渲染下的 CDN 加载方案:我们将页面的公共样式资源(全局样式、骨架屏样式和全局响应式方案)以 <style> 的形式内联至 HTML 的 <head> 标签内,避免因 CSS 加载阻塞首屏渲染;次屏样式则跟随具体的模块懒加载。
我们基于 Qwik 的 useStylesScoped$ 钩子结合 Vite 的编译能力,编译时通过 Vite 的 ?inline 查询参数将组件的样式文件在构建过程中提取为字符串,服务端渲染时这些样式片段直接被注入到 <style> 标签中。这一优化彻底消除 CSS 资源的网络往返时延(RTT),当 HTML 下载完成的瞬间 CSSOM 树即构建完成,浏览器可以立即进行 Layout,直接达成 首次内容绘制(FCP)。使 FCP 时间 TP90 降低 100+ms,对性能指标提升明显。
5.2 资源编排优化:关键渲染路径优先
资源加载顺序是影响首屏阶段进程和网络带宽调度效率的重要因素,一些不必要的第三方依赖在错误的时机引入极有可能直接阻塞 HTML 解析和 JS 执行,在旧版 M 站商详页中我们就面临着资源编排混乱的问题。我们在这方面做了不少优化:
静态资源优化
为了让 LCP(最大内容绘制)元素(通常是商户头图、笔记头图)以最快速度 Ready,我们对这类图片资源进行了加载优先级抢占。服务端编译阶段向预装的 HTML 头部动态注入 LCP 图片 Preload 标签,提前触发资源下载;同时设置请求优先级(设置 fetchPriority="high")抢占网络带宽。
在 UGC 详情页中,我们复用了站内信息流优化沉淀的能力,针对笔记头图接入 AVIF、WebP、JPG 三种图片格式,这些图片本身是在前置流程中预处理的,我们根据用户设备兼容性分级展示,绝大多数现代浏览器可使用 AVIF 格式进行加载。AVIF 格式资源在相同宽高下平均体积相较最古老的 JPG 格式降低 80%,接入 AVIF 后 UGC 详情页的 LCP 时间平均下降了 300+ ms。
此外,我们前置对静态资源进行了域名收敛,针对这些 CDN 域名我们在 HTML 模板中注入了 DNS 预解析和预连接逻辑,提前建立网络连接,降低静态资源网络准备阶段耗时。非首屏图片我们采取懒加载的策略,避免占用首屏带宽。
碎片化 JS 加载
依托 Qwik Optimizer 的编译期优化,我们摒弃传统框架 “组件级拆分” 的粗放模式,实现了交互行为级细粒度拆分,将页面逐一编译为无数个极小的 Chunk。首屏阶段仅加载 “静态 HTML + 序列化元数据”,并依据交互行为细粒度加载 Chunk,对比重构前 2MB 的首屏 JS 体积降幅超 99%,从根源上消除了 JS 解析、编译、执行带来的首屏阻塞。
高效管理第三方依赖
M 站的第三方依赖失控是原先架构中存在的一项严重问题,随着需求迭代,这些第三方依赖会变得无序、冲突。除了依赖 Vite 的 Tree Shaking 机制,新版架构严格执行 “首屏无冗余” 原则,通过 HTML 模板注入所需的第三方依赖(埋点 SDK、日志收集 SDK、微信 JSSDK 等),按最小必要原则引入工具库。
结合依赖使用场景我们动态调整了引入优先级,例如:埋点 SDK 需要尽快引入以保证埋点准确性;唤起 SDK、口令能力需要在用户发起手势交互前就绪;而微信 JSSDK 在微信环境按需导入,但由于其未就绪也不会影响正常体验,它只能利用网络和进程空闲延迟加载。
5.3 接口聚合和优化
M 站旧商详页接口在重构之前,面临着请求碎片化(一个完整的商详页渲染需要前端并发或串行调用多个分散的业务接口,导致网络开销巨大)、串行依赖严重(关键流程如“登录态校验”、“Token 续期”、“A/B 实验配置”主要依赖前端串行请求,客户端必须先拿到这些前置结果,才能发起主接口数据请求,导致 FMP 被大幅拉长)等问题。
服务端研发和我们紧密配合,构建商详页 BFF 聚合层,通过接口聚合、编排、数据裁剪(将分散的业务模块统一封装,在服务端内部进行高效的并发编排,并剔除冗余字段,减少传输包体积)、前置流程下沉(将“登录态校验”、“Token 续期”及“实验分流决策”逻辑全部下沉至商详主接口内部处理,不再依赖前端发起独立请求。节省 2-3 次网络 RTT)等措施提升聚合接口性能,后端聚合主接口响应 TP95 降低 31.6%。同时结合我们做的连接池复用等端到端上的优化,对首屏性能的提升作用明显。
六、成果与展望
本次大众点评 M 站基于 Qwik 的重构,实现了“技术优化 → 体验升级 → 业务增长”的完整闭环,在核心体验与业务价值上均取得显著成果,充分验证了前沿框架在站外场景的落地价值,也彰显了我们团队的技术攻坚与全链路优化能力。
首先,首屏性能的跨越式提升。核心商详页首屏秒开率大幅提升,首屏加载时间显著缩短,用户等待成本大幅降低;UGC 详情页相较旧版本,首屏体验同样实现明显优化,弱网、低端机型下表现尤为突出;首屏 JS 加载体积大幅缩减,进一步提升了资源效率。其次,业务价值实现高效转化。性能优化直接带动 M 站相关的业务指标稳步增长,不同页面的重构与性能优化动作均实现正向业务贡献,充分验证了“体验决定转化”的核心逻辑,高效释放了公域流量价值。
新架构的主要优势在于初始页面加载和交互响应时间,它的 Resumability 模式为站外生态的性能优化提供了全新思路与可复用经验。未来,我们计划进一步探索边缘计算渲染、RPC 改造优化、优化数据缓存策略、以 Partytown 方案拆分更多依赖资源,继续探索高性能前端页面开发。
在前端技术快速迭代的今天,社区里新框架、新工具、新范式层出不穷,性能优化早已不再是遥不可及的技术大山。真正的挑战早已不再是“能不能做”。只要我们敢于跳出舒适区,敢于在真实业务场景中探索、试错、验证、沉淀,就能把理论上的性能潜力,转化为用户可感知的真实体验提升。性能优化没有终点,只有持续的迭代与突破。
本次重构过程中,我们踩过框架适配、基建兼容的诸多坑,也沉淀了工程化适配等可复用的实践。希望本文所分享的 Qwik 落地经验、性能优化思路,能够为更多面临站外场景性能瓶颈、计划探索前沿 SSR 框架的业务提供参考,助力更多团队高效落地高性能前端架构。
ShowCase:
| 商详页 - 重构前后对比 | 商详页 - 弱网重构前后对比 |
| UGC 详情页 - 重构前后对比 | UGC 详情页 - 弱网重构前后对比 |
七、致谢
全新渲染架构的落地及其带来的效果提升离不开大众点评增长业务的产研测团队的紧密配合和高效协作,在此表示感谢!
注释
- [1] QRL 信息:Qwik Resource Locator,Qwik 的事件定位符,指向按需加载的交互代码片段
- [2] TTI:可交互时间
- [3] TTFB(首字节时间:TTFB 指从客户端发起 HTTP 请求到接收到服务器返回的第一个字节的时间间隔,是衡量服务端响应速度的关键指标。
| 关注「美团技术团队」微信公众号,阅读更多技术干货!
| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至 tech@meituan.com 申请授权。