前端

Qwik、SolidJS等新前端框架介绍和前端发展思考

梳理后单页应用(SPA)时代的三大前沿架构路径:Qwik 的可恢复性(Resumability)如何通过序列化闭包彻底消除水合(Hydration)成本;SolidJS 如何利用细粒度响应式(Signals)在移除虚拟 DOM 的同时保持 O(1) 的更新复杂度;以及 Astro 的群岛架构(Islands Architecture)在内容驱动型网站中的性能优势。以及个人对于所谓UX、DX到AX的想法

约 18 分钟

其实从我的感觉出发,前端框架,应该是软件工程技术里面迭代最快的那一批技术了,几乎每年都有声量不小的新的框架产生,甚至许多大true公司都有自己的框架。其实细想一下也可以理解,现在人们与电子屏幕的交互越来越多,对于体验的要求也越来越高,交互习惯也在变化,前端框架必然做出及时的响应;而框架的重要性,我觉得还是更多体现在开发者体验上,开发者对于性能和开发效率的极致追求和不断的取舍平衡,也促成了框架的快速迭代。一个更显著的趋势是前端技术范式的变化:从服务端渲染 (SSR) ,再到客户端渲染 (CSR) ,到现在的混合渲染 (Hybrid),这背后有交互习惯、用户习惯变化的原因,也有硬件性能更迭、网络设备架构变化的影响,可以说正是因为前端作为直接与人打交道的界面,涉及到的“利益相关方”或“影响因素”实在是太多了,为了不断地动态地适应和取舍,快读迭代也成了一种必然。所以这篇文章主要想总结一下目前前沿的前端框架的设计,以及从中得出的一些个人的思考。

1. Qwik:可恢复性架构

1.1 产生背景与解决的问题

Qwik 的诞生源于前端领域一个日益严峻的矛盾:设备性能与 JavaScript 代码体积之间的博弈。在过去十几年里,单页应用(SPA)和服务器端渲染(SSR)虽然提升了交互体验,但它们都无法逃脱一个核心瓶颈,那就是“水合(Hydration)”。

传统的现代框架如 React 或 Vue,在服务端渲染出 HTML 后,浏览器虽然能立刻显示内容,但页面是“死”的。为了让按钮能点击、菜单能展开,浏览器必须下载巨大的 JavaScript 包,解析它,然后执行一遍与服务端完全相同的渲染逻辑,将事件监听器绑定到 DOM 节点上。这个过程就是水合。随着应用越来越复杂,水合过程消耗的 CPU 时间和网络带宽呈指数级增长,导致的首屏交互延迟(TTI)成为了大型网站难以逾越的性能高墙。

Qwik 就是为了彻底消灭“水合”而生的。它由 Angular 的核心创作者 Misko Hevery 领衔开发,旨在解决 Web 应用启动速度与应用体积成正比的魔咒。在 Qwik 的愿景里,无论你的应用是一千行代码还是一百万行代码,它的首屏加载和启动速度都应该保持恒定且瞬间完成。

Qwik、SolidJS等新前端框架介绍和前端发展思考 封面

1.2 发展历程与设计哲学

Qwik 项目启动于 2020 年左右,经过了两三年的深耕,在近期迎来了稳定版本。Misko Hevery 在离开 Google 加入 Builder.io 后,带着他对 Angular 的反思和对未来 Web 的构想,从零打造了 Qwik。

Qwik 的核心设计哲学非常激进,被称为“HTML 优先”和“零 JS 启动”。它认为,HTML 本身就应该包含应用启动所需的所有信息,不仅仅是结构,还包括状态和逻辑的引用。

Qwik 提出了一个革命性的概念——可恢复性(Resumability)。可以把这想象成虚拟机的工作原理:当你保存虚拟机状态时,内存被序列化到硬盘上;当你恢复时,不需要重新启动操作系统,而是直接从暂停的那一刻继续运行。Qwik 对 Web 应用做了同样的事情。服务端执行应用,将框架的状态、组件的状态、甚至事件监听器的闭包上下文全部序列化到 HTML 字符串中。当 HTML 发送到浏览器时,应用不需要“重新启动”(水合),而是直接“恢复”执行。这意味着,在用户与页面交互之前,Qwik 不需要执行任何 JavaScript。

1.3 特色底层技术原理与代码展示

Qwik 实现这一魔法的核心技术在于它独特的优化器(Optimizer)和细粒度的代码分割策略。

传统的代码分割通常以路由或组件为边界,而 Qwik 的代码分割粒度是“函数级”的。它引入了一个特殊的语法标记 $。任何跟在 $ 符号后面的函数,都会被 Qwik 的编译器自动提取出来,打包成一个独立的、极小的 JavaScript 文件。

例如,在 Qwik 组件中,一段简单的交互代码可能长这样:

import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);

  return (
    <button onClick$={() => count.value++}>
      Count: {count.value}
    </button>
  );
});

在浏览器看来,这段代码极其神奇。首屏加载时,浏览器只会接收到一段纯 HTML,以及一个极小的全局加载器(Qwik Loader,大小不到 1KB)。那个 onClick$ 里面的箭头函数逻辑,根本不会在页面加载时下载。

Qwik 会将事件监听器序列化为 HTML 属性。示例(代码形式显示,避免被渲染成实际按钮):

<button on:click="./chunk-123.js#handler">Click</button>

只有当用户真正点击了这个按钮时,Qwik Loader 才会拦截这个点击事件,根据 HTML 里的路径去网络请求下载 chunk-123.js,然后执行其中的逻辑更新界面。

这种技术被称为“懒执行(Lazy Execution)”。更有趣的是,Qwik 能够序列化闭包。在上面的例子中,count 变量是被点击事件引用的。在传统框架中,这要求父级作用域必须存在内存中。而在 Qwik 中,count 的状态也被序列化到了 DOM 属性中。当点击发生,JS 代码下载下来后,它能自动从 DOM 中“恢复”出之前的状态,仿佛这个闭包从来没有断开过一样。这就是 Qwik 能够实现 O(1) 复杂度启动的原因。

2. SolidJS:放弃虚拟 DOM

2.1 产生背景与解决的问题

在 React 盛行之后,虚拟 DOM(Virtual DOM)似乎成了现代前端框架的标配。然而,随着对极致性能的追求,虚拟 DOM 自身的开销逐渐显现。每次状态更新,React 都需要重新运行组件函数,生成新的虚拟 DOM 树,然后与旧树进行对比(Diff),最后再打补丁到真实 DOM 上。这个过程虽然比直接操作 DOM 快,但在高频更新或大型应用中,依然存在大量的无效计算和内存浪费。

SolidJS 的出现,就是为了挑战“虚拟 DOM 是必须的”这一成见。它的作者 Ryan Carniato 是 JavaScript 性能测试领域的资深专家,他深知现代浏览器原生 DOM 操作其实已经非常快了,慢的是框架层的抽象。SolidJS 试图解决的问题是:如何在保留 React 那样优秀的开发体验(JSX、Hooks)的同时,彻底移除虚拟 DOM,让性能逼近原生 JavaScript 的极限。

2.2 发展历程与设计哲学

SolidJS 的开发始于 2016 年,实际上比许多当红框架都要早,但它一直作为一个相对低调的实验性项目存在,直到 2021 年发布 1.0 版本后,凭借在 JS Framework Benchmark 中碾压级的性能表现一战成名。

SolidJS 的设计哲学可以概括为“一次构建,细粒度更新”。它坚信组件只是用来组织代码的逻辑单元,而不是运行时的渲染边界。在 SolidJS 中,组件函数只会在初始化的那一刻执行一次,之后就再也不会运行了。这与 React 组件每次更新都要重新执行一遍有着天壤之别。

它追求的是绝对的“响应式优先(Reactivity First)”。在这种哲学下,数据驱动视图,但数据变化时,不通过对比整棵树来寻找变化点,而是数据点对点地直接连接到对应的 DOM 节点。数据一变,对应的那个文本节点或属性就直接更新,不仅没有虚拟 DOM,甚至没有组件级的 Diff。

特色底层技术原理与代码展示

SolidJS 的核心技术是“细粒度响应式系统”,也就是现在大火的 Signals(信号)机制的集大成者。它利用编译器将 JSX 直接转换成原生的 DOM 操作代码,而不是创建虚拟对象。

2.3 特色底层技术原理与代码展示

Qwik、SolidJS等新前端框架介绍和前端发展思考 图2

在 SolidJS 中,状态管理是通过 createSignal 完成的,它返回一个读取器(Getter)和一个设置器(Setter)。

import { createSignal, createEffect } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);

  const double = () => count() * 2;

  createEffect(() => {
    console.log("The count is now", count());
  });

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count()}, Double: {double()}
    </button>
  );
}

这段代码表面上看和 React 很像,但执行原理完全不同。当 Counter 组件渲染时,它只运行一次。createSignal 创建了一个响应式的数据源。关键在于 JSX 中的 {count()}。SolidJS 的编译器会识别出这里依赖了信号,因此它会生成一段订阅代码。当 setCount 被调用时,信号发出通知,SolidJS 会直接定位到 DOM 中显示数字的那个文本节点并更新它,绝不会触碰按钮的其他属性,也不会重新评估 double 函数,除非 double 内部依赖的值也变了。

此外,由于组件不重新执行,开发者在 SolidJS 中再也不需要像 React 那样处理 useCallback 或 useMemo 的依赖数组问题,闭包陷阱也几乎不存在了。它的编译器极其高效,生成的代码体积非常小,且运行时开销几乎可以忽略不计。这种“无损耗抽象”让 SolidJS 成为了当前性能最强悍的前端框架之一。

3. Astro:以内容为中心的群岛架构

3.1 产生背景与解决的问题

在 Qwik 和 SolidJS 还在卷“应用”性能的时候,Astro 将目光投向了另一个广阔的领域:内容型网站(博客、文档、营销页面、电商前台)。过去几年,开发者习惯了用 Next.js 或 Nuxt.js 等全栈框架来开发这类网站。虽然开发体验统一了,但带来了一个严重的副作用:哪怕是一个简单的静态博客,用户也被迫下载几十 KB 甚至上百 KB 的框架运行时代码。这对于内容型网站来说,是巨大的资源浪费。

Astro 的诞生就是为了解决“SPA 滥用”的问题。它认为,对于绝大多数以内容为主的网站,纯 HTML 才是最高效的传输格式。JavaScript 应该是按需加载的锦上添花,而不是页面渲染的必要条件。

3.2 发展历程与设计哲学

Astro 由 Fred K. Schott 等人发起,短短几年内迅速崛起,成为了 Jamstack 2.0 时代的代表性框架。它的核心理念是“群岛架构(Islands Architecture)”和“UI 框架无关(UI-Agnostic)”。

群岛架构是 Astro 的灵魂。想象一片全是静态 HTML 的海洋(网页背景、侧边栏、页脚),这些部分是纯静态的,不包含任何 JavaScript。在这片海洋中,漂浮着几个“岛屿”(Islands),比如一个交互式的轮播图或一个搜索栏。只有这些岛屿是动态的,会被“水合”并加载 JavaScript。这种架构让页面的 JS 载入量大大降低,通常能减少 80% 以上的脚本体积。

Qwik、SolidJS等新前端框架介绍和前端发展思考 图3另一方面,Astro 也是一个“元框架”。它本身不发明新的 UI 语法,而是允许你使用自己喜欢的框架。你可以在 Astro 项目里混用 React 组件、Vue 组件、Svelte 组件,甚至是 SolidJS 组件。

特色底层技术原理

Astro 引入了一种新的文件格式 .astro。这种文件有点像 HTML,但拥有极其强大的服务端构建能力。它的代码分为两部分:顶部的“Frontmatter 脚本区”(用 --- 包裹)和底部的模板区。

Qwik、SolidJS等新前端框架介绍和前端发展思考 图4

在构建阶段,Astro 会将所有的组件渲染成纯 HTML 字符串。如果某个组件需要交互,你需要显式地告诉 Astro。

4. 前端发展思考:转向 AX(AI Experience)

其实这么多新的框架看下来,各家有各家的理念、有各自的取舍,但至少有一个脉络是清晰的,那就是紧随时代发展。前端领域,好像没有不变的技术,没有不变的架构,以前虚拟DOM感觉要一统江山,现在也在慢慢被新框架所抛弃;其实现在还有叫XHTML的框架,直接回归到原始的写html界面,包括之前开头说的从SSR到CSR框架转变,其实也不尽然,比如react这几年的新技术叫RSC,就是回到了SSR的范式。这样一琢磨,前端可以算得上是“祖宗之法不可法”的典型了。那之后的前端发展会走上怎样一条轨迹呢?我想说说我个人的看法。

还是回到之前我们所说的前端框架发展的清晰脉络——“紧随时代发展”,毫无疑问,这个时代如果要选一个唯一的关键词,那就是AI。从我个人的感官出发,AI将要变革的不仅仅是生产力工具,更要变革人们的交互习惯。我一直以来的观点是,浏览器生态将要被AI彻底重构,现在大厂都在推AI浏览器,把各种agent往浏览器上面堆料,试图打造一个超级引擎,驱动搜索和执行。前段时间manus被meta重金收购,manus创始人直接到meta到了副总裁,这带给了我不小的震撼——龙头企业对于AI浏览器的如此重视也能反映出来一些本质东西。浏览器作为前端的主要战场,现在迎来了“AI”这个前所未见的新变量。

回忆一下,我们拿浏览器是在干什么呢?我们做的最多的一件事情就是,搜索。我可以说,任何操作、具体的执行,都是建立在“搜索”之上,或者发生在“搜索”之后的。我这里的想说的搜索,是个很广泛的概念:在百度里面搜索某个关键字、在某个应用界面搜索你想要的功能、在一篇文档里面搜索你想要的内容。只有在“搜索”完成且清晰的前提下,“操作”才有真实且准确的意义,无论你是在脑子里“操作思考”还是回到界面上“操作下一个选项“。那么前端一直在做的工作是什么呢?我个人认为,一直是在更好地引导用户去“搜索”,或者说,把“搜索”这个词进行了增强,变为了“检索”,让用户拥有了高效检索的能力——UI的设计、SEO的优化、路由管理等等等等,都是让用户的“检索之路”更加顺畅,从开始到结束的时间距离越来越短,所需要的心智负担越来越小。对于我而言,这就是前端本质的意义,提供了一种用于“检索”的快捷交互范式。

在AI的时代,“检索”的意义已经完全变化了,不再依赖于人们按图索骥的操作,而仅仅需要一句指令、一句语言,交由AI即可完成。回想一下,现在遇到一个问题,我不再习惯去点开纷繁的博客寻找答案,我会做的第一步是去打开AI聊天窗口。AI有了自主检索的能力,自然也有根据检索结果进行操作的能力。这个时候,前端对于用户来说,又意味着什么呢?如果只需要AI自动化的操作,那些精心设计的UI、精心设计的路由逻辑、精心优化的渲染效果,还有显著的意义和价值吗?可能会有,但绝不像之前十几年那样重要了。可能那些页面更多地只需要展示纯粹的视觉逻辑,就像是在告诉用户,AI是怎样一步步操作的,也就是说,它只需要给用户提供一种“安慰剂”,让用户知道:哦,AI是在有逻辑地操作。

这对于前端框架意味着什么呢?我觉得,这意味着框架的关注点不再只是DX,即developer experience,开发者体验,更多应该去思考,AX,即AI,Experience。当然了,这个AX这个词是我自己瞎造的。我觉得,前端框架在设计时,应该考虑,怎样让输出的代码或者html界面,对于AI来说具备充足的可读性和可操作性。其实我们人在编写前端代码的时候,很容易忽略一些语义。比如 <div>,本身是不存在button这种“可点击”的语义的,但我们可以强行赋予“click”这种属性,让它变得可以点击。其实我觉得这可能会一定程度上混淆AI的判断,或者让这种判断变得更复杂、更不直观。而且我们现在输出到浏览器的代码基本抹去了所有注释、语义层面的信息,这让AI需要通读整个代码上下文才能判断操作逻辑,这其实是不健康的。

其实前端设计领域有一个有意思的指标叫accessibility,也叫无障碍,原本是为了照顾残障人士的,包括运动障碍、视觉障碍、听力障碍等。为了优化无障碍性能,时常需要在前端代码里面嵌入更多更准确的语义,来更好地引导。如果我们将AI看作一种“残疾人”——至少现在来说确实还比较残疾(视觉能力欠缺,依赖语言)——那么我们的前端就必须为它们的缺陷做优化。这种优化,就像我之前说的,嵌入更多准确的语义,让整个前端代码对于AI来说更加易于理解和操作。

前端框架将重心转向AX,不代表放弃了DX和UX,这其实一种权衡的过程。UX也是需要的,人们不可能完全放心AI的操作,所以更好地展示AI的执行逻辑,可能是UI设计更需要考虑的。但进一步来讲,前端目前的首要客户,可能会是AI,而不是人类。

相关推荐