Svelte 5の概要
Svelte 5は、フレームワークの根本的な進化となるメジャーリリースです。「Runes」と呼ばれる新しいリアクティビティシステムにより、より明示的で予測可能なコードが書けるようになりました。
flowchart TB
subgraph Svelte5["Svelte 5 主要な変更点"]
subgraph Runes["Runes (新リアクティビティ)"]
R1["$state() - リアクティブな状態宣言"]
R2["$derived() - 派生値の計算"]
R3["$effect() - 副作用の実行"]
R4["$props() - コンポーネントのプロパティ"]
R5["$bindable() - 双方向バインディング可能なプロップ"]
end
subgraph Perf["パフォーマンス向上"]
P1["最大2倍の高速化"]
P2["バンドルサイズ削減"]
P3["メモリ使用量の改善"]
P4["より効率的な差分検出"]
end
subgraph DX["開発者体験"]
D1["TypeScriptサポート強化"]
D2["より明示的なリアクティビティ"]
D3["デバッグの容易さ"]
D4["段階的な移行パス"]
end
end
Runesの基本
$state - リアクティブな状態
<script>
// Svelte 4
let count = 0;
// Svelte 5 with Runes
let count = $state(0);
// オブジェクトの状態
let user = $state({
name: 'John',
age: 25
});
// 配列の状態
let items = $state(['apple', 'banana', 'orange']);
function increment() {
count++; // 自動的にリアクティブ
}
function updateName() {
user.name = 'Jane'; // ネストしたプロパティも追跡
}
function addItem() {
items.push('grape'); // 配列メソッドも自動追跡
}
</script>
<button onclick={increment}>
Count: {count}
</button>
<p>Name: {user.name}</p>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
$derived - 派生状態
<script>
let count = $state(0);
let multiplier = $state(2);
// シンプルな派生値
let doubled = $derived(count * 2);
// 複数の依存関係
let multiplied = $derived(count * multiplier);
// 複雑な計算($derived.by)
let stats = $derived.by(() => {
const values = [count, doubled, multiplied];
return {
sum: values.reduce((a, b) => a + b, 0),
avg: values.reduce((a, b) => a + b, 0) / values.length,
max: Math.max(...values)
};
});
// フィルタリング
let items = $state([
{ name: 'Apple', price: 100 },
{ name: 'Banana', price: 80 },
{ name: 'Orange', price: 120 }
]);
let expensive = $derived(items.filter(item => item.price > 100));
</script>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Stats: Sum={stats.sum}, Avg={stats.avg}</p>
ポイント: $derivedはSvelte 4の$:リアクティブ宣言に代わるもので、依存関係が明示的になりデバッグが容易になります。
$effect - 副作用
<script>
let count = $state(0);
let searchQuery = $state('');
// 基本的な副作用
$effect(() => {
console.log(`Count changed to: ${count}`);
});
// クリーンアップ付き
$effect(() => {
const timer = setInterval(() => {
count++;
}, 1000);
// クリーンアップ関数を返す
return () => clearInterval(timer);
});
// 検索のデバウンス
$effect(() => {
const query = searchQuery; // 依存関係をキャプチャ
const timeout = setTimeout(() => {
if (query) {
fetchSearchResults(query);
}
}, 300);
return () => clearTimeout(timeout);
});
// pre効果(DOM更新前に実行)
$effect.pre(() => {
console.log('Before DOM update');
});
// root効果(コンポーネントライフサイクルに縛られない)
const cleanup = $effect.root(() => {
// グローバルなイベントリスナーなど
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
</script>
コンポーネントのプロパティ
$props
<!-- Button.svelte -->
<script>
// Svelte 4
export let variant = 'primary';
export let disabled = false;
// Svelte 5 with $props
let { variant = 'primary', disabled = false, onclick, children } = $props();
// 型定義付き
interface Props {
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onclick?: () => void;
children?: import('svelte').Snippet;
}
let { variant = 'primary', disabled = false, ...rest }: Props = $props();
</script>
<button
class="btn btn-{variant}"
{disabled}
onclick={onclick}
{...rest}
>
{@render children?.()}
</button>
<!-- 使用側 -->
<Button variant="danger" onclick={() => alert('Clicked!')}>
Delete
</Button>
$bindable - 双方向バインディング
<!-- Input.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input
type="text"
bind:value
oninput={(e) => value = e.currentTarget.value}
/>
<!-- 使用側 -->
<script>
let name = $state('');
</script>
<Input bind:value={name} />
<p>Hello, {name}!</p>
Snippets(スニペット)
<script>
let items = $state([
{ id: 1, name: 'Apple', price: 100 },
{ id: 2, name: 'Banana', price: 80 },
{ id: 3, name: 'Orange', price: 120 }
]);
</script>
<!-- スニペットの定義 -->
{#snippet row(item)}
<tr>
<td>{item.name}</td>
<td>¥{item.price}</td>
</tr>
{/snippet}
{#snippet header()}
<tr>
<th>Name</th>
<th>Price</th>
</tr>
{/snippet}
<!-- スニペットの使用 -->
<table>
<thead>
{@render header()}
</thead>
<tbody>
{#each items as item (item.id)}
{@render row(item)}
{/each}
</tbody>
</table>
<!-- コンポーネントに渡す -->
<Table data={items} {row} {header} />
Svelte 4 vs Svelte 5 比較
| 機能 | Svelte 4 | Svelte 5 |
|---|---|---|
| リアクティブ変数 | let count = 0; | let count = $state(0); |
| 派生値 | $: doubled = c * 2; | let doubled = $derived(c*2); |
| 副作用 | $: console.log(c); | $effect(() => log(count)); |
| プロパティ | export let name; | let { name } = $props(); |
イベントハンドリング
<script>
let count = $state(0);
// Svelte 5ではonXXX形式を推奨
function handleClick(event) {
count++;
}
function handleInput(event) {
console.log(event.currentTarget.value);
}
</script>
<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>
<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>
<!-- インラインハンドラ -->
<button onclick={() => count++}>Count: {count}</button>
<!-- イベント修飾子 -->
<button onclick|once={() => alert('Once!')}>Click once</button>
<form onsubmit|preventDefault={handleSubmit}>...</form>
<!-- カスタムイベント -->
<CustomComponent onmyevent={(e) => console.log(e.detail)} />
クラスコンポーネント
<!-- Counter.svelte -->
<script module>
// モジュールコンテキスト(静的メソッドなど)
export function createCounter(initial = 0) {
return $state(initial);
}
</script>
<script>
let count = $state(0);
// エクスポートされたメソッド
export function increment() {
count++;
}
export function reset() {
count = 0;
}
</script>
<button onclick={increment}>Count: {count}</button>
<!-- 使用側 -->
<script>
import Counter from './Counter.svelte';
let counterRef;
function handleReset() {
counterRef.reset();
}
</script>
<Counter bind:this={counterRef} />
<button onclick={handleReset}>Reset</button>
実践メモ: Svelte 5への移行はnpx sv migrateコマンドで自動化できます。段階的に移行でき、Svelte 4の構文も引き続き動作します。
マイグレーション
自動マイグレーション
# svelte-migrateを使用
npx sv migrate svelte-5
# または特定のファイル
npx sv migrate svelte-5 src/components/Button.svelte
段階的な移行
<!-- 従来の構文も引き続き動作 -->
<script>
// レガシーモード(Runesなし)
let count = 0;
$: doubled = count * 2;
$: console.log(count);
export let name;
</script>
<button on:click={() => count++}>
{count}
</button>
<!-- svelte.config.js でレガシーモードを有効化 -->
// svelte.config.js
export default {
compilerOptions: {
// Runesを無効化してレガシーモードで動作
runes: false
}
};
SvelteKit統合
// +page.svelte
<script>
let { data } = $props();
let posts = $state(data.posts);
let search = $state('');
let filtered = $derived(
posts.filter(p => p.title.toLowerCase().includes(search.toLowerCase()))
);
</script>
<input bind:value={search} placeholder="Search..." />
{#each filtered as post (post.id)}
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
{/each}
注意: Svelte 5では$:リアクティブ宣言は非推奨です。既存コードは動作しますが、新規コードでは$state/$derived/$effectを使用してください。
パフォーマンス改善
Svelte 5 パフォーマンス比較 (js-framework-benchmark)
| 項目 | Svelte 4 | Svelte 5 |
|---|---|---|
| DOM操作速度 | 1.0x | 0.5x (2倍高速) |
| バンドルサイズ (Hello World) | 3.2KB | 2.8KB (-12.5%) |
| メモリ使用量 | 100% | 70% (-30%) |
まとめ
| 機能 | Svelte 4 | Svelte 5 |
|---|---|---|
| リアクティブ変数 | let x = 0 | let x = $state(0) |
| 派生値 | $: y = x * 2 | let y = $derived(x * 2) |
| 副作用 | $: { ... } | $effect(() => { ... }) |
| プロパティ | export let prop | let { prop } = $props() |
| スロット | <slot /> | {@render children()} |
| イベント | on:click | onclick |