你大概率遇到过:页面上一行数字,你只是把它从 1 改成 2,结果整个页面卡了一下。这不能怪框架"笨"——框架根本不知道你改了哪个数字,它只知道"可能有东西变了"。为了不遗漏,它宁可多干点活。
核心概念速览 (TL;DR)
- 响应式 (Reactive):数据变化时,视图自动更新的机制。主流框架正从“轮询”转向“订阅制”,让数据自己通知依赖它的视图。
- 虚拟 DOM (Virtual DOM):内存中的 DOM 副本,用于对比前后差异,找出真正需要更新的节点,避免直接操作昂贵真实 DOM 的“全量重绘”。
- Signal:一种“谁订阅谁收通知”的响应式状态模型。数据源(Signal)变化时,只通知订阅它的视图,实现精准更新。Angular、Vue、Svelte 5 均采用此思路。
- 编译时优化 (Compile-time Optimization):在代码构建阶段(而非运行时)分析依赖关系,生成最优更新逻辑。Svelte 5 的 Runes 和 React Compiler 是典型代表。
- 过度渲染 (Over-rendering):框架因无法精确感知数据变化,导致渲染范围远大于实际需要更新的部分,是前端性能问题的核心根源。
一、现象:四个主流框架今年集体转型了
先说事实,四件事:
-
React 端出了一个叫 React Compiler 的东西,让你以后基本不用再手写 useMemo 这种优化。
-
Vue 推出了 Vapor Mode,一个可以完全跳过虚拟 DOM 的新渲染引擎,老项目还能渐进启用。
-
Svelte 5 换上了 Runes 语法,把“隐式响应式”改成了“显式响应式”。
-
Angular 21 起新项目默认开启 Zoneless,22 版连默认的变更检测策略都换了。
单看任何一件,都可以解释成"某个框架自己的升级"。但放在一起看,它们其实在回答同一个问题:
数据变了,框架到底该怎么知道该更新哪里?
以前,这个问题的答案多是“凑合”——框架不知道,就让开发者手动帮忙(useMemo 就是这么来的),或者框架多检查几遍(Angular 的 Zone.js 就是这么干的)。
2026 年,四个框架集体说:不凑合了。
一句话概括这场转型:开发者把“什么是响应式的”说清楚,编译器把“该怎么优化”做干净。 一个叫显式化,一个叫自动化。
四件事 → 同一个问题
二、原理:一切性能问题的根源是"过度渲染"
前端页面的本质是:状态(数据)→ 视图(DOM)。状态变了,视图要跟着变。这个“跟着变”的过程,就是渲染。
问题在于:DOM 操作很贵。改一个节点的属性、删一个节点、插一个节点,浏览器都要重新做布局、重绘,甚至会触发整个页面的回流。如果你改动频繁,这些操作叠加起来就是肉眼可见的卡顿。
所以框架们想了个聪明的办法:先别直接改 DOM,先在内存里模拟一份,改完之后对比一下,找出真正变了的地方,再去动真实的 DOM。
这份"内存里的模拟",就是虚拟 DOM。它和真实 DOM 的关系,可以类比成装修图纸和实体墙面——设计师先在图纸上改,改完对照图纸确定哪面墙要拆、哪面墙要留,最后才让工人上手。比起把墙全砸了重砌,这省事太多。
但这里有个隐藏成本:对比(diff)本身也要花时间。 而且,框架必须等数据变了之后,才知道要重新跑一遍对比。那么问题来了——
框架怎么知道数据变了?
这就是"响应式"(Reactive)要回答的问题。业界常用的类比是订阅制 vs 轮询:
|
轮询 |
每隔一秒问一遍"你变了吗?" |
简单粗暴,但浪费 |
|
订阅制 |
数据自己带通知功能,谁关注它,它变了就通知谁 |
精准,但要靠"订阅"机制支撑 |
主流框架的响应式,本质都在往"订阅制"靠。但做到什么程度,差别巨大。
订阅制 vs 轮询
最原始的方案是:框架根本不知道哪个数据变了,它只知道"我的代码跑了一下,可能变了"。于是它选择——把整个组件,甚至整棵组件树重新渲染一遍。渲染前跑一次 diff,把没变的过滤掉。
这就是过度渲染的真相:框架不信任自己知道"变了什么",于是宁可渲染一大片,再用 diff 兜底。 表现就是你在开头看到的那个卡顿——改一个数字,整个页面像被重新装修。
手动 useMemo 就是在这个背景下出现的:开发者告诉框架"这个计算结果没变,你不用重算"。它是把"圈范围"的活儿,从框架手里接过来自己干。
到这里,你可以把"显式化"理解成:把"圈范围"的活儿说清楚。 四个框架接下来的分歧,本质是——谁来做这件事,什么时候做。
虚拟 DOM 的三段流程
三、路线一:编译时动手 —— Svelte 5 的 Runes
Svelte 的选择最彻底:别等运行时猜了,写代码的时候就把一切都定死。
先看 Svelte 4 时代它怎么干活。它的做法很激进——编译时就把模板和逻辑翻译成最朴素的 DOM 操作代码,连虚拟 DOM 都不要。一个数据变了,它直接生成对应的原生语句去改那一个节点。
听起来已经很好了?但它有个遗留问题:响应式是隐式的。
在 Svelte 4 里,你在组件顶层写一个 let count = 0,它就自动是响应式的。你不需要写任何"订阅"或"声明"——框架自己从"这行代码出现在组件里"推断出它该是响应式的。
听起来很方便。但代价是:这个"推断"规则很脆。 变量放在函数里、解构出来、跨文件传递……这些场景下,框架猜不准它到底该不该响应式。开发者心里也没底——到底哪些变量会触发更新?规则是隐形的,只能靠记。
Svelte 5 的 Runes 就是来改这个的。Runes 是以 $ 开头的一组符号,比如:
<script>
let count = $state(0); // 显式声明:这是响应式状态
let doubled = $derived(count * 2); // 显式声明:这个值由 count 派生
</script>
区别在哪?核心在"推断"和"识别"这两个词上。
Svelte 4 靠推断:编译器扫描组件,发现顶层声明了变量,就默认它是响应式的,为它生成更新逻辑。规则本身简单,但推断是有歧义的——变量一旦进了函数、被解构、跨文件传递,编译器就分不清它到底该不该响应式。歧义意味着编译器要么漏判(该更新不更新),要么为了保险过度生成更新代码。
Svelte 5 改成识别:只有被 $state() 明确包裹的变量,编译器才认定它是响应式状态,为它建立更新依赖;没包裹的,就是普通变量,编译器直接当静态值处理。规则从"按位置推断"变成"按标记识别",编译器看到什么就处理什么,没有歧义。
所以 $state 不是新的 JS 语法,它是 Runes 这套标记体系里最核心的一个:声明"这个变量是响应式的"。 想要响应式,就包一层 $state();不想要,就不包。从"框架猜"变成"你说"。
$derived 也是同样的哲学:过去 Svelte 里用 $: 开头的语句来表达"这行代码依赖那个变量",语义比较模糊——它既可能是纯计算,也可能是有副作用的操作,编译器分不清。现在拆成两个:$derived 专门做纯计算,$effect 专门做副作用。语义清晰了,编译器就能生成更精确的更新代码。
把"隐式"改成"显式",付出的代价是每次写状态多敲几个 $。换来的是:编译器和开发者,都对"哪里是响应式的"心知肚明,不再猜。
这也正是 Svelte 5 能保持"编译时细粒度"的关键——因为信息在编译期就齐了,运行时根本不需要 diff,不需要虚拟 DOM。每个数据变更,更新范围精确到那一行文本。
Runes 核心符号代码示例
下面通过三个简短的组件示例,展示 $state、$derived 和 $effect 的基本用法和更新效果。
1. $state:声明响应式状态
<script>
// 声明一个响应式状态
let count = $state(0);
function increment() {
count++; // 直接修改,视图自动更新
}
</script>
<button on:click={increment}>
点击了 {count} 次
</button>
说明:$state(0) 将 count 声明为响应式状态。当 increment 函数修改 count 时,按钮上的文本会自动更新。编译器知道 count 是响应式的,因此会生成精确更新该文本节点的代码。
2. $derived:声明派生值
<script>
let price = $state(100);
let quantity = $state(1);
// 声明一个派生值,当 price 或 quantity 变化时自动重新计算
let total = $derived(price * quantity);
function applyDiscount() {
price = price * 0.9; // 修改 price,total 会自动更新
}
</script>
<div>
<p>单价:{price} 元</p>
<p>数量:{quantity}</p>
<p><strong>总价:{total} 元</strong></p>
<button on:click={applyDiscount}>打九折</button>
</div>
说明:$derived(price * quantity) 创建了一个派生值 total,它依赖 price 和 quantity。当任一依赖项变化时,total 会自动重新计算,并且所有使用 total 的视图部分也会更新。编译器会追踪依赖关系,只更新必要的部分。
3. $effect:执行副作用
<script>
let searchQuery = $state('');
let searchResults = $state([]);
// 当 searchQuery 变化时,自动执行副作用(例如发起搜索)
$effect(() => {
if (searchQuery.trim() === '') {
searchResults = [];
return;
}
// 模拟异步搜索
setTimeout(() => {
searchResults = [`结果1: ${searchQuery}`, `结果2: ${searchQuery}`];
}, 300);
});
</script>
<input type="text" bind:value={searchQuery} placeholder="输入搜索词" />
<ul>
{#each searchResults as result}
<li>{result}</li>
{/each}
</ul>
说明:$effect 用于执行副作用(如数据获取、DOM 操作、日志记录)。当 searchQuery 变化时,$effect 内的函数会自动重新运行。与 $derived 不同,$effect 不返回一个值,而是用于执行操作。编译器会确保副作用在依赖变化时运行,并在组件销毁时清理。
一句话:Svelte 选了"编译时"这个时间点动手,把响应式的真相写死在编译结果里。
Svelte 推断 → 识别
四、路线二:运行时重构 —— Angular 去掉 Zone.js,拥抱 Signal
Angular 走的是另一条路:不动编译策略,把运行时的"检测机制"整个换掉。
先看它过去的问题。Angular 从 2.0 时代就带了一个叫 Zone.js 的东西。它干的事很粗暴:给浏览器几乎所有异步 API 打补丁——setTimeout、Promise、addEventListener、fetch……凡是异步的,它都劫持一遍。
劫持来干嘛?为了"检测变化"。Angular 的旧机制是:只要任何异步操作完成(定时器响了、请求回来了、用户点了一下),它就认为"数据可能变了",然后把整棵组件树跑一遍变更检测,挨个问每个组件"你有没有变?"——哪怕那个异步操作跟任何数据都无关。
打个比方:一栋楼装了烟雾报警器,但每个报警器都分辨不出是有人在 smoking 还是着火。于是只要闻到一点烟味,就全楼广播。安全是安全了,但一天到晚吵个不停。
这就是 Zone.js 被人诟病的点:它没有"数据到底变没变"的信息,只能靠"可能有异步"来触发全树检查。 组件树大了,每次广播的成本就很可观。而且 Zone.js 本身也是个负担——它要打包进产物,还偶尔跟某些异步库打架。
Angular 的解法是:把 Zone.js 这个"全楼广播"机制拿掉,换成一个精确的模型——Signal。
Signal 是什么?一种"谁订阅谁收通知"的状态模型。一个 Signal 就是一个可以读、可以写的值,读它的人会"订阅"它,它一变,就只通知那些订阅者。数据源主动通知,而不是全楼广播。
这就是我在原理部分说的订阅制——Angular 终于从轮询+广播,转向了真正的订阅制。
Zoneless 这个名字,直译就是"没有 Zone"。去掉 Zone.js 之后,Angular 靠什么触发更新?答案很干净:只有 Signal 变了、模板事件触发了、或者你显式调用 markForCheck,才会去更新。 默认不检查,除非你说要检查。
Angular 21 起新项目默认就是 Zoneless,22 版更是把 OnPush(按需检查)设成了默认策略——"默认不检查"成了默认行为。这是个很本质的转向:Angular 从"我帮你兜底"变成了"我信任你,你把响应式的边界说清楚"。
一句话:Angular 选了"运行时"这个时间点动手,把全楼广播换成了精确通知。
Zone.js 广播 vs Signal 通知
五、路线三:不换引擎,让编译器接活 —— React Compiler 与 Vue Vapor
Svelte 和 Angular 的共性,是它们都能相对自由地改自己的内核——Svelte 本来就没多少历史包袱,Angular 的公司背景给了它大刀阔斧的底气。
React 和 Vue 不一样。它们俩的生态太大、历史太长,重写内核等于让全世界的存量项目重来一遍——这不现实。于是它们选了第三条路:保留引擎,但让编译器把以前开发者手动的活儿接过去。
5.1 React Compiler:把 useMemo 变成编译器的活
React 的旧方案,你一定不陌生:组件数据一变,整个组件重新执行一遍,跑 diff,找出真正变化的 DOM 再更新。为了不白干,开发者手动圈范围——useMemo 缓存计算结果,useCallback 稳定回调引用,React.memo 阻止不必要的子组件重渲染。
这套手动挡机制的问题在原理部分说过了:优化成了开发者的认知负担。 每个人都要背"什么时候该用 useMemo、依赖数组怎么写才不踩坑",写错了性能反而更差——漏圈范围会卡,多圈范围引用会错。
React Compiler 干的事,就是把这套手动挡变成自动挡。它在构建时分析你的组件代码,自动识别"这个计算没变、不用重算",然后自动插入记忆化代码。你写的代码还是那么朴素,编译器在背后帮你优化。
对存量项目,React 也给了渐进路径:你可以在组件顶部加一行 'use memo' 注释,告诉编译器"这个组件你看着办"——先小范围试,再逐步铺开。
当然,自动挡也有自动挡的脾气。它只对"遵守 React 规则的组件"有效——你要是违反了规则(比如渲染期间改了 state),编译器就帮不上忙,只能跳过。生态里也有个别第三方库因为依赖"引用相等"的约定,跟编译器相处得不太融洽。这点我不替你粉饰。
5.2 Vue Vapor:跳过虚拟 DOM,直接操作真实 DOM
Vue 走的路跟 React 很像但技术选择不同——它没动响应式的心智模型,而是把虚拟 DOM 这一层整个跳过了。
Vue 的老方案跟 React 一样:虚拟 DOM + diff。它当然也很好,但 Vapor Mode 的意思是:这次,连"图纸对比"这一步都省了。
Vapor 的编译器会把模板直接翻译成对真实 DOM 的操作代码。以前是"内存里模拟一份 → 对比 → 改真实 DOM",现在是"编译期就把该更新哪个节点的逻辑写死,运行时数据一变,直接改那一个节点"。虚拟 DOM、diff、patch,这些老朋友在 Vapor 里都不存在了。
类比一下:装修图纸还在,但老师傅看了太多次图纸,已经把每面墙的尺寸记在脑子里——直接上手砌,不用每次都对图。
Vapor 厉害的地方在于渐进哲学:它不是 Vite 3、Vue 4 那种"逼你重写"的重构,而是 Vue 3.6 里可选的一个新引擎。你可以在单个组件上开 Vapor(<script setup vapor>),也可以整项目开,还能和旧组件混着用。老项目想升级,不用推倒重来。
说到这,还有一条暗线值得点出来:Vue 这次连自己的响应式内核都换了——新内核叫 alien-signals,采用的正是 Signal 那套"订阅制"思路,性能比旧内核高了一个数量级。
你数一下就会发现:四个主流框架里,有三个(Vue、Svelte、Angular)的底层都在用 Signal 的思路。只有 React 坚持"组件重渲染 + 编译器自动化"这条路。
这不是谁对谁错,是生态包袱不同。React 的生态太大,它的解法是让编译器适配现有的心智模型;而其他三家,更愿意直接改心智模型。谁更适合你,没有标准答案——这正是下一节要说的。
三路线 → 双轴收束
六、选型:你的项目该关注什么
看完四条路线,最没用的结论是"哪个框架快"。因为选框架选的是"团队怎么想问题",不是基准测试谁快。
我把四种心智模型粗分一下,你自己对号入座:
|
React |
组件级重渲染 + 编译器自动化 |
团队熟悉 React、项目已大、不想动心智模型 |
|
Svelte / Vue Vapor |
编译时精确 |
新项目、愿意用"多写几个符号换精确" |
|
Angular |
Signal-first 显式订阅 |
大型团队、强规范约束、异步逻辑复杂 |
我的个人判断:大多数团队选框架,根本不会因为这几条路线而换家。 现实是——你已经在某个框架里了,迁移成本远大于收益。所以对存量项目,重点是看自家框架的渐进路径:React 项目去了解 Compiler 怎么开、Vue 项目去了解 Vapor 怎么渐进启用、Angular 项目去了解 zoneless 迁移。它们都设计成了"不用重写"。
三种心智模型选型导图
心智模型与性能开销对比
为了更直观地理解三种主流心智模型在实践中的差异,下面从更新粒度、运行时开销和心智负担三个维度进行对比:
|
组件重渲染 + 虚拟 DOM (React 传统模式) |
组件级:数据变化触发整个组件重新执行,通过虚拟 DOM diff 找出最小变更 |
较高:需要维护虚拟 DOM 树,执行 diff 算法,存在过度渲染风险 |
中等:开发者需手动优化(useMemo、useCallback、React.memo)以避免性能问题 |
|
Signal + 编译时指令 (Vue Vapor / Angular Signal) |
细粒度:Signal 变化仅通知订阅它的视图部分,Vapor 编译时生成精确 DOM 指令 |
较低:跳过虚拟 DOM diff,直接操作真实 DOM,Signal 订阅制减少无效检查 |
较低:响应式边界清晰(ref/computed/watch),但需理解 Signal 订阅关系 |
|
编译时完全精确 (Svelte 5 Runes) |
最细粒度:编译时分析 Runes 标记,为每个状态变化生成精确的 DOM 更新代码 |
最低:无虚拟 DOM,无运行时 diff,更新路径最短,打包体积最小 |
中等偏高:需显式标记响应式边界($state/$derived/$effect),但规则明确无歧义 |
核心差异总结:
- React(编译器优化后):保留组件重渲染心智模型,但将优化工作交给编译器,减少手动记忆化负担。
- Vue/Angular(Signal 路线):转向细粒度订阅,编译时生成高效更新指令,平衡了心智清晰度与运行时性能。
- Svelte(编译时精确):将响应式关系完全前置到编译期,换取最优运行时性能,但要求开发者显式标记。
对新项目,我的建议很朴素:别只看谁新、谁快,看看你们团队更习惯哪种"想问题的方式"。习惯组件重渲染的就选 React,喜欢"写的代码就是最终逻辑"的试试 Svelte,需要强规范和约束的选 Angular,想站在"渐进演进"中间位置的选 Vue。
没有哪个答案天生正确,但理解了它们的底层选择,你至少能做出有依据的选择,而不是跟风。
实战对比:三框架计数器组件示例
为了更直观地展示不同框架的心智模型差异,下面用 React、Vue 和 Svelte 分别实现一个功能相同的计数器组件,包含状态、派生值和副作用(日志),并附上各自的更新机制说明。
React(React Compiler 时代)
import { useState, useEffect } from 'react';
function Counter() {
// 状态声明
const [count, setCount] = useState(0);
// 派生值(React Compiler 会自动优化)
const doubled = count * 2;
// 副作用:每次 count 变化时打印日志
useEffect(() => {
console.log(`Count changed to: ${count}`);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
更新机制说明:在 React Compiler 之前,开发者需要手动使用 useMemo 来缓存 doubled 计算,用 useCallback 稳定回调。React Compiler 会在编译时分析组件,自动识别 doubled 只依赖 count,并插入记忆化代码。组件重渲染时,如果 count 未变,doubled 不会重新计算。副作用通过 useEffect 声明依赖数组,React 在依赖变化时重新执行。
Vue 3.6(Vapor Mode + Alien Signals)
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
// 状态声明(使用 ref,底层是 Signal)
const count = ref(0);
// 派生值(使用 computed)
const doubled = computed(() => count.value * 2);
// 副作用:监听 count 变化
watch(count, (newVal) => {
console.log(`Count changed to: ${newVal}`);
});
function increment() {
count.value++;
}
</script>
更新机制说明:Vue 3.6 的响应式内核已升级为 Alien Signals(Signal 模型)。ref 创建一个 Signal,computed 创建依赖 Signal 的派生值。当 count.value 变化时,只有依赖它的 doubled 和模板中用到 count 的部分会更新。Vapor Mode 下,编译器将模板直接编译为操作真实 DOM 的指令,跳过虚拟 DOM diff。副作用通过 watch 显式声明依赖。
Svelte 5(Runes 语法)
<script>
// 状态声明(显式标记)
let count = $state(0);
// 派生值(显式声明依赖)
let doubled = $derived(count * 2);
// 副作用:当 count 变化时执行
$effect(() => {
console.log(`Count changed to: ${count}`);
});
function increment() {
count++;
}
</script>
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button on:click={increment}>Increment</button>
</div>
更新机制说明:Svelte 5 的 Runes 语法要求开发者显式标记响应式边界。$state 声明响应式状态,$derived 声明纯计算派生值,$effect 声明副作用。编译器在构建时分析这些标记,生成精确的更新代码。运行时没有虚拟 DOM diff,数据变化直接触发对应 DOM 节点的更新。所有响应式关系在编译期就已确定,更新路径最短。
对比总结
|
React |
useState |
普通计算(编译器自动优化) |
useEffect |
组件级重渲染 + 编译器自动记忆化 |
|
Vue |
ref(Signal) |
computed |
watch |
Signal 订阅 + 编译时生成 DOM 指令(Vapor) |
|
Svelte |
$state |
$derived |
$effect |
编译时分析 Runes,生成精确 DOM 更新 |
三个框架都朝着「显式化」和「自动化」演进:React 让编译器接管记忆化,Vue 换用 Signal 内核并跳过虚拟 DOM,Svelte 要求开发者用 Runes 显式标记响应式边界。选择哪种,取决于团队更适应「声明式重渲染+编译器优化」、「Signal+编译时指令」还是「编译时完全精确」的心智模型。
七、升华:这不是框架战争,是生态的自我修正
你有没有想过,为什么四个框架会在同一时间窗口集体转型?它们之间可没有串通过。合理的解释是:手动优化的时代,到顶了。
过去几年,前端性能优化的主流叙事是"开发者手动圈范围"——useMemo、shouldComponentUpdate、性能预算、分包加载……这套玩法有一个上限:它消耗的是人的注意力,而注意力是稀缺的。项目越大,这套玩法的维护成本越高,收益越边际。
于是四个框架,几乎不约而同地往两个方向走:让开发者把边界说清楚(显式化),让机器把优化做干净(自动化)。 Svelte 让你显式写 $state,Angular 让你显式订阅 Signal,Vue 换内核做细粒度,React 让编译器自动记忆化——名字各不相同,动作惊人一致。
这件事对开发者个人的意义,比"我该不该换框架"更大:
学一个框架,本质是学它"怎么回答该更新哪里"这道题。 以前我们学框架,学的是 API、是组件、是生命周期。但这些都会过时。真正沉淀下来的,是框架对那道"总根源问题"的答案——而这个答案,恰恰是 2026 年四个框架集体给出的:不猜了,说清楚;不手动,交给编译器。
把这道题的四种答案装进脑子里,下次无论哪个新框架出来,你都能一眼看穿它站在哪条路线上、抄了谁的思想。这比追着版本号跑,有用得多。
收束图
八、结语
回到开头那个问题:改一个数字,页面为什么会卡?
因为框架以前不知道你改了哪个数字,它只能多干活。而 2026 年四个框架在做的事,是让这个"不知道"越来越少——要么你告诉它(显式化),要么它自己算出来(自动化)。
纵观 React、Vue、Svelte 和 Angular 四大框架的转型,其核心共性清晰可见:从过去的“框架猜”转向“开发者说清楚”。无论是 Svelte 5 的 Runes、Angular 的 Signal、Vue 的 Alien Signals,还是 React Compiler 的自动记忆化,都在推动开发者更明确地声明响应式边界,同时让编译器或运行时更精准地执行更新。
对于开发者而言,这意味着:
- 关注渐进升级:如果你正在使用 React,可以逐步启用 React Compiler;Vue 项目可尝试 Vapor Mode;Angular 项目则迁移到 Zoneless + Signal。无需重写整个项目。
- 理解底层心智模型:掌握“显式化”与“自动化”的双轴思路,无论未来出现什么新框架,你都能快速理解其设计哲学。
- 以团队习惯为先:选型时优先考虑团队熟悉的心智模型(组件重渲染、Signal 订阅或编译时精确),而非盲目追求性能基准。
这场转型不是框架战争,而是整个前端生态在性能与开发体验上的集体进化。拥抱变化,但不必焦虑——关键在于理解原理,然后选择最适合你当前项目的渐进路径。
术语索引
|
响应式 (Reactive) |
数据变化时,视图自动更新的机制。核心是解决“框架如何知道数据变了”的问题,主流方向是从“轮询”转向“订阅制”。 |
二、原理:一切性能问题的根源是“过度渲染” |
|
虚拟 DOM (Virtual DOM) |
内存中的 DOM 模拟副本。框架通过对比虚拟 DOM 的前后差异(diff),找出最小变更集再更新真实 DOM,以避免昂贵的直接操作。 |
二、原理:一切性能问题的根源是“过度渲染” |
|
Signal |
一种“谁订阅谁收通知”的响应式状态模型。一个 Signal 是一个可读写的值,其变化会精准通知所有订阅者,而非广播式检查。是 Angular、Vue、Svelte 5 的底层思路。 |
四、路线二:运行时重构 —— Angular 去掉 Zone.js,拥抱 Signal |
|
编译时优化 (Compile-time Optimization) |
在代码构建阶段(编译时)分析数据依赖与视图关系,生成精确的更新逻辑,从而在运行时避免不必要的计算和对比。Svelte 5 的 Runes 和 React Compiler 是典型实践。 |
三、路线一:编译时动手 —— Svelte 5 的 Runes 五、路线三:不换引擎,让编译器接活 —— React Compiler 与 Vue Vapor |
|
过度渲染 (Over-rendering) |
由于框架无法精确感知数据变化,导致渲染范围远大于实际需要更新的部分,造成性能浪费。这是传统虚拟 DOM + diff 方案的核心性能瓶颈。 |
二、原理:一切性能问题的根源是“过度渲染” |
参考资料
React 官方文档 — React Compiler 介绍(自动记忆化 / Rules of React / 增量采用) React Compiler – React
Svelte 官方 v5 迁移指南 — Runes 动机($state / $derived / $effect) Svelte 5 migration guide • Svelte Docs
Angular 官方 Zoneless 指南 — Zone.js 移除 / 触发源 / 迁移 Zoneless • Angular
stackblitz/alien-signals — push-pull Signal 模型与性能约束 https://github.com/stackblitz/alien-signals
vuejs/core PR #12349 — alien-signals 移植回 Vue 3.6 https://github.com/vuejs/core/pull/12349
js-framework-benchmark — 跨框架性能基准(加权几何均值) https://github.com/krausest/js-framework-benchmark
talkingtech — React Compiler 生产实践报告(2026-06)
kanopylabs — 三框架真实应用基准(Svelte / Vue Vapor / React)
sharpskill — Vue 3.6 Vapor 与 Alien Signals 技术解析(2026-06)
sharpskill — Angular Zoneless 时间线与收益分析(2026-04)
掘金析数塔 — 2026 前端框架深度解析(三轴收敛框架)(2026-07)
CSDN — Vue Vapor 详解 / Svelte 5 详解(2026-07)


