从一段让我崩溃的代码说起
去年接手一个老项目,用 jQuery 写的。有个表单联动逻辑:选省份 → 加载城市 → 选城市 → 加载区县 → 选完后联动刷新列表。当时代码是这样的:
// 这就是让我崩溃的代码,省市区三级联动
$('#province').change(function() {
var pid = $(this).val();
$.ajax({
url: '/api/cities?pid=' + pid,
success: function(data) {
$('#city').empty();
data.forEach(function(c) {
$('#city').append('<option value="' + c.id + '">' + c.name + '</option>');
});
$('#city').trigger('change'); // 手动触发下一个联动
}
});
});
// 下面还有区县、列表刷新、分页重置……
我就想知道一件事:数据变了,DOM 为什么不能自己跟着变? 我需要手动管理 DOM 增删、事件绑定、数据回填、还要小心内存泄漏。这不是写业务,这是在写 DOM 操作说明书。
后来我对比了三个框架:Svelte 5.16.0、Vue 3.5.13、React 19.0.0。结论先说:Svelte 的编译时方案,跟 Vue/React 的运行时方案,走的根本不是一条路。 这篇文章不吹不黑,纯从实战角度拆解:怎么选、怎么写、有什么坑。
三种方案的设计思路差异
| 维度 | Svelte 5.16.0 | Vue 3.5.13 | React 19.0.0 |
|---|---|---|---|
| 核心思路 | 编译时框架,没有 VDOM | 运行时 + 虚拟DOM | 运行时 + 虚拟DOM |
| 响应式实现 | 编译器生成依赖代理,变量赋值直接更新DOM | Proxy 代理对象,get 收集依赖,set 触发更新 | Hooks 状态机制,setState 触发re-render |
| 运行时体积 | 几乎0(编译产物是原生JS) | 约 61.4 KB(含 runtime) | 约 161.1 KB(含 react-dom) |
| 学习曲线 | 低,接近原生HTML/CSS/JS | 中,需要理解响应式API和生命周期 | 中高,需要理解hooks、闭包、渲染时机 |
| 性能瓶颈 | 无运行时开销,更新粒度精确到节点 | VDOM diff 开销,Proxy代理监听有开销 | re-render 默认全量,需要手动优化 |
一句话总结:Vue 和 React 是在运行时把工作做掉,Svelte 是在编译期把工作做掉。 Svelte 没有虚拟 DOM,它直接把「数据变了 → 哪个 DOM 节点要更新」这件事在编译阶段就翻译成原生 JS 了。
Svelte 的编译原理(看懂了就理解了一切)
写一段最简单的 Svelte 代码:
<!-- App.svelte -->
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
count is {count}
</button>
看 Svelte 编译产物(简化后)长什么样:
// 编译产物(高度简化)
let count = 0;
function increment() {
count += 1;
// 编译生成的精确更新语句
if (count_text) count_text.data = "count is " + count;
}
let button = document.createElement("button");
let count_text = document.createTextNode("count is " + count);
button.appendChild(count_text);
button.addEventListener("click", increment);
document.body.appendChild(button);
看到没?没有虚拟 DOM diff,没有 Proxy 代理,没有 reconciler。 Svelte 直接生成了一条「count 变了,就把这个文本节点更新」的原生操作。这就是为什么它跑得快、打得小。
而 Vue 拿到 .vue 文件要做的事:
// Vue 3 的工作方式
const state = reactive({ count: 0 }); // 创建 Proxy 代理
effect(() => {
// 渲染函数内部收集依赖
renderTemplate(state);
});
function increment() {
state.count++; // 触发 Proxy set → 触发 effect → 重新渲染
}
React 的工作方式:
// React 的工作方式
function App() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>count is {count}</button>;
}
// 每个 setCount 都会触发 App 函数重新执行
// 返回新的 React 元素 → diff → 更新真实 DOM
三个框架的核心差异就这么大。
Vue 的响应式是运行时拦截对象读写;
React 的响应式是状态触发整组件重跑;
Svelte 的响应式是编译器帮你精确生成更新代码。
实战对比:同一套功能,三版代码
用一个完整 TodoList 举例。同样功能:输入内容 → 添加任务 → 点击完成 → 双击删除。我分别用三个框架写了一遍,下面给完整可跑代码。
Svelte 版本(推荐直接跑起来看)
<!-- Todo.svelte -->
<script>
let todos = [];
let inputValue = "";
function addTodo() {
if (!inputValue.trim()) return;
todos = [...todos, { id: crypto.randomUUID(), text: inputValue, done: false }];
inputValue = "";
}
function toggleTodo(id) {
todos = todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
function removeTodo(id) {
todos = todos.filter(t => t.id !== id);
}
</script>
<input
bind:value={inputValue}
placeholder="输入任务,回车添加"
onkeydown={(e) => e.key === "Enter" && addTodo()}
/>
<button onclick={addTodo}>添加</button>
<ul>
{#each todos as todo (todo.id)}
<li
class:done={todo.done}
onclick={() => toggleTodo(todo.id)}
oncontextmenu={(e) => { e.preventDefault(); removeTodo(todo.id); }}
>
{todo.text}
<button onclick={(e) => { e.stopPropagation(); removeTodo(todo.id); }}>删除</button>
</li>
{/each}
</ul>
<style>
.done { text-decoration: line-through; color: #aaa; }
li { cursor: pointer; padding: 8px; border-bottom: 1px solid #eee; }
</style>
Vue 3 版本
<!-- Todo.vue -->
<template>
<input v-model="inputValue" placeholder="输入任务,回车添加"
@keydown.enter="addTodo" />
<button @click="addTodo">添加</button>
<ul>
<li v-for="todo in todos" :key="todo.id"
:class="{ done: todo.done }"
@click="toggleTodo(todo.id)"
@contextmenu.prevent="removeTodo(todo.id)">
{{ todo.text }}
<button @click.stop="removeTodo(todo.id)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from "vue";
const todos = ref([]);
const inputValue = ref("");
function addTodo() {
if (!inputValue.value.trim()) return;
todos.value.push({
id: crypto.randomUUID(),
text: inputValue.value,
done: false
});
inputValue.value = "";
}
function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<style scoped>
.done { text-decoration: line-through; color: #aaa; }
li { cursor: pointer; padding: 8px; border-bottom: 1px solid #eee; }
</style>
React 版本
// Todo.jsx
import { useState } from "react";
export default function Todo() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState("");
function addTodo() {
if (!inputValue.trim()) return;
setTodos([...todos, {
id: crypto.randomUUID(),
text: inputValue,
done: false
}]);
setInputValue("");
}
function toggleTodo(id) {
setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
}
function removeTodo(id) {
setTodos(todos.filter(t => t.id !== id));
}
return (
<>
<input
value={inputValue}
placeholder="输入任务,回车添加"
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addTodo()}
/>
<button onClick={addTodo}>添加</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.done ? "line-through" : "none" }}
onClick={() => toggleTodo(todo.id)}
onContextMenu={(e) => { e.preventDefault(); removeTodo(todo.id); }}
>
{todo.text}
<button onClick={(e) => { e.stopPropagation(); removeTodo(todo.id); }}>删除</button>
</li>
))}
</ul>
</>
);
}
看代码感受一下:
Svelte 的 bind:value 比 Vue 的 v-model 更接近原生,不用 .value;
Svelte 的 onclick 就是 HTML 的事件名,不像 React 的 onClick 要驼峰;
Svelte 没有依赖数组(不用 useEffect 的 deps),没有 useMemo,没有 useCallback。
如果你写过 React,Svelte 的代码让你像是回到了原生 JS 的世界,只是这个原生 JS 自动帮你处理了 DOM 更新。
效果数据:真实的性能对比
测试环境:MacBook Pro M1 Pro 16GB,Chrome 131.0.6778.69,Node v22.11.0。
测试方式:同样渲染 10,000 条 Todo 列表,分别测「初始渲染」「点击更新 1 条(共10轮)」「全部删除」。用 Performance API 记录,跑 5 次取中位数。
| 指标 | Svelte 5.16.0 | Vue 3.5.13 | React 19.0.0 |
|---|---|---|---|
| 包体积(gzip压缩前) | 18.2 KB | 61.4 KB | 161.1 KB |
| 包体积(gzip压缩后) | 7.8 KB | 24.5 KB | 48.6 KB |
| 首次渲染 10,000 条 | 1,483 ms | 2,271 ms | 2,908 ms |
| 单条更新(中位数) | 1.2 ms | 6.8 ms | 8.5 ms |
| 全量删除 10,000 条 | 903 ms | 1,742 ms | 2,353 ms |
| 运行中内存占用(峰值) | 52 MB | 84 MB | 96 MB |
注意:这是我自己测试的一组数据,不是行业标准基准。你的场景不同,数字可能不同。但「Svelte 比 Vue/React 体积小 3~8 倍、更新速度快 5~7 倍」这个量级,是能稳定复现的。
补充一个更实务的对比:用 WebPageTest 测一个包含 500 条 DOM 节点 + 20 个事件绑定的页面:
- Svelte 产物:1 个 JS 文件 18 KB,无框架运行时注入;
- Vue 产物:1 个 JS 文件 61 KB + runtime + compiler(如用 runtime-only 可小点但仍有额外开销);
- React 产物:2 个 JS 文件 161 KB + react-dom,需要额外 polyfill(IE11 时代的事儿,现在好了点)。
什么时候该选 Svelte,什么时候该选 Vue/React
这是实战中最常被问到的问题。给一个决策清单:
- 用 Svelte:内部工具、CMS 后台、数据面板、小程序(Svelte 编译到自定义元素很合适);团队小、追求极致性能;不需要大量生态库。
- 用 Vue:需要中国团队协作、需要 Nuxt 做 SSR、需要成熟的 UI 框架(Element Plus 等);团队成员习惯选项式 API。
- 用 React:需要 Next.js 生态、需要庞大的 npm 生态(太多库只有 React 版);团队成员更熟悉 React hooks 心智模型。
Svelte 的生态确实是短板。我实测过:Svelte 的 UI 库很少,svelte-virtual-viewer 之类的虚拟滚动库在 React/Vue 里一抓一大把,Svelte 里能打的就一两个。如果你的项目重度依赖组件库,选 Svelte 前先查一下 svelte.dev/repl 里有没有你需要的。
快速上手:从零跑一个 Svelte 项目
环境要求:Node.js 18+(2024 年 12 月确认 Svelte 5 要求 Node ≥ 18.13)。用官方脚手架:
# 创建项目(用 Vite,比默认的快)
npm create vite@latest my-svelte-app -- --template svelte
cd my-svelte-app
npm install
npm run dev
# 打开 http://localhost:5173
如果要在已有项目里逐步引入 Svelte,用 Vite 插件:
// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [
svelte({
// 版本:@sveltejs/vite-plugin-svelte 4.0.0
compilerOptions: {
dev: process.env.NODE_ENV !== 'production'
}
})
]
});
Svelte 5 的一个新东西:runes(符文模式)。在新项目中,$state 取代了旧的 let 声明响应式变量:
// Svelte 5 runes 模式(推荐新项目用)
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>count is {count}</button>
这看起来跟 Vue 3 的 ref 用法几乎一样,但区别在于:$state 是编译时的概念,Vue 的 ref 是运行时的对象。Svelte 5 编译后,$state 变成了普通的变量声明,没有 Proxy、没有 getter/setter 包装。
避坑指南
这部分是血泪教训。我用 Svelte 写了两个生产项目后,列了 5 个一定要避开的坑。
坑 1:Svelte 的响应式不会追踪数组方法和索引赋值
我在第一个项目里写过这种代码:
// 这样写,DOM 不会更新!
let todos = $state([]);
todos.push({ id: 1, text: "hei", done: false }); // 不会刷新 UI
在 runes 模式下,push 能触发更新(Svelte 5 对数组方法做了特殊处理),但在旧语法下会失效。更安全的写法是:永远用「重新赋值」而不是「修改原变量」。
// 这样写,一定能触发更新(新旧语法都适用)
todos = [...todos, { id: 1, text: "hei", done: false }];
坑 2:模板里的 console.log 不生效
Svelte 模板里的表达式只会在组件初始化时执行一次,之后数据更新时不会重新执行。我为了调试,在模板里写了 {console.log(todos)},结果只打印了一次。
正确调试方式:
// 使用 $inspect(Svelte 5 新增)
$inspect(todos); // 每次变化都会在控制台打印
// 或者手动写一个响应式副作用
$effect(() => {
console.log("todos 变了:", todos);
});
坑 3:忽略 Vue 3 的响应式代理限制会踩大坑
不是在写 Svelte 吗?但对比框架的人,往往两个框架都在项目里用。Vue 3 的响应式基于 Proxy,有两个官方文档没说透的坑:
// 坑:解构后丢失响应式
const { count } = reactive({ count: 0 });
count++; // 不会触发更新!
// 坑:Map/Set 的 update 方法不是响应式的
const map = reactive(new Map());
map.set("key", 1); // 不会触发更新
map = new Map(map); // 才能触发
这些都是 Vue 3 响应式代理的边界问题。Svelte 没有这个问题,因为它没有代理,变量赋值直接在编译期生成更新代码。
坑 4:React 的闭包陷阱会让你调试到怀疑人生
用 React 写计数器的时候很常见:
// 经典闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 永远是 0,闭包捕获了初始值
}, 1000);
return () => clearInterval(id);
}, []); // 如果你加依赖 [count],又会导致 setInterval 被反复重置
}
React 的 useEffect 依赖数组是出了名的容易写错。Svelte 不存在这个问题,因为 $effect 自动追踪依赖,不需要手动声明 deps:
// Svelte 的 $effect 自动追踪依赖,没有闭包陷阱
let count = $state(0);
$effect(() => {
const id = setInterval(() => {
count += 1; // 每次都拿到最新的 count
}, 1000);
return () => clearInterval(id);
});
坑 5:Svelte 的绑定语法不能用在动态生成的结构上
<!-- 错误的用法:bind:value 不能绑定到 {#each} 动态生成的内容 -->
{#each items as item}
<input bind:value={item.text} /> <!-- 不推荐 -->
{/each}
Svelte 官方文档里说 bind 不支持在 {#each} 块中绑定到 item 属性的赋值。实际测过:绑定会失效,输入框的 value 不会更新。正确做法是:
{#each items as item}
<input
value={item.text}
oninput={(e) => (item.text = e.target.value)}
/>
{/each}
但注意,item.text = e.target.value 这种修改,在 Svelte 5 的 runes 模式下是响应式的,但在旧语法下不保证。为了保险,还是用整体替换:
function updateItem(id, newText) {
items = items.map(i => i.id === id ? { ...i, text: newText } : i);
}
Svelte 的生态现状(2025年初真实情况)
写这篇文章时我查了 npm 上的下载量:
- Svelte 包周下载量约 80 万次
- Vue 约 200 万次
- React 约 3,000 万次
Svelte 的 GitHub stars 不少,但实际生产使用远少于前两者。这意味着:遇到问题时,Stack Overflow 上的答案少,npm 上的可用组件库也少。 我在生产项目里找不到好的日期选择器组件,最后只能自己写了一个 200 行的 Svelte 组件。这在 React 里根本不算事,react-datepicker 有几十种配置。
但 Svelte 的优势在编译到 Web Component 这个方向很突出。Svelte 官方支持 svelte:options 自定义元素,可以直接把一个 Svelte 组件编译成原生的 <my-widget>,不依赖任何框架运行时。这点 Vue 也能做,但 React 的 Web Component 支持在 2025 年依然不够顺滑。
SvelteKit(Svelte 的元框架)在我实际使用的体验里,比 Next.js 在本地开发时快很多(因为编译快,HMR 热更新是毫秒级)。生产构建后,SvelteKit 的 JS 体积确实比 Next.js 小 3-4 倍。如果你的团队能驾驭 Svelte 的生态局限,SvelteKit 做全栈应用(逻辑不多、重 UI 交互)很合适。
选择建议:别纠结框架,看哪个让你少写废代码
框架之争扯了很多年,但我现在的心态是:用顺手的,就是好框架。
我做了一个内部用的数据报表系统,12 个页面,全是表格、表单、弹窗。用 Svelte 写的,全部代码(含样式)2681 行,产物 gzip 后 34KB(含图表库)。同事用 React 写了同类的模块,产物 gzip 后 120KB。数据交互流畅度上,Svelte 的方案在大表格滚动、筛选联动时明显更顺滑(因为精确更新没有 VDOM diff),但在快速原型迭代上,Vue 的生态更成熟,碰到新需求直接搜一下就能找现成方案。
如果你追求极致的性能和体积,喜欢「编译期帮你搞定一切」的思路,Svelte 值得一试。如果你需要一个生态成熟、团队好招人的框架,Vue 或 React 更稳。
反正这年头,写前端最贵的成本是——人花在「让数据变成 DOM」上浪费掉的精力。 选一个能让你少写 DOM 操作代码的框架,比什么都值。