📋 目錄
React 的
useEffect一直被抱怨「太複雜」——dependency array 稍有不慎就會造成 stale closure,各種 lint 規則讓人疲於奔命。Vue 的ref和reactive簡單很多,但響應式系統藏在 runtime 裡,有隱性開銷。Svelte 5 的 Runes,則是另一條路——編譯器幫你處理一切,API 表面簡單,實際上是用編譯時的最佳化,換取 runtime 的效能。
前端工程師為什麼要關心 Svelte 5
Svelte 這個框架在過去幾年一直處於「大家都知道它快,但很少人真的在用」的狀態。2024 年 Svelte 5 發布、Runes 系統正式登場之後,這個情況開始改變了。
Runes 不只是新語法,而是 Svelte 對「響應式」這個問題的重新思考。它借鑒了 Signals(信號)的概念,讓狀態追蹤在編譯時就完成,而不是在 runtime 時靠 Proxy 或 Observable 實現。這讓 Svelte 5 的效能領先多數框架:根據 benchmark,在復雜的響應式更新場景下,Svelte 5 可達到每秒 39.5 萬次操作。
如果你對現有的響應式方案(React 的 useEffect、Vue 的 watchEffect、MobX 的 autorun)感到疲憊,或者你在選下一個專案的框架,Runes 值得你認真評估。
Runes 是什麼:編譯時的響應式魔法
Runes(符文)是 Svelte 5 引入的編譯指令,以 $ 前綴標示。它們會在編譯階段被分析,建立起精确的依賴追蹤圖(dependency graph),runtime 時只需要執行這個已經最佳化的圖。
這跟 React 的做法有根本差異:
- React:runtime 時靠 Hooks 的 caller cache + dependency array 來猜測依賴,compiler 是可選的(React Compiler)
- Svelte:整個 framework 是 compiler-first,dependency graph 在 build time 就已經確定,runtime 只是執行確定了的指令
Runes 一覽表
| Rune | 功能 | 類比 |
|---|---|---|
$state | 反應式狀態 | React: useState |
$derived | 計算值(自動快取) | React: useMemo |
$effect | 副作用(自動 cleanup) | React: useEffect |
$props | 元件屬性(输入) | React: props |
$bindable | 雙向綁定 | Vue: v-model |
$state:最基礎的響應式狀態
基本用法
<script>
// 簡單的計數器
let count = $state(0);
// count 是一個信號,修改它會觸發所有依賴它的地方更新
function increment() {
count += 1; // 直接賦值,Svelte 編譯器會自動包裝成 .value 的存取
}
function decrement() {
count -= 1;
}
</script>
<button onclick={decrement}>-</button>
<span>{count}</span>
<button onclick={increment}>+</button>
物件與陣列
<script>
// 淺層響應式(預設)
let user = $state({ name: '小明', age: 28 });
// 深層響應式:當物件內任意屬性變化時,整個 user 被視為 dirty
let settings = $state({
theme: 'dark',
notifications: true,
language: 'zh-TW'
});
function updateTheme() {
settings.theme = settings.theme === 'dark' ? 'light' : 'dark';
}
</script>
<!-- settings.theme 變化時,這一行會更新 -->
<p>Current theme: {settings.theme}</p>
<button onclick={updateTheme}>Toggle Theme</button>
$state 和普通变量的区别
<script>
let normalVar = 0; // ❌ 不是響應式,變化不會觸發 UI 更新
let reactiveState = $state(0); // ✅ 是響應式
</script>
$derived:不再手寫 Dependency Array
基本用法
$derived 讓你用宣告式的方式宣告一個計算值——不需要 dependency array,不需要 useMemo,編譯器自動幫你建立依賴圖。
<script>
let price = $state(100);
let quantity = $state(2);
let discount = $state(0.1); // 10% 折扣
// ✅ derived:只要 price/quantity/discount 任一個變化,total 自動重新計算
let total = $derived(price * quantity * (1 - discount));
// ✅ derived 也可以用表達式描述複雜邏輯
let formattedTotal = $derived(
new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD'
}).format(total)
);
</script>
<p>總計:{formattedTotal}</p>
巢狀 Derived:鏈式計算
<script>
let items = $state([
{ name: '鍵盤', price: 1500, qty: 1 },
{ name: '滑鼠', price: 800, qty: 2 },
{ name: '螢幕', price: 8000, qty: 1 },
]);
// 鏈式 derived
let subtotals = $derived(items.map(item => item.price * item.qty));
let totalAmount = $derived(subtotals.reduce((sum, val) => sum + val, 0));
let isExpensive = $derived(totalAmount > 10000); // derived 也可以 derived
let message = $derived(
isExpensive ? `訂單金額 ${totalAmount},已超過万元大關!` : `訂單金額 ${totalAmount}`
);
</script>
<p>{message}</p>
$derived vs $derived.by
當計算邏輯複雜時,用 $derived.by 寫成函式:
<script>
let items = $state([3, 1, 4, 1, 5, 9, 2, 6]);
// 簡單表達式 → $derived
let count = $derived(items.length);
// 複雜邏輯 → $derived.by
let stats = $derived.by(() => {
const sorted = [...items].sort((a, b) => a - b);
const sum = items.reduce((a, b) => a + b, 0);
const mean = sum / items.length;
const median = sorted.length % 2 === 0
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
: sorted[Math.floor(sorted.length / 2)];
return { sum, mean, median, min: sorted[0], max: sorted[sorted.length - 1] };
});
</script>
<p>Sum: {stats.sum}, Mean: {stats.mean.toFixed(2)}, Median: {stats.median}</p>
$effect:自動 Cleanup 的副作用
基本用法
$effect 是 Svelte 5 的響應式副作用,自動追踪依賴,自動 cleanup。
<script>
let query = $state('');
let results = $state([]);
// ✅ 當 query 變化時,自動重新執行
// 組件卸載時,自動執行 cleanup 函式
$effect(() => {
if (!query) {
results = [];
return;
}
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => { results = data; })
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
// ✅ cleanup 函式:下次 effect 執行前 或 組件卸載時 自動呼叫
return () => {
controller.abort();
};
});
</script>
<input bind:value={query} placeholder="搜尋..." />
{#each results as result}
<p>{result.name}</p>
{/each}
$effect 的 dependency 自動追蹤
<script>
let count = $state(0);
let theme = $state('dark');
// ✅ 這個 effect 只追蹤 theme 的變化,count 改變不會觸發它
$effect(() => {
document.body.dataset.theme = theme;
// 你不需要寫 dependency array
// Svelte 自動知道這個 effect 只依賴 theme
});
</script>
React useEffect 的常見痛苦 vs Svelte $effect
// React:dependency array 的痛苦
useEffect(() => {
// 這個 callback 每次渲染都創建新的
// 裡面的變量可能是 stale
const id = setInterval(() => {
setCount(c => c + 1); // 閉包裡的 count 可能是舊值
}, 1000);
return () => clearInterval(id);
// ESLint 會一直提醒你:count or theme 在 dependency array 裡嗎?
}, [count, theme]); // ← 這裡忘記加任何一個,都可能造成 bug
// Svelte:不需要 dependency array
<script>
import { onDestroy } from 'svelte';
let count = $state(0);
let theme = $state('dark');
$effect(() => {
// 這個 effect 只在 theme 變化時執行
// 不需要 dependency array,compiler 幫你分析
document.body.dataset.theme = theme;
});
// setInterval 當然也可以放在 $effect 裡
$effect(() => {
const id = setInterval(() => {
count += 1; // 這個 count 是 reactive 的,Svelte 保證是最新值
}, 1000);
onDestroy(() => clearInterval(id)); // 或者用 onDestroy
// 也可以 return cleanup 函式,兩者等效
});
</script>
$props 與 $bindable:元件的輸入輸出
$props:宣告式接收屬性
<script>
// ✅ $props() 是 reactive 的,解構出來的都是 reactive
let { name, age = 18, onClick } = $props();
// 也可以用 $derived 從 props 衍生值
let isAdult = $derived(age >= 20);
</script>
<button onclick={onClick}>
{name} ({isAdult ? '成年' : '未成年'})
</button>
$bindable:雙向綁定
當你需要讓父元件可以直接雙向綁定子元件的狀態時,使用 $bindable:
<!-- Toggle.svelte -->
<script>
let { value = $bindable(false), label = '' } = $props();
</script>
<label>
<input type="checkbox" bind:checked={value} />
{label}
</label>
<!-- Parent.svelte -->
<script>
import Toggle from './Toggle.svelte';
let isEnabled = $state(false);
</script>
<!-- ✅ bind: 雙向綁定,isEnabled 跟 Toggle 內部的 value 同步 -->
<Toggle bind:value={isEnabled} label="啟用功能" />
<p>目前狀態:{isEnabled ? '啟用' : '停用'}</p>
Props 解構的 Default Values
<script>
let {
// 必填屬性
title,
// 有預設值的屬性
count = 0,
// 函式屬性(事件處理)
onUpdate = () => {},
// rest props
...rest
} = $props();
</script>
Runes 生態:從 React/Vue 遷移的實務觀點
從 React 遷移
| React 概念 | Svelte 5 Runes 對應 |
|---|---|
useState | $state |
useMemo | $derived 或 $derived.by |
useEffect | $effect |
useCallback | 直接用 function(Svelte 不需要) |
| 受控元件 | $bindable |
| Context | Svelte context API(不是 Runes) |
從 Vue 遷移
| Vue 概念 | Svelte 5 Runes 對應 |
|---|---|
ref | $state |
computed | $derived |
watch / watchEffect | $effect |
v-model | $bindable |
defineProps | $props |
Runes 的效能優越性
Svelte 5 的編譯器將 reactive 語句直接編譯成 vanilla JavaScript DOM 操作,沒有 Virtual DOM 這個中間層:
// 這個 Svelte 程式碼
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>
// 編譯後大概像這樣(簡化):
let count = 0;
const btn = document.querySelector('button');
const span = document.querySelector('span');
btn.addEventListener('click', () => {
count++;
span.textContent = count; // 直接操作 DOM,沒有 VDOM diffing
});
這就是為什麼 Svelte 在 benchmark 裡領先——它沒有 React 的 reconciliation 開銷,也沒有 Vue 的 Proxy 攔截 overhead。
結語:Runes 適合你嗎?
Runes 不是銀彈,但它是近年來最有新意的響應式系統設計之一。
Runes 的優點:
- 編譯器處理依賴追蹤,開發者不需要操心 dependency array
$effect的自動 cleanup 機制大幅減少記憶體洩漏風險- 沒有 Virtual DOM,直接操作 DOM,效能優秀
- API 表面簡單,但底層概念嚴謹
Runes 的缺點:
- 學習曲線:Signal 模型對 React 開發者來說需要心態調整
- 生態系小:社群資源、第三方庫落後 React/Vue
- 團隊熟悉度:多數前端團隊對 React 經驗更深
$state的深層響應式需要理解 compiler 的行為
如果你在評估新框架、或你對效能非常敏感,Runes 值得你花週末下午認真研究一下。
延伸閱讀
- 2026 前端框架比較:React、Vue、Svelte 誰與爭鋒 — 三大框架完整比較
- SolidJS Signals 生態系統 — 另一個 Signal 框架的選擇
本文是「2026 前端框架選擇」系列文章之一。如果你有興趣看「Svelte 5 遷移實戰」或「Svelte vs React Compiler」的深入比較,歡迎留言。