Svelte の使い方 — コンパイラベースのUIフレームワーク完全ガイド
一言でいうと
Svelteは、宣言的に書いたコンポーネントをビルド時に最適化されたバニラJavaScriptへコンパイルするUIフレームワークです。仮想DOMを使わず、DOMを直接・外科的に更新するため、ランタイムのオーバーヘッドが極めて小さいのが最大の特徴です。
どんな時に使う?
- パフォーマンス重視のWebアプリケーション開発 — バンドルサイズを最小限に抑えたいSPA・MPA構築時。仮想DOMのランタイムコストを排除できます
- SvelteKitと組み合わせたフルスタック開発 — SSR/SSG/SPAを柔軟に切り替えられるメタフレームワークSvelteKitの基盤として使用
- インタラクティブなUIコンポーネントの構築 — リアクティブな状態管理が言語レベルで組み込まれており、少ないボイラープレートで複雑なUIを実装可能
インストール
※ 本記事はSvelte 5系(v5.55.1時点)を対象としています。Svelte 4以前とはリアクティビティの仕組みが大きく異なります。
通常はSvelteKitのプロジェクト作成コマンドを使うのが推奨です:
npx sv create my-app
cd my-app
npm install
npm run dev
既存プロジェクトにSvelte単体を追加する場合:
# npm
npm install svelte
# yarn
yarn add svelte
# pnpm
pnpm add svelte
Viteと組み合わせる場合は @sveltejs/vite-plugin-svelte も必要です:
npm install --save-dev @sveltejs/vite-plugin-svelte
基本的な使い方
Svelteコンポーネントは .svelte ファイルに記述します。HTML・CSS・JavaScriptが1ファイルにまとまる単一ファイルコンポーネント形式です。
最もシンプルなカウンターの例
<!-- Counter.svelte -->
<script lang="ts">
let count: number = $state(0);
function increment(): void {
count++;
}
</script>
<button onclick={increment}>
クリック回数: {count}
</button>
<style>
button {
padding: 0.5rem 1rem;
font-size: 1.2rem;
border-radius: 4px;
cursor: pointer;
}
</style>
Propsを受け取るコンポーネント
<!-- Greeting.svelte -->
<script lang="ts">
interface Props {
name: string;
greeting?: string;
}
let { name, greeting = 'こんにちは' }: Props = $props();
</script>
<p>{greeting}、{name}さん!</p>
親コンポーネントからの利用
<!-- App.svelte -->
<script lang="ts">
import Counter from './Counter.svelte';
import Greeting from './Greeting.svelte';
</script>
<Greeting name="田中" />
<Greeting name="鈴木" greeting="おはよう" />
<Counter />
よく使うAPI — Svelte 5 Runes(ルーン)の使い方
Svelte 5では「Runes」と呼ばれる $ プレフィックス付きのコンパイラ指示子がリアクティビティの中核です。
1. $state — リアクティブな状態の宣言
最も基本的なルーンです。値が変更されると、それを参照しているUIが自動的に更新されます。
<script lang="ts">
// プリミティブ値
let name: string = $state('Svelte');
// オブジェクト(ディープリアクティブ)
let user = $state({
name: '田中太郎',
age: 30,
hobbies: ['読書', 'プログラミング']
});
function addHobby(): void {
// 配列のpushも検知される(ディープリアクティビティ)
user.hobbies.push('ゲーム');
}
</script>
<p>{user.name}({user.age}歳)</p>
<ul>
{#each user.hobbies as hobby}
<li>{hobby}</li>
{/each}
</ul>
<button onclick={addHobby}>趣味を追加</button>
2. $derived — 派生状態(算出値)
他のリアクティブな値から自動計算される値を定義します。依存する値が変わると自動的に再計算されます。
<script lang="ts">
let items = $state([
{ name: 'りんご', price: 150, quantity: 3 },
{ name: 'バナナ', price: 100, quantity: 5 },
{ name: 'みかん', price: 80, quantity: 10 }
]);
// 単純な派生値
let totalItems: number = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
// 複雑なロジックには $derived.by を使用
let totalPrice: number = $derived.by(() => {
let sum = 0;
for (const item of items) {
sum += item.price * item.quantity;
}
return sum;
});
</script>
<p>合計 {totalItems} 個 / {totalPrice.toLocaleString()} 円</p>
3. $effect — 副作用の実行
リアクティブな値の変更に応じて副作用を実行します。コンポーネントのマウント時にも実行され、アンマウント時にはクリーンアップ関数が呼ばれます。
<script lang="ts">
let query: string = $state('');
let results: string[] = $state([]);
// queryが変わるたびにAPIを呼び出す
$effect(() => {
if (query.length < 2) {
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);
});
// クリーンアップ関数(次回実行前 or アンマウント時に呼ばれる)
return () => {
controller.abort();
};
});
</script>
<input type="text" bind:value={query} placeholder="検索..." />
<ul>
{#each results as result}
<li>{result}</li>
{/each}
</ul>
4. $props — コンポーネントプロパティ
親コンポーネントから渡されるプロパティを受け取ります。TypeScriptの型定義と自然に統合できます。
<!-- UserCard.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
name: string;
email: string;
role?: 'admin' | 'user' | 'guest';
onDelete?: (name: string) => void;
children: Snippet;
}
let {
name,
email,
role = 'user',
onDelete,
children
}: Props = $props();
</script>
<div class="card">
<h3>{name}</h3>
<p>{email}</p>
<span class="badge">{role}</span>
<!-- Snippet(子コンテンツ)のレンダリング -->
{@render children()}
{#if onDelete}
<button onclick={() => onDelete(name)}>削除</button>
{/if}
</div>
親からの利用:
<UserCard
name="田中太郎"
email="tanaka@example.com"
role="admin"
onDelete={(name) => console.log(`${name}を削除`)}
>
<p>追加の情報をここに記述できます。</p>
</UserCard>
5. $bindable — 双方向バインディング可能なProp
親コンポーネントから bind: で双方向バインディングできるプロパティを定義します。
<!-- TextInput.svelte -->
<script lang="ts">
interface Props {
value: string;
label?: string;
}
let { value = $bindable(''), label = '' }: Props = $props();
</script>
<label>
{#if label}{label}{/if}
<input type="text" bind:value />
</label>
<!-- 親コンポーネント -->
<script lang="ts">
import TextInput from './TextInput.svelte';
let username: string = $state('');
</script>
<TextInput bind:value={username} label="ユーザー名" />
<p>入力値: {username}</p>
類似パッケージとの比較
| 特徴 | Svelte 5 | React 19 | Vue 3 | Solid.js |
|---|---|---|---|---|
| レンダリング方式 | コンパイル時変換 | 仮想DOM | 仮想DOM + コンパイラ最適化 | コンパイル時変換(Fine-grained) |
| ランタイムサイズ | 極小(〜2KB) | 約40KB+ | 約33KB+ | 約7KB |
| リアクティビティ | Runes($state等) | Hooks(useState等) | Composition API(ref等) | Signals(createSignal等) |
| 学習コスト | 低い | 中程度 | 中程度 | 中程度 |
| TypeScript対応 | ◎(ネイティブ) | ◎ | ◎ | ◎ |
| メタフレームワーク | SvelteKit | Next.js / Remix | Nuxt | SolidStart |
| エコシステム規模 | 中 | 非常に大きい | 大きい | 小〜中 |
.svelte独自構文 | あり | なし(JSX) | .vueあり | なし(JSX) |
選定の指針:
- エコシステムの豊富さを重視するならReact
- バンドルサイズとパフォーマンスを最優先するならSvelteまたはSolid
- 既存のVue資産があるならVue 3
- 少ないコード量で直感的に書きたいならSvelte
注意点・Tips
1. Svelte 4からの移行に注意
Svelte 5ではリアクティビティの仕組みが根本的に変わりました。letによる暗黙的リアクティビティ(Svelte 4)から、明示的な$state/$derivedルーン(Svelte 5)への移行が必要です。公式のマイグレーションガイドを参照してください。
<!-- ❌ Svelte 4 スタイル(5でも動くがレガシー) -->
<script>
let count = 0; // 暗黙的リアクティブ
$: doubled = count * 2; // リアクティブ宣言
</script>
<!-- ✅ Svelte 5 スタイル -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
2. $effectの使いすぎに注意
$effectはReactのuseEffectと似ていますが、依存配列を手動で指定する必要がありません(自動追跡)。ただし、状態の同期には$derivedを優先し、$effectは本当の副作用(DOM操作、API呼び出し、ログ出力など)にのみ使いましょう。
<script lang="ts">
let firstName: string = $state('太郎');
let lastName: string = $state('田中');
// ❌ $effectで状態を同期するのはアンチパターン
// let fullName = $state('');
// $effect(() => { fullName = `${lastName} ${firstName}`; });
// ✅ $derivedを使う
let fullName: string = $derived(`${lastName} ${firstName}`);
</script>
3. $stateのディープリアクティビティを理解する
$stateで宣言したオブジェクトや配列はProxyでラップされ、ネストしたプロパティの変更も自動検知されます。ただし、Proxyを意識する場面もあります。
<script lang="ts">
let items = $state([1, 2, 3]);
// ✅ 直接変更が検知される
function addItem() {
items.push(4);
}
// ⚠️ Proxyされたオブジェクトを外部ライブラリに渡す場合、
// $state.snapshot() で生のオブジェクトを取得する
function sendToApi() {
const raw = $state.snapshot(items);
fetch('/api/items', {
method: 'POST',
body: JSON.stringify(raw)
});
}
</script>
4. CSSのスコーピングはデフォルトで有効
<style>ブロック内のCSSは自動的にそ