Svelte 5正式リリース - Runesによる新しいリアクティビティシステム

2025.12.02

公式ドキュメント

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 4Svelte 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 4Svelte 5
DOM操作速度1.0x0.5x (2倍高速)
バンドルサイズ (Hello World)3.2KB2.8KB (-12.5%)
メモリ使用量100%70% (-30%)

まとめ

機能Svelte 4Svelte 5
リアクティブ変数let x = 0let x = $state(0)
派生値$: y = x * 2let y = $derived(x * 2)
副作用$: { ... }$effect(() => { ... })
プロパティexport let proplet { prop } = $props()
スロット<slot />{@render children()}
イベントon:clickonclick

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR