- Published on
Svelte 5 Runes — リアクティビティの書き直しと、Solid・Vue・MobX・React Compiler との真っ向比較 (2026 深掘り) (日本語)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
プロローグ — コンパイラ魔法が終わり、rune が始まった
Svelte 3 は約束だった。「リアクティブはコンパイラの仕事、ランタイムは軽く」。let count = 0 がリアクティブになり、$: doubled = count * 2 が依存を自動追跡し、React の useState と依存配列のダンスなしで生きていけた。
しかしその約束には小さな注釈があった。
- リアクティビティはコンポーネント内だけ。
.svelteファイルの外、普通の.ts/.jsでは$:は何もしない。 $:ラベルの 2 つの顔。 同じ構文が「派生値」と「副作用」の両方を意味した。コンパイラは判別したが、人間は度々混乱した。letが魔法だった。 コンポーネント最上位のletはリアクティブ、関数内のletは普通の変数。同じ構文で違う意味。- TypeScript との摩擦。 コンパイラが意味を書き換える識別子に型を付けるのは厄介だった。
2024 年 10 月 22 日、Svelte 5 が GA で出た。そして 1 行ですべての小さな注釈を消した。
「リアクティビティは魔法ではない。rune だ」
rune は $ プレフィックスのコンパイラキーワードで、関数呼び出しのように見える — $state・$derived・$effect・$props、それに数個。コンポーネント内でも、外でも、クラス内でも、モジュールのどこでも同じように動く。意味はその行に書いてある。
<script>
// Svelte 5
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => console.log(count))
</script>
本稿はその変化を最初から最後まで — なぜ・何を・どう・いつ移行するか — 一息で整理する。そしてその隣に Solid のシグナル、Vue の ref/computed、MobX の observable、React Compiler の自動メモ化を並べ、「2026 年、私たちはどこに立っているか」を問う。
1 章 · なぜ rune なのか — 暗黙から明示へ
まず Svelte 4 と Svelte 5 の同じカウンタを並べる。
<!-- Svelte 4 -->
<script>
let count = 0
$: doubled = count * 2
$: if (count > 10) console.log('high')
function increment() {
count += 1
}
</script>
<button on:click={increment}>{count}</button>
<p>doubled: {doubled}</p>
<!-- Svelte 5 -->
<script>
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => {
if (count > 10) console.log('high')
})
function increment() {
count += 1
}
</script>
<button onclick={increment}>{count}</button>
<p>doubled: {doubled}</p>
違いは 2 つ。
- リアクティビティが識別子に付く、位置には付かない。 Svelte 4 は「最上位
let」という位置ルールだった。Svelte 5 は$state()を通った値だけがリアクティブ。 $:ラベルの 2 つの顔が分離された。 派生値は$derived、副作用は$effectで明示。
なぜ大事か? 3 つのシナリオで見る。
1.1 コンポーネント外でのリアクティビティ
Svelte 4 でカウンタロジックを別ファイルに出したかったとする。
// counter.js (Svelte 4 時代 — 動かない)
let count = 0
$: doubled = count * 2 // ← .svelte ファイル外ではコンパイラが無視
$: は .svelte ファイル内のコンパイラディレクティブだった。普通の .js/.ts では JS のラベル構文に過ぎず、ランタイムでは無意味。だから store という別の抽象を使った — writable・readable・derived と $store 接頭辞。
Svelte 5 では?
// counter.svelte.js
export function createCounter() {
let count = $state(0)
let doubled = $derived(count * 2)
return {
get count() { return count },
get doubled() { return doubled },
increment: () => (count += 1),
}
}
拡張子が .svelte.js または .svelte.ts ならコンパイラが rune を処理する。実用的な帰結は store がほぼ不要になった こと。rune ひとつでコンポーネントの内と外が統一される。
1.2 $: の 2 つの顔
Svelte 4 の $: は同じ構文が 2 つの意味を持った。コンパイラが右辺を解析して「代入なら派生、式なら副作用」と振り分けた。
<script>
let a = 1
let b = 2
$: sum = a + b // ← 派生 (代入)
$: console.log(a, b) // ← 副作用 (式)
$: if (sum > 5) alert() // ← 副作用 (if 文)
</script>
読み手は毎回右辺をもう一度見る必要があった。Svelte 5 は 2 つを分離する。
<script>
let a = $state(1)
let b = $state(2)
let sum = $derived(a + b)
$effect(() => console.log(a, b))
$effect(() => { if (sum > 5) alert() })
</script>
1.3 依存の追跡
両者とも静的解析で依存を追う。React は依存配列を人が書く — useMemo(() => a + b, [a, b])。Svelte は書かない。React Compiler が自動化しようとしているのと同じ地点だ。
2 章 · 4 つの rune — $state・$derived・$effect・$props
2.1 $state — リアクティビティの出発点
<script>
let count = $state(0)
let user = $state({ name: '영주', age: 32 })
let tags = $state(['svelte', 'runes'])
function rename() {
user.name = '영주2' // ← deep reactivity、動く
tags.push('signals') // ← 配列の変更も追跡される
}
</script>
$state は デフォルトで深いリアクティビティ だ。オブジェクト・配列を Proxy でラップし、ネストしたプロパティ変更まで追跡する。大規模な構造でオーバーヘッドが負担なら $state.raw() で浅いリアクティビティのみに切り替えられる。
let snapshot = $state.raw({ huge: data })
// snapshot.huge.foo = 'bar' ← 追跡されない
snapshot = { ...snapshot, huge: { ...snapshot.huge, foo: 'bar' } } // ← 追跡される
2.2 $derived — 派生、そして遅延
<script>
let count = $state(0)
let doubled = $derived(count * 2)
let expensive = $derived.by(() => {
let acc = 0
for (let i = 0; i < count; i++) acc += i
return acc
})
</script>
$derived は 遅延 & キャッシュ される。読まれなければ計算されず、依存が変わらなければ再計算されない。Solid の createMemo、Vue の computed と同じ意味だ。
2.3 $effect — 副作用の明示的ゲート
<script>
let url = $state('/api/me')
$effect(() => {
const ctrl = new AbortController()
fetch(url, { signal: ctrl.signal })
.then(r => r.json())
.then(console.log)
return () => ctrl.abort() // ← cleanup
})
</script>
$effect は DOM マウント後、そして依存が変わるたびに実行される。cleanup は React の useEffect と同じ形で返す。$effect.pre は DOM 更新の 前 に走り、$effect.root は明示的に dispose できるエフェクトツリーを作る。
肝心なルール: $effect 内で $state に書き込むな。 無限ループと同期の暴発の元凶だ。派生が欲しいなら $derived を使う。
2.4 $props — コンポーネント入力の新しい構文
<!-- Svelte 4 -->
<script>
export let name
export let age = 0
export let onSave
</script>
<!-- Svelte 5 -->
<script>
let { name, age = 0, onSave, ...rest } = $props()
</script>
複数の export let が 1 行の分割代入にまとまる。デフォルト値・rest props・リネーム、すべて標準 JS 構文で書ける。TypeScript とも自然に流れる。
<script lang="ts">
interface Props {
name: string
age?: number
onSave: (id: string) => void
}
let { name, age = 0, onSave }: Props = $props()
</script>
加えて $bindable() で双方向バインディングを明示し、$host() でカスタムエレメントホストにアクセスし、$inspect は開発時のデバッグ rune。
3 章 · rune vs シグナル vs ref vs observable vs useState — 比較マトリクス
同じカウンタを 5 つのリアクティビティモデルで。
<!-- Svelte 5 / Runes -->
<script>
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => console.log(count))
</script>
<button onclick={() => count++}>{count}</button>
// Solid / Signals
function Counter() {
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => console.log(count()))
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
}
<!-- Vue 3 / Composition + ref -->
<script setup>
import { ref, computed, watchEffect } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
watchEffect(() => console.log(count.value))
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
// MobX
import { makeAutoObservable } from 'mobx'
import { observer } from 'mobx-react-lite'
class Store {
count = 0
constructor() { makeAutoObservable(this) }
get doubled() { return this.count * 2 }
inc() { this.count += 1 }
}
const store = new Store()
const Counter = observer(() => (
<button onClick={() => store.inc()}>{store.count}</button>
))
// React 19 + React Compiler
function Counter() {
const [count, setCount] = useState(0)
// Compiler がメモ化する — 手書きの useMemo は不要
const doubled = count * 2
useEffect(() => console.log(count), [count])
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
比較マトリクス。
| 軸 | Svelte 5 Runes | Solid Signals | Vue 3 ref/computed | MobX | React + Compiler |
|---|---|---|---|---|---|
| 読み構文 | count (識別子) | count() (呼び出し) | count.value | store.count | count (分割代入) |
| 書き構文 | count++ | setCount(c => c + 1) | count.value++ | store.count++ | setCount(c => c + 1) |
| 派生 | $derived(...) | createMemo(...) | computed(...) | ゲッタ get x() | 暗黙 (Compiler) |
| 副作用 | $effect(...) | createEffect(...) | watchEffect(...) | autorun(...) | useEffect(..., [deps]) |
| コンポーネント外 | .svelte.ts | どこでも | どこでも | どこでも | フック: 不可 |
| 深いリアクティビティ | あり (Proxy) | 浅い (store で補強) | あり (Proxy) | あり (Proxy/getter) | 浅い (参照比較) |
| コンパイル段階 | コンパイル時変換 | コンパイル時変換 | ランタイム Proxy | ランタイム Proxy | コンパイル + ランタイム |
| 依存配列 | なし (自動) | なし (自動) | なし (自動) | なし (自動) | 手書き or Compiler |
| TypeScript | 自然 | 自然 | 自然 | デコレータ可 | 自然 |
| バンドル容量 | とても小さい | とても小さい | 中 | 中 | 大きい |
一言で — Svelte 5 は「コンパイル時に書き換えられるシグナル」 だ。Solid に最も近い。ただし読み構文が関数呼び出しでなく素の識別子なので、普通の JavaScript のように読める。
4 章 · $: から rune へ — 実戦マイグレーション
良いニュース: legacy モードは動き続ける。 Svelte 5 のコンパイラは $: も処理する。svelte.config.js で compilerOptions.runes = false を指定するか、コンポーネント最上部に <svelte:options runes={false}/> を書けば、Svelte 4 時代の構文がそのまま動く。
もう 1 つ良いニュース: コンポーネント単位で段階移行できる。 1 つのプロジェクトに rune コンポーネントと legacy コンポーネントが共存できる。
よく出会う 5 つの変換パターン。
4.1 let → $state
<!-- before -->
<script>
let count = 0
</script>
<!-- after -->
<script>
let count = $state(0)
</script>
マイグレーションスクリプト (npx sv migrate svelte-5) がこの変換をほぼ自動でやってくれる。書き換えられない let は変換しない — リアクティビティが必要ない変数だ。
4.2 $: foo = ... → $derived
<!-- before -->
<script>
let a = 1, b = 2
$: sum = a + b
</script>
<!-- after -->
<script>
let a = $state(1), b = $state(2)
let sum = $derived(a + b)
</script>
4.3 $: { ... } または $: if (...) → $effect
<!-- before -->
<script>
let count = 0
$: if (count > 10) document.title = `high: ${count}`
</script>
<!-- after -->
<script>
let count = $state(0)
$effect(() => {
if (count > 10) document.title = `high: ${count}`
})
</script>
4.4 store → クラス内 rune
Svelte 4 の store パターンをクラス + rune に移すのが 2026 年の推奨パターン。
// before — store
import { writable, derived } from 'svelte/store'
function createCart() {
const items = writable([])
const count = derived(items, ($i) => $i.length)
return {
items,
count,
add: (item) => items.update((arr) => [...arr, item]),
}
}
// after — runes in class (.svelte.ts)
export class Cart {
items = $state([])
count = $derived(this.items.length)
add(item) { this.items.push(item) }
}
export const cart = new Cart()
呼び出し側も短くなる。$cart.count のような接頭辞が消えて、cart.count になる。
4.5 on:click → onclick
rune と直接は関係ないが、Svelte 5 の変更だ。
<!-- before -->
<button on:click={handler}>x</button>
<input on:input={handler} bind:value={text} />
<!-- after -->
<button onclick={handler}>x</button>
<input oninput={handler} bind:value={text} />
コロン付きディレクティブの代わりに標準 HTML 属性名で書く。props でハンドラを渡すパターンと自然に噛み合う。
5 章 · SvelteKit 2 — データフローが明示的になった
rune と同じ方向の変化が SvelteKit にも起きた。2024 年初頭の SvelteKit 2.0 が導入した 2 つの変化が、2026 年には標準だ。
5.1 明示的な無効化
// before (SvelteKit 1)
export const load = async ({ fetch, params }) => {
const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())
return { post }
}
// after (SvelteKit 2)
export const load = async ({ fetch, params, depends }) => {
depends('app:post')
const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())
return { post }
}
depends で無効化キーを宣言し、invalidate('app:post') で再読み込みをトリガする。以前は URL パターンマッチで暗黙にやっていたものを明示する。
5.2 enhance 付きフォームアクション — JS なしでも、JS ありでも
フォームアクションは rune と並んで元気だ。プログレッシブエンハンスメントのクリーンなデモになっている。
<script>
import { enhance } from '$app/forms'
let { form } = $props()
</script>
<form method="POST" use:enhance>
<input name="title" />
{#if form?.error}
<p>error: {form.error}</p>
{/if}
<button>save</button>
</form>
// +page.server.js
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const title = data.get('title')
if (!title) return { error: 'title required' }
await db.posts.create({ title })
return { success: true }
},
}
JS オフ? 普通のフォーム送信。JS オン? enhance が fetch で横取りして SPA 風に処理する。
5.3 Universal vs server load
+page.js (universal) と +page.server.js (server-only) の区別が rune でさらに鋭くなった。universal load 内で作った $state はハイドレーション後も生きている。server-only load はプレーン JSON を返す。
6 章 · 実例 — 誰が rune を使っているか
GA から 1 年半、rune は Svelte エコシステムのほぼ全域に広まった。
- SvelteKit 自体 のスターターテンプレートが rune ベース。
- Skeleton UI v3 (Tailwind 4 系コンポーネントライブラリ) が rune で書き直された。
- shadcn-svelte が rune に移行。
$store接頭辞は消えた。 - TanStack Query for Svelte v5 が rune ベースの API を出した。
- HeyApi・Bits UI・Melt UI — 全て移行済みか移行中。
ユーザ側では、Spotify・Apple・1Password が公の場で Svelte を使っており、Apple Music の Web の一部が Svelte と知られている。2026 年の State of JS 2024 調査では、Svelte が retention (過去に使った人がまた使う割合) で先頭を維持し、React と Vue が絶対使用率で迫る、という構図が続いている。
7 章 · 何を諦めたか — 正直な取引
rune はほぼ全ての軸で 4 に勝つ。しかし「全て良い」は言い過ぎだ。
7.1 タイプ数が増えた
let count = 0 が let count = $state(0) に。「ただの JS」の簡潔さがやや薄れた。その対価が 明示性・一貫性・拡張性 だ。
7.2 Proxy のコスト
$state はオブジェクト・配列を Proxy でラップする。大規模な構造ではオーバーヘッドがある。緩和策 — ホットパスでは $state.raw() を使うか、不変データを 1 回の代入で差し替える。
7.3 .svelte.ts という拡張子
普通のモジュールで rune を使うには .svelte.js または .svelte.ts でなければならない。プレーン .ts はコンパイラが触らない。小さいが、知らないと踏む地雷だ。
7.4 $effect の罠
$effect 内で $state に書くと無限ループになる。ESLint ルール (svelte/no-reactive-reassign) とコンパイラ警告でほぼ捕まるが、人間の注意は依然必要。
7.5 移行期の学習曲線
Svelte 4 を知る人にとって、しばらくは 2 つのメンタルモデルを頭に持つ期間がある。rune が全面的に良くても、ネット上の資料の半分は $: 構文で書かれている。
7.6 React のエコシステムには及ばない
これは rune 自体の問題ではなくエコシステムの大きさの問題。2026 年でも npm ダウンロード・ライブラリ数・求人市場はすべて React が大きい。Svelte は満足度・学習曲線・バンドル容量・ランタイム性能で優位。
8 章 · 「React より良い」は 2026 年も真か
古い命題がある。「Svelte = React より良い」。2026 年にもう一度問う。
8.1 React 19 と React Compiler
React 19 が use()・Actions・Server Components の時代を固めた。React Compiler が GA に入り、手書きの useMemo・useCallback が減った。メモ化の自動化は Solid・Svelte・Vue が既に持っていたもの — React がついに追いついた。
それでも React が勝つところ:
- エコシステムの広さ。 ライブラリ・ツール・求人・資料が全部多い。
- Server Components の深さ。 Svelte も似た方向 (サーバデータ + クライアントインタラクション) だが、React が抽象の層で先行。
- 互換性のフットプリント。 React Native・Expo、超大企業のデフォルト。
8.2 Svelte 5 が勝つところ
- バンドル容量。 比較にならない。ルータ・リアクティビティ・DOM 処理を合わせて React + ReactDOM の 1/5 程度。
- 明示的リアクティビティ。 rune が意図をコードに刻む。React Compiler が「自動」なら、rune は「宣言」だ。
- 内外の一貫性。 rune はどこでも同じ。React フックはコンポーネント (または別フック) 内でしか使えない。
- DX のシンプルさ。
bind:value・transition:fade・use:enhanceのようなディレクティブが今もそこにある。
8.3 結局は「コンテキスト」
「Svelte = React より良い」は 開発体験 では依然真。エコシステムの広さと深さ では React が依然大きい。採用・既存コード・チームの慣れを引いたなら、Svelte 5 の魅力は GA 時点より強くなった。rune が最後の弱点 (コンポーネント外でのリアクティビティ) を埋めたからだ。
9 章 · Solid・Vue と並べてもう一段深く
rune をシグナル・ref と並べた時、ディテールでどこが違うか。
9.1 vs Solid Signals
// Solid
const [count, setCount] = createSignal(0)
console.log(count()) // ← 関数呼び出し
setCount(1)
setCount(c => c + 1)
<!-- Svelte 5 -->
<script>
let count = $state(0)
console.log(count) // ← 変数読み
count = 1
count += 1
</script>
Solid はシグナルを関数にする。呼び出しが追跡の単位だ。利点 — どこで追跡が起きるかコードで直接見える。欠点 — タイプ数が増え、オブジェクト内でシグナルを扱うのがやや面倒 (Solid は createStore で解決)。
Svelte 5 はコンパイラに同じ仕事をさせる。識別子アクセスを自動で追跡呼び出しに書き換える。利点 — 普通の JS のように読める。欠点 — 「今ここで追跡されているか」を確認するにはコンパイル結果を見る必要がある。
9.2 vs Vue 3 ref/computed
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
Vue は .value で ref アクセスを明示する。テンプレート内では .value が自動 unwrap される。rune に最も近いモデルだが、ランタイム Proxy であり、コンパイル時書き換えではない。
Vue 3.4+ は Vapor mode (コンパイル時リアクティビティ、Solid・Svelte スタイル) を実験中。2026 年時点で Vapor が一般化すれば、ref と rune の距離はさらに縮まる。
9.3 vs MobX
MobX は OOP 親和的だ。クラスインスタンスに makeAutoObservable を呼べば、すべてのフィールドが自動 observable に、すべてのゲッタが自動 computed になる。
Svelte 5 の「runes in classes」パターンは意図的に MobX 風だ。
// Svelte 5
class Cart {
items = $state([])
count = $derived(this.items.length)
add(item) { this.items.push(item) }
}
// MobX
class Cart {
items = []
constructor() { makeAutoObservable(this) }
get count() { return this.items.length }
add(item) { this.items.push(item) }
}
MobX はデコレータかコンストラクタ呼び出しで変換。Svelte 5 は rune で明示。意味はほぼ同じ。
10 章 · コンパイル結果を覗く — 本当に何が起きているか
rune を使ったコードをコンパイラは何に変えるか。単純な例で追う。
<!-- source -->
<script>
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => console.log(count))
</script>
<button onclick={() => count++}>{count}</button>
概念的には、出力はおおよそこうなる (実際の出力はもっと長く低レベル)。
import * as $ from 'svelte/internal/client'
function Component($$anchor) {
let count = $.state(0)
let doubled = $.derived(() => $.get(count) * 2)
$.user_effect(() => console.log($.get(count)))
const button = $.template('<button> </button>')()
$.event('click', button, () => $.set(count, $.get(count) + 1))
$.text(button.firstChild, () => $.get(count))
$.append($$anchor, button)
}
要点:
- 識別子
countが シグナルセル に変換される。 - 読みは
$.get(count)、書きは$.set(count, ...)。 $derivedは$.derived(thunk)— Solid のcreateMemoと事実上同じ実装。$effectは$.user_effect(thunk)— マウント後、依存変更ごとに再実行。
つまり rune は コンパイラトリックで、人間が呼び出し構文なしにシグナルを使えるようにする仕掛け だ。Svelte 4 の魔法と違うのは、トリガが位置ではなく、コード内の 明示的なトークン であること。
11 章 · 大規模アプリでの rune — パターンとアンチパターン
11.1 推奨パターン
- ドメインロジックは
.svelte.tsモジュールへ。 コンポーネントは表現に集中。 - クラス + rune で状態コンテナ。 store より型が自然に流れる。
- 派生は
$derived、副作用だけ$effect。 混ぜない。 $effectの cleanup を必ず返す。 メモリリークの 8 割がここ。- 浅いリアクティビティが欲しければ
$state.raw。 大きなツリーで Proxy コストを避ける。 - prop 型はインターフェースで —
let { ... }: Props = $props()。 $bindable()は本当に双方向が要る prop だけ。 デフォルトは単方向。
11.2 アンチパターン
$effect内で$stateに書く — 無限ループ。派生なら$derived。- モジュールレベルの
$stateを直接 export — SSR で複数リクエストが共有。ファクトリ関数でラップする。 - 巨大なオブジェクトを
$state— Proxy が深く走り高くつく。平坦化するか$state.raw。 $:と rune を同じコンポーネントで混在。 legacy モードでない限りコンパイラが拒否。1 コンポーネント 1 流派。- store と rune を同じドメインで並行運用。 可能だが頭に 2 つのモデルを置くことになる。可能なら一本化。
$effectでデータフェッチ。 可能だが SvelteKit のloadの方がよい。$effectは真のクライアント側副作用用に。$inspectを本番に残す。 開発専用のデバッグ rune だ。
12 章 · ツール・エコシステム — rune 時代の周辺
12.1 IDE・言語サーバ
VS Code の Svelte for VS Code 拡張が rune を完璧に認識する。JetBrains WebStorm 2024.3+ も同様。rune がコンパイラキーワードなので、意味は LSP が知っている。
12.2 ESLint・Prettier
eslint-plugin-svelte が rune 用ルールセットを提供する。svelte/no-reactive-reassign・svelte/require-state-with-init が要だ。
12.3 テスト
vitest + @testing-library/svelte が標準。.svelte.ts 内の rune をテストする時は、同期更新を強制する flushSync() パターンがよく使われる。
12.4 ビルド
Vite 5 が推奨バンドラ、Vite 6 も互換。SvelteKit がその上にルーティング・SSR・アダプタを乗せる。Bun も Svelte 5 を正式サポートするが、SvelteKit アダプタは Node・Vercel・Cloudflare・Netlify がより成熟している。
12.5 コンポーネントライブラリ
- shadcn-svelte — rune ベースのアンスタイルコンポーネント、コピペモデル。
- Skeleton v3 — rune + Tailwind 4。
- Bits UI — ヘッドレスコンポーネント、rune ベース。
- Melt UI — ビルダーパターンのヘッドレス、rune 移行完了。
13 章 · 未来 — rune の次は何か
Svelte コアチームが公の場で話している方向は 3 つ。
- Svelte 6。 rune ベース API を安定化、legacy モードを段階的に削減。6 で legacy は deprecation 候補、7 で削除候補。
- コンパイラ最適化。 Vapor mode のような発想を Svelte 5 のパイプラインにさらに深く入れる。fine-grained 更新は既にあり、その上にテキスト・属性・部分ツリー単位の最適化が乗る。
- SvelteKit と RSC の境界。 React Server Components と同じ種類の分離 (サーバ部品 vs クライアント部品) を、rune・
load・フォームアクションの上でどう自然に解くかが課題。
そしてもっと大きな絵: シグナルの標準化。 TC39 シグナル提案が進行中。Svelte・Solid・Vue・Angular が同じプリミティブを共有すれば、rune は標準ライブラリの上の表面構文になる — ES モジュールや Promise が競合実装の年月を経て安定化したのと同じ筋道だ。
エピローグ — 明示性へ向けた一歩
Svelte 4 の let は美しかった。「ただの JS」だった。しかし「ただの JS」ではなかった。コンパイラが意味を変えた。その秘密が新参者の足を引っかけ、TypeScript と摩擦し、コンポーネント外へ出られなかった。
Svelte 5 の rune はその秘密をソースコードに書き込んだ。$state があるところにリアクティビティがある。 より長くなった。より正直になった。Solid・Vue・MobX が各々の道でたどり着いた同じ結論に、Svelte は Svelte の道でたどり着いた。
一文で残しておく。
「魔法はよい。ただし魔法を呼ぶ言葉は人間に見えていなければならない」
12 項目チェックリスト
.svelteコンポーネントは rune モードか (runes: trueか自動検出)?let変数は本当にリアクティビティが必要か — 必要なものだけ$state?- 派生値は
$derived、副作用は$effectと明確に分離されているか? $effect内で$stateに書き込んでいないか?$effectの cleanup を返しているか (リソース所有時)?- 大きなオブジェクトは
$state.rawで浅いリアクティビティにしているか? - コンポーネント外の rune モジュールは
.svelte.ts拡張子か? - props は
let { ... }: Props = $props()スタイルで書いているか? - 双方向が要るものだけ
$bindable()か? - store から rune に移行する際 SSR セーフか (ファクトリで包む)?
- SvelteKit
loadの無効化をdepends/invalidateで明示しているか? - 新コードに legacy の残骸 (
$:・on:click・export let) が混ざっていないか?
10 のアンチパターン
$effect内で$stateに書く — 無限ループ。- モジュールレベル
$stateをそのまま export — SSR 共有バグ。 - 巨大なオブジェクトを
$state— Proxy コスト。 $:と rune を同コンポーネントに混在。- store と rune を同ドメインに共存。
$effectでデータフェッチ —loadの方が適切。$inspect・$state.snapshotのデバッグコードを本番に残す。$bindable()を単方向データに乱用。.svelteでも.svelte.tsでもないファイルで rune を使う。- 移行中にコンパイラ警告を消す。
次回予告
次回候補: SvelteKit 2 データフロー深掘り — load・actions・enhance の本当の意味、TC39 Signals を追う — Svelte・Solid・Vue・Angular が共有する未来、Svelte vs Astro vs SolidStart — 2026 年のコンテンツサイト選び。
「リアクティビティは魔法ではなく道具だ。道具の名前はコードに書かれているべきだ」
— Svelte 5 Runes、終わり。
参考 / References
- Svelte 公式サイト
- Svelte 5 発表ブログ
- Svelte 5 マイグレーションガイド
- Runes ドキュメント
$stateリファレンス$derivedリファレンス$effectリファレンス$propsリファレンス- SvelteKit 公式ドキュメント
- SvelteKit 2 マイグレーション
- Solid 公式 — Reactivity ガイド
- Vue 3 Reactivity API
- Vue Vapor mode RFC
- MobX 公式
- React Compiler ドキュメント
- TC39 Signals 提案
- State of JS 2024
- shadcn-svelte
- Skeleton UI
- Bits UI
- Melt UI