JavaScript 框架的 AI 成本基准测试
The AI cost of JavaScript frameworks
Nolan Lawson 用 PokéAPI 设计了一个小型 Pokémon 应用,让 Opus 5.5 在 12 种 JavaScript 框架或元框架下各构建 5 次,对比开发成本、token 消耗、代码行数和 Lighthouse 指标。
我对 web 开发社区的一大不满是,我们做出的许多决定都是基于感觉的。框架“快如闪电”,工具会“超级加速”你的开发,正确的抽象会让开发者和用户双双受益(DX 和 UX 一举两得!)。所有这些大言不惭的吹嘘通常都没有任何证据支撑。Alex Russell 称之为“前端的失落十年”,Baldur Bjarnason 把编程称为一种流行文化,而我也在自己的博客里尝试用事实和基准测试来对抗这一趋势(至少在我状态最好的时候)。
很长一段时间里,我们社区对证据的过敏似乎是由对更高 DX 的渴望驱动的。换句话说,开发者就是喜欢在 React 里敲 JSX 的那种感觉,而推崇 React 或相关工具的那些论调(“虚拟 DOM 很快!”)很大程度上只是为这些最初的直觉感受事后找补的合理化说辞。(Jonathan Haidt 大概会认同这一判断。)
但在智能体时代,开发者不再手写代码,因而受 DX 的约束更少,我仍然看到感觉在主导一切(而且不只是“氛围编程”意义上的)。前端被重写,是因为某个框架“对智能体更友好”。(有人想过问问智能体本人吗?)重写后的代码“更快”,智能体“犯的错误更少”,等等。这些说法同样常常没有证据。每当我读到这类叙述,我都会想:“基准测试在哪儿?”
于是我很好奇,就写了一个基准测试。这个基准测试的结构相当直接:
- 用 PokéAPI 设计一个小巧但不简单的宝可梦应用(关注过我的工作的人不会感到意外)
- 指示智能体用某个特定的 JavaScript 框架/元框架(例如 Next.js、Nuxt、SvelteKit 等)构建这个应用
- 用规范一致性测试套件检验产出(防止智能体作弊)
- 从开发时间、消耗的 token 数、代码行数,以及 Lighthouse 中的性能和可访问性得分来评估结果
这个应用相当简单:你有一个宝可梦列表,包含它们的属性、招式以及来自各款游戏的数据。你可以搜索、筛选等等:

我得先说明一些前提:这不是一个完美的实验。这个应用只代表一种正模标本——它是一个相对偏内容型的网站,交互性很少,更接近维基百科而不是 Figma、GMail 之类。话虽如此,我总得选一个应用来构建,而这个应用至少在高层次上容易理解,也相对容易写规范和测试。
为了尽量公平,我让每个框架/元框架都使用该工具的官方 MCP 或 skills。我想避免被人指责“这个框架在训练数据里代表性不足,所以我们才做了自己的 MCP,我们期望人们使用它”等等。我也没有过度规定要用的确切架构(例如 SSR 还是 CSR),也没有要求应用必须尽可能高性能。我只是把规范交给高投入模式下的 Opus 5.5,并告诉它使用给定框架的最新版本。每个智能体都在沙箱中运行,且不知道它会在 Lighthouse 上接受性能和可访问性测试,不过它拿到了规范一致性测试套件。
注意,我也没有强制要求任何 CSS 方法论,只是要求避免使用 Tailwind、Bootstrap 或任何其他组件框架,并基于一组规范 token 进行设计,以强制实现一定的视觉一致性。我不想让这个测试完全围绕 CSS 展开,而是围绕 JS 框架、CSR 与 SSR、SPA 与 MPA 等展开。最终的应用是基于无障碍树而非原始 DOM 的一致性测试套件进行测试的,以避免强制要求任何特定的 HTML 结构——其中一个框架使用了依赖 shadow DOM 的 Web Components,因此这是必需的。
我从这个实验中学到的一件事是,我所测量的一切都非常具有随机性。LLM 是随机词生成器,所以有时你会掷出低点数,然后某个 agent 会跑偏到某个奇怪的思路上,浪费大量 token,而这并不是框架或其工具的过错。Lighthouse 本身也可能给出不一致的结果:最大内容绘制(LCP)可能判定某张图片是“最大”元素,然后在下一轮运行中又选择另一张。很难有一个完全公平的实验,但运行多次迭代确实有帮助,我也尽量对差异保持诚实。
把这些开场白都说完,我们来看结果。
结果
我将从多个维度来拆解:构建成本、质量和可维护性。对于每个框架/元框架,我取 5 次运行的中位数:换句话说,我在同一个框架中把同一个应用构建了 5 次。(是的,这就是如今用编码 agent 能做的这种疯狂事情。)
例外是 Angular,它两次未能创建出可用的应用,所以我改用了 3 次迭代。(原因见下文。)
成本
| 框架 | 成本($) |
|---|---|
| Hono+htmx | $2.67 (2.23–3.19) |
| Astro | $2.76 (2.16–3.63) |
| Nuxt | $2.82 (2.39–4.25) |
| Vanilla | $2.98 (2.24–3.37) |
| Next.js | $3.14 (2.58–4.13) |
| Qwik | $3.27 (2.35–3.80) |
| TanStack Start | $3.54 (3.11–3.96) |
| SolidStart | $3.79 (2.84–4.81) |
| React Router | $3.81 (2.82–3.97) |
| SvelteKit | $4.28 (3.59–4.65) |
| Gracile+Lit | $4.65 (3.92–5.07) |
| Angular | $4.97 (3.19–6.82) |
3-5 次运行的中位数。
请注意,这里直接引用的是 Claude Code 的 API 价格,并不实际代表我花了多少钱。我用的是 Max 套餐,所以这只是我每月 100 美元总支出的一部分。
还要注意方差:React Router 的中位数大约比 Hono+htmx 差 1 美元,但 React Router 最便宜的版本(2.82 美元)仍然比 Hono+htmx 最贵的版本(3.19 美元)便宜。不过,一些可靠的模式确实浮现了出来。
最明显的是,SvelteKit、Gracile+Lit 和 Angular 的构建成本高于其他框架。对于 SvelteKit,这可能是因为 agent 在每次运行中都阅读了 SvelteKit v3 迁移指南,很可能是因为它太新,不在 Opus 5.5 的训练数据中。Gracile 类似,agent 对该框架不太了解,要么克隆了 Gracile 仓库,要么从 node_modules 读取了其编译后的源码。对于 Angular,这似乎是总代码量的函数(见下文)。
我还应该提到,并非每个 agent 都使用了 MCP/skill/Claude Code 插件,也并非每个框架都有:
点击查看 AI 工具使用情况
| 框架 | 已安装 | agent 是否使用? |
|---|---|---|
| Angular | Angular MCP 和 Angular agent skills | 3/5 次运行使用了 MCP,4/5 次运行使用了 skill 工具,2/5 次使用了 skill 文件 |
| Astro | Astro MCP server | 2/5 次运行使用了 MCP |
| Gracile+Lit | 不适用 | 不适用 |
| Hono+htmx | Hono skills | 5/5 次运行使用了 skill 工具 |
| Next.js | next-devtools-mcp 和 next-dev-loop skill | 1/5 次运行使用了 skill 文件,2/5 次运行使用了 AGENTS.md |
| Nuxt | Nuxt MCP | 不适用 |
| Qwik | 不适用 | 不适用 |
| React Router | React Router skill | 5/5 次运行使用了 skill 工具,5/5 次运行使用了 skill 文件 |
| SolidStart | N/A | N/A |
| SvelteKit | Svelte Claude Code 插件 | 5/5 次运行中使用了 MCP |
| TanStack Start | N/A | 5 次运行中有 2 次使用了技能文件 |
| Vanilla | N/A | N/A |
例如,TanStack Start 的 agent 只在 5 次运行中的 2 次发现了它的技能文件(它们随项目的 node_modules 一起提供)。对于 Nuxt,agent 从未使用过 MCP。对于 Next.js,agent 只在 5 次运行中的 1 次读取了技能文件,并在 5 次运行中的 2 次读取了 AGENTS.md。结果范围很广,这又回到了 LLM 的随机性本质。不过,我仍然认为这是一个有效的实验,因为我怀疑很多 vibe-coder 不会用全大写字母冲着他们的 agent 喊叫去使用 MCP(而且很多人可能完全跳过了它)。
另外,如果你想知道我是怎么想出 Hono+htmx 的:基本上,我想以某种方式代表 htmx,而 Hono 似乎是个不错的搭配(即作为 Rails/Django 等的 JavaScript 替代品)。至于 Gracile+Lit,我想通过其标准框架(Lit)来代表 Web 组件,而且我只知道 Gracile 是一个元框架。如果有更好的候选者,那么它们将很适合后续实验。
对于“Vanilla”,请注意 agent 被告知使用“vanilla JavaScript 或 TypeScript 配合 Vite,不使用 UI 框架”。令人惊讶的是,它的表现不如几个框架,这似乎表明拥有某种框架总比没有好(或者至少比原始 Vite 好)。选择 Vite 是因为它支撑了许多框架,并代表了相当标准和通用的 JavaScript 基础设施。
总时间和 token 与成本紧密相关:
| 框架 | 时间(分钟) |
|---|---|
| Astro | 8.2 分钟(6.2–9.7) |
| Hono+htmx | 8.6 分钟(5.9–9.6) |
| Nuxt | 9.0 分钟(8.0–17.6) |
| Vanilla | 10.1 分钟(6.9–18.6) |
| TanStack Start | 10.3 分钟(8.3–13.9) |
| Qwik | 10.7 分钟(7.1–12.9) |
| SvelteKit | 10.8 分钟(8.6–13.8) |
| Next.js | 10.9 分钟(8.3–11.9) |
| SolidStart | 11.2 分钟(8.2–15.9) |
| Gracile+Lit | 12.1 分钟(10.8–12.9) |
| React Router | 12.5 分钟(8.4–13.6) |
| Angular | 17.2 分钟(9.8–23.7) |
从 agent 视角的挂钟分钟数。3-5 次运行的中位数。
| 框架 | Token(M) |
|---|---|
| Nuxt | 3.85 M(3.16–8.18) |
| Astro | 4.13 M(3.03–7.57) |
| Hono+htmx | 4.26 M(3.40–5.61) |
| Next.js | 4.99 M(3.54–8.82) |
| Vanilla | 5.00 M(3.44–6.32) |
| Qwik | 5.64 M(3.41–7.03) |
| TanStack Start | 7.07 M(5.85–7.99) |
| SolidStart | 7.13 M(5.02–10.07) |
| React Router | 7.58 M(4.55–8.18) |
| SvelteKit | 8.53 M(6.79–9.29) |
| Gracile+Lit | 9.04 M(7.36–11.10) |
| Angular | 9.76 M(4.92–14.94) |
缓存读取、未缓存输入和输出。3-5 次运行的中位数。
顺便说一下,这些 token 是由 Claude Code 报告的。时长只是总挂钟时间。
质量
当然,因为我认为结果很重要,所以我也用 Lighthouse 测试了这些 agent。作为软件开发者,我认为重要的是我们不仅要低成本地构建东西,还要保证性能和可访问性。
我首先应该说明关于我的基准测试的两个注意事项:
- 这个 Pokémon 应用很小,不能代表大型、复杂的真实世界应用。
- Lighthouse 和 Core Web Vitals 总体上对 SPA 有偏见,所以 MPA 框架在总阻塞时间(TBT)、JavaScript 字节数等指标上几乎必胜。我曾考虑加入一些应用内性能指标,但放弃了,因为这会增加基准测试的复杂性和变异性,而且这些指标是我自己发明的,而非行业标准。
为简单起见,我只报告首页的分数。还有其他页面,但它们的得分都相当相似,或者像 LCP 这样的指标波动很大,所以包含它们只会让事情复杂化。
简而言之:性能、可访问性和 CLS(累积布局偏移)的 Lighthouse 分数几乎饱和,且各框架之间差异不大:
| 框架 | CLS | 性能 | 可访问性 |
|---|---|---|---|
| Angular | 0.000 | 98 | 100 |
| Astro | 0.000 | 100 | 100 |
| Gracile+Lit | 0.000 | 100 | 100 |
| Hono+htmx | 0.000 | 100 | 100 |
| Next.js | 0.000 | 99 | 100 |
| Nuxt | 0.000 | 100 | 100 |
| Qwik | 0.000 | 100 | 100 |
| React Router | 0.000 | 100 | 100 |
| SolidStart | 0.000 | 100 | 100 |
| SvelteKit | 0.000 | 100 | 100 |
| TanStack Start | 0.000 | 99 | 100 |
| Vanilla | 0.111 | 97 | 100 |
因此,要得到一些更有趣的数字,我们需要看其他指标。最可靠的指标是传输的 JavaScript 字节数,这在很大程度上反映了客户端框架的大小:
| 框架 | JS (KiB) |
|---|---|
| Astro | 0 KiB (0–1) |
| Vanilla | 7 KiB (6–7) |
| Gracile+Lit | 8 KiB (1–13) |
| Hono+htmx | 18 KiB (18–18) |
| SvelteKit | 36 KiB (35–36) |
| SolidStart | 40 KiB (38–40) |
| Qwik | 40 KiB (40–41) |
| Nuxt | 84 KiB (83–85) |
| Angular | 93 KiB (81–102) |
| React Router | 115 KiB (115–119) |
| TanStack Start | 118 KiB (112–119) |
| Next.js | 142 KiB (136–143) |
由 Lighthouse 报告。3-5 个应用的中位数。
总阻塞时间(TBT)有趣之处在于它并非纯粹是 JavaScript 字节数的函数;有些应用在主线程上花费的时间比其他应用更多。尽管如此,这些数字都相当小:
| 框架 | TBT (ms) |
|---|---|
| Astro | 0 ms (0–0) |
| Gracile+Lit | 0 ms (0–0) |
| Hono+htmx | 0 ms (0–0) |
| SvelteKit | 0 ms (0–1) |
| Vanilla | 0 ms (0–0) |
| TanStack Start | 1 ms (0–36) |
| Qwik | 3 ms (0–30) |
| SolidStart | 11 ms (0–12) |
| React Router | 17 ms (0–52) |
| Nuxt | 40 ms (14–54) |
| Next.js | 71 ms (66–121) |
| Angular | 103 ms (93–123) |
根据 Lighthouse 报告,模拟移动端节流。每个应用取 5 次 Lighthouse 运行的中位数,然后再取 3-5 个应用的中位数。
最大内容绘制(LCP)由于上述指标本身的问题而高度可变。不过,正如预期,它总体上惩罚 SPA 而奖励 MPA:
| 框架 | LCP (ms) |
|---|---|
| Astro | 810 ms (792–1662) |
| Gracile+Lit | 956 ms (796–980) |
| Hono+htmx | 978 ms (977–1662) |
| Qwik | 979 ms (954–1278) |
| SvelteKit | 1130 ms (1129–1131) |
| SolidStart | 1204 ms (1203–1278) |
| Vanilla | 1667 ms (764–1676) |
| Nuxt | 1727 ms (1427–1844) |
| React Router | 1729 ms (1729–1770) |
| Angular | 1954 ms (1351–1955) |
| TanStack Start | 2028 ms (1805–2104) |
| Next.js | 2090 ms (1028–2187) |
根据 Lighthouse 报告,模拟移动端节流。每个应用取 5 次 Lighthouse 运行的中位数,然后再取 3-5 个应用的中位数。
请注意,Next.js 数值的巨大差异可以解释为:一个应用避免了图片懒加载,而另外四个应用没有。Astro 和 Vanilla 也有类似的波动,似乎是由优先级或 LCP 的怪癖引起的。
不过总的来说,这个指标倾向于奖励用 HTML 快速绘制的 MPA,而不是在客户端 JavaScript 运行后才绘制的 SPA(可能还包括多次受节流影响的往返请求来获取 Pokémon 数据),这并不令人意外。忽略方差,这里的 LCP 主要是 JavaScript 包大小的函数。(Qwik 在这里是一个显著的例外:从技术上讲,它是一个使用客户端路由的 SPA,但它的得分却处于 MPA 框架之列。)
但请注意,一些框架在更侧重于客户端还是服务器方面存在差异,甚至是否使用客户端框架也有所不同。例如,Gracile 代理在两次运行中甚至没有使用 Lit。对于“Vanilla”,代理拥有完全的自由度(除了被告知要使用 Vite),但 5 次都仍然选择构建 SPA。
SSR 与 CSR 也是 Angular 问题的一部分来源:在 2 次运行中,代理构建了一个拒绝非 localhost 的 SSR 应用(尽管说明中明确指出该应用将位于反向代理之后)。我认为这是未能遵守规范的有效失败,并决定不重新运行这 2 次失败的迭代。
可维护性
衡量可维护性的方法有很多(包括圈复杂度之类的高级方法),但我决定保持简单,报告一些久经考验的东西:代码总行数。
| 框架 | 代码行数 |
|---|---|
| Hono+htmx | 1370 (1264–1489) |
| Nuxt | 1415 (1382–1458) |
| Astro | 1461 (1379–1503) |
| Vanilla | 1492 (1437–1742) |
| Next.js | 1539 (1452–1627) |
| React Router | 1564 (1421–1621) |
| SvelteKit | 1611 (1503–1816) |
| SolidStart | 1630 (1528–1894) |
| TanStack Start | 1697 (1506–1771) |
| Gracile+Lit | 1717 (1412–1748) |
| Qwik | 1744 (1646–1827) |
| Angular | 2009 (2003–2051) |
非空源文件行数。3-5 个应用的中位数。
对我来说,这是比较有趣的图表之一。从某种意义上说,框架的全部意义就在于减少你需要维护的代码量。用 Rich Harris 令人难忘的话来说:
“框架不是组织代码的工具,而是组织思维的工具。”
– Rich Harris,“重新思考响应式”
在智能体时代,这一点更加凸显,因为代码规模最终会影响 token 数量,包括未来的编辑,这意味着它会影响随时间推移维护代码库的实际成本。这并不意味着简洁是唯一重要的价值,但看到 Hono+htmx 能用约 70% 的代码完成与 Angular 大致相同的事情(而且启动更快),确实很有意思。
结论
你可以通过多种方式改进这个基准测试。例如,你可以要求智能体使用 Service Workers 进行离线缓存,或者优化 Pokémon 图片文件,或者使用懒加载、fetchpriority、Speculation Rules API 等。尽管我知道很多这些性能技巧,但我故意没有引导智能体使用其中任何一个,因为这不是典型的 vibe-coder 会做的事情。
另一方面,整个基准测试可以通过将所有 HTML 文件预生成为静态资源来作弊(Pokémon 数量有限)。我在说明中明确禁止了这一点,并在测试框架中将其设为显式检查,因为我觉得这不符合基准测试的精神。(我的目标是模拟大量可能动态的数据,这比纯静态网站更具挑战性。)
你也可以通过运行更多迭代、使用不同的编码智能体、尝试不同的努力程度、使用不同的框架或框架的不同版本、尝试使用和不使用 MCPs/skills、在 Chromium 之外的更多浏览器中测试、构建不同类型的应用等方式来改进我运行基准测试的方式。我没有做这些事情主要是为了节省时间。不过,如果有人想自己运行基准测试或 fork 它,它在 GitHub 上,所以我鼓励你尽情发挥。我会很乐意在这里报告你的结果(以及你可能发现的任何错误或缺陷)。
我在这里的目标不是为“我是个 vibe-coder,我应该选择哪个框架?”编写终极基准测试。这篇文章的重点是证明在 AI 编码时代,仍然有可能基于证据而非直觉做出决策。基准测试本身很大程度上是 vibe-coded 的,尽管我确实审查了代码,所以如果有错误,请视为我的错误。我也非常乐意看到竞争对手框架尝试改进我在这里所做的工作。
我也不是试图诋毁任何特定框架。在运行基准测试之前,我不知道哪个会“赢”,尽管我确实怀疑 MPA 会胜过 SPA(我在整篇文章中都乐于承认这一点)。你当然可以通过将应用做成复杂仪表板、视频游戏、生产力应用等来编写一个更偏向 SPA 的基准测试。你也可以选择不同的指标:Lighthouse 和 Core Web Vitals 对 SPA 相当苛刻,所以你可以改为衡量应用内交互性。
我也确信框架作者本身可以改进他们的框架或 AI 工具,并在这个基准测试中做得更好。例如,我强烈怀疑当前这批 MCPs 和 skills 可能弊大于利。所以如果这篇文章是这些框架性能的最终定论,我会非常失望。我也确信还有新兴框架正迫不及待地想要在此类基准测试中表现出色。
如果别的没有,我希望这篇文章能启发你在这个奇异的 vibe coding 新时代里,对 JavaScript 框架的选择多加审视。你真的是基于证据来挑选技术栈的吗?还是只是沿用你已经熟悉的东西?这完全可以理解,尤其是如果你仍然亲自阅读、审查和调试代码的话。但如果你已经基本放手不管,那么对于当年像动物一样一行行敲代码时所做的选择,你究竟还有什么执念呢?如果你正在重新评估构建软件的整个方式,也许值得重新评估一下你在 npm install 之后输入的内容?只是说说而已。
就个人而言,除了我相信我们应该努力为用户打造尽可能最佳的体验之外,我对上面评估过的框架没有任何执念。我认识并尊重上述框架的许多作者,我也知道他们正为我所倡导的相同价值观而不懈努力。我只是希望推动整个社区朝着更好的方向前进,打造更好、更快、更易用的框架,为下一代 Web 应用提供动力。
附录
测试中使用的框架版本(注意 Qwik 在测试中途发布了一个补丁版本):
| 框架 | 版本 |
|---|---|
| Angular | @angular/core 22.2.3 |
| Astro | astro 7.4.2 |
| Gracile+Lit | @gracile/gracile 0.11.0, lit 3.3.3 |
| Hono+htmx | hono 4.13.14, htmx.org 2.0.11 |
| Next.js | next 16.4.3, react 19.3.1 |
| Nuxt | nuxt 4.6.2, vue 3.5.44 |
| Qwik | @builder.io/qwik-city 1.20.2(第 1 次迭代),1.20.3(第 2、3、4、5 次迭代),@builder.io/qwik 1.20.2(第 1 次迭代),1.20.3(第 2、3、4、5 次迭代) |
| React Router | react-router 8.5.0, react 19.3.1 |
| SolidStart | @solidjs/start 2.1.0, solid-js 1.9.17 |
| SvelteKit | @sveltejs/kit 3.1.0, svelte 5.57.2 |
| TanStack Start | @tanstack/react-start 1.169.4, react 19.3.1 |
| Vanilla | vite 8.3.5 |
你可以在 fediverse 上发表评论。
来源:Hacker News · nolanlawson.com