SvelteKit 2025の概要
SvelteKitは、Svelteをベースにしたフルスタックフレームワークとして、2025年に大きな進化を遂げました。Svelte 5で導入された「Runes」システムと、SvelteKit独自のサーバーサイド機能が融合し、開発者体験とパフォーマンスの両面で他のフレームワークを凌駕する存在となっています。
flowchart TB
subgraph SvelteKit2025["SvelteKit 2025 アーキテクチャ"]
subgraph Compiler["コンパイル時最適化"]
C1["仮想DOMなし"]
C2["直接DOM操作コード生成"]
C3["Tree Shaking最適化"]
C4["デッドコード除去"]
end
subgraph Runes["Svelte 5 Runes"]
R1["$state - リアクティブ状態"]
R2["$derived - 派生値"]
R3["$effect - 副作用"]
R4["$props - コンポーネントプロパティ"]
end
subgraph Server["サーバーサイド機能"]
S1["Server Load Functions"]
S2["Form Actions"]
S3["Remote Functions"]
S4["Streaming SSR"]
end
subgraph New2025["2025年の新機能"]
N1["Async Components"]
N2["$state.eager"]
N3["fork API"]
end
end
なぜSvelteKitが注目されるのか
コンパイラファーストのアプローチ
SvelteKitの最大の特徴は、ReactやVueのような仮想DOMを使用しないことです。Svelteはビルド時にコンポーネントを高度に最適化されたバニラJavaScriptにコンパイルします。これにより、ランタイムオーバーヘッドが最小限に抑えられ、驚異的なパフォーマンスを実現します。
// Svelteのコンパイル結果イメージ
// 仮想DOMの差分計算なしで直接DOM操作
function update(changed, ctx) {
if (changed.count) {
set_data(t1, ctx.count); // 直接テキストノードを更新
}
}
バンドルサイズの圧倒的な軽さ
「Hello World」アプリケーションのバンドルサイズ比較:
| フレームワーク | バンドルサイズ | 備考 |
|---|---|---|
| Svelte 5 | 約1.6KB | ランタイムほぼなし |
| Vue 3 | 約20KB | Composition API含む |
| React 19 | 約42KB | ReactDOM含む |
| Angular 18 | 約130KB | Zone.js含む |
Svelteのバンドルサイズは、Reactの約1/25、Vueの約1/12という驚異的な軽さです。これは、必要なコードのみがコンパイル時に生成されるためです。
Svelte 5 Runesの詳細
$state - 明示的なリアクティブ状態
Svelte 5では、$stateルーンによってリアクティブな状態を明示的に宣言します。これにより、どの変数がリアクティブなのかが一目瞭然になります。
<script>
// プリミティブ値の状態
let count = $state(0);
let message = $state('Hello');
// オブジェクトの状態(ディープリアクティブ)
let user = $state({
name: '田中太郎',
profile: {
age: 28,
city: '東京'
}
});
// 配列の状態
let todos = $state([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: true }
]);
function increment() {
count++; // シンプルな更新
}
function updateCity() {
user.profile.city = '大阪'; // ネストしたプロパティも追跡
}
function addTodo() {
todos.push({
id: Date.now(),
text: '新しいタスク',
done: false
}); // 配列メソッドも自動追跡
}
</script>
<button onclick={increment}>カウント: {count}</button>
<p>都市: {user.profile.city}</p>
$derived - 効率的な派生値
$derivedは、他のリアクティブな値から自動的に計算される派生値を定義します。
<script>
let items = $state([
{ name: 'りんご', price: 150, quantity: 3 },
{ name: 'バナナ', price: 100, quantity: 5 },
{ name: 'オレンジ', price: 200, quantity: 2 }
]);
let taxRate = $state(0.1);
// シンプルな派生値
let itemCount = $derived(items.length);
// 計算を伴う派生値
let subtotal = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// 複数の依存関係を持つ派生値
let total = $derived(subtotal * (1 + taxRate));
// 複雑なロジックには $derived.by を使用
let summary = $derived.by(() => {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const avgPrice = subtotal / totalItems;
return {
totalItems,
avgPrice: Math.round(avgPrice),
mostExpensive: items.reduce((max, item) =>
item.price > max.price ? item : max
)
};
});
</script>
<div>
<p>商品数: {itemCount}種類</p>
<p>小計: {subtotal.toLocaleString()}円</p>
<p>合計(税込): {total.toLocaleString()}円</p>
<p>平均単価: {summary.avgPrice}円</p>
<p>最高額商品: {summary.mostExpensive.name}</p>
</div>
$effect - 副作用の管理
$effectは、リアクティブな値の変更に応じて副作用を実行します。
<script>
let searchQuery = $state('');
let results = $state([]);
let isLoading = $state(false);
// 検索クエリの変更を監視してAPI呼び出し
$effect(() => {
const query = searchQuery; // 依存関係をキャプチャ
if (query.length < 2) {
results = [];
return;
}
isLoading = true;
const controller = new AbortController();
const timeout = setTimeout(async () => {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
});
results = await response.json();
} catch (error) {
if (error.name !== 'AbortError') {
console.error('検索エラー:', error);
}
} finally {
isLoading = false;
}
}, 300);
// クリーンアップ関数
return () => {
clearTimeout(timeout);
controller.abort();
};
});
// ローカルストレージへの永続化
$effect(() => {
localStorage.setItem('lastSearch', searchQuery);
});
</script>
<input
type="text"
bind:value={searchQuery}
placeholder="検索..."
/>
{#if isLoading}
<p>検索中...</p>
{:else}
<ul>
{#each results as result}
<li>{result.title}</li>
{/each}
</ul>
{/if}
SvelteKitのフルスタック機能
Server Load Functions
SvelteKitでは、ページごとにサーバーサイドでデータを取得できます。
// src/routes/posts/+page.server.ts
import { db } from '$lib/server/database';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals, url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const limit = 10;
const [posts, total] = await Promise.all([
db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: limit,
include: {
author: {
select: { name: true, avatar: true }
}
}
}),
db.post.count({ where: { published: true } })
]);
return {
posts,
pagination: {
page,
totalPages: Math.ceil(total / limit),
total
}
};
};
<!-- src/routes/posts/+page.svelte -->
<script>
let { data } = $props();
let posts = $derived(data.posts);
let pagination = $derived(data.pagination);
</script>
<h1>ブログ記事一覧</h1>
{#each posts as post}
<article>
<h2><a href="/posts/{post.slug}">{post.title}</a></h2>
<p>著者: {post.author.name}</p>
</article>
{/each}
<nav>
{#if pagination.page > 1}
<a href="?page={pagination.page - 1}">前へ</a>
{/if}
<span>{pagination.page} / {pagination.totalPages}
{#if pagination.page < pagination.totalPages}
<a href="?page={pagination.page + 1}">次へ</a>
{/if}
</nav>
ポイント: Server Load FunctionsはSSR時にサーバー上で実行され、クライアントにはシリアライズされたデータのみが渡されます。機密データの漏洩を防ぎつつ高速なデータフェッチが可能です。
Form Actions
SvelteKitのForm Actionsは、プログレッシブエンハンスメントを実現します。
// src/routes/contact/+page.server.ts
import { fail } from '@sveltejs/kit';
import { sendEmail } from '$lib/server/email';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name')?.toString();
const email = formData.get('email')?.toString();
const message = formData.get('message')?.toString();
// バリデーション
const errors: Record<string, string> = {};
if (!name || name.length < 2) {
errors.name = '名前は2文字以上で入力してください';
}
if (!email || !email.includes('@')) {
errors.email = '有効なメールアドレスを入力してください';
}
if (!message || message.length < 10) {
errors.message = 'メッセージは10文字以上で入力してください';
}
if (Object.keys(errors).length > 0) {
return fail(400, { errors, values: { name, email, message } });
}
// メール送信
try {
await sendEmail({ name, email, message });
return { success: true };
} catch (error) {
return fail(500, {
errors: { form: '送信に失敗しました。後でもう一度お試しください。' },
values: { name, email, message }
});
}
}
};
<!-- src/routes/contact/+page.svelte -->
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let isSubmitting = $state(false);
</script>
<h1>お問い合わせ</h1>
{#if form?.success}
<div class="success">
メッセージを送信しました。ありがとうございます!
</div>
{:else}
<form
method="POST"
use:enhance={() => {
isSubmitting = true;
return async ({ update }) => {
await update();
isSubmitting = false;
};
}}
>
<div class="field">
<label for="name">お名前</label>
<input
type="text"
id="name"
name="name"
value={form?.values?.name ?? ''}
/>
{#if form?.errors?.name}
<span class="error">{form.errors.name}
{/if}
</div>
<div class="field">
<label for="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={form?.values?.email ?? ''}
/>
{#if form?.errors?.email}
<span class="error">{form.errors.email}
{/if}
</div>
<div class="field">
<label for="message">メッセージ</label>
<textarea
id="message"
name="message"
rows="5"
>{form?.values?.message ?? ''}</textarea>
{#if form?.errors?.message}
<span class="error">{form.errors.message}
{/if}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
{/if}
実践メモ: Form ActionsはJavaScript無効環境でもフォーム送信が動作するプログレッシブエンハンスメントを標準サポートしています。
Remote Functions(2025年新機能)
Remote Functionsは、型安全なサーバー通信を実現する新機能です。
// src/lib/server/api.ts
export async function createPost(title: string, content: string) {
'use server';
const post = await db.post.create({
data: { title, content, authorId: getCurrentUserId() }
});
return post;
}
export async function updatePost(id: string, data: { title?: string; content?: string }) {
'use server';
const post = await db.post.update({
where: { id },
data
});
return post;
}
<script>
import { createPost, updatePost } from '$lib/server/api';
let title = $state('');
let content = $state('');
async function handleSubmit() {
// 型安全にサーバー関数を呼び出し
const post = await createPost(title, content);
console.log('作成完了:', post.id);
}
</script>
2025年の新機能
Async Components
非同期コンポーネントにより、コンポーネント内で直接awaitが使用できるようになりました。
<script>
let { userId } = $props();
// コンポーネント内で直接awaitが使用可能
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
</script>
<div class="profile">
<h1>{user.name}</h1>
<p>{user.bio}</p>
<h2>投稿一覧</h2>
{#each posts as post}
<article>{post.title}</article>
{/each}
</div>
$state.eager
$state.eagerは、UIを即座に更新するための新しいルーンです。
<script>
// 通常の$stateは、awaitの解決を待つ
let data = $state(null);
// $state.eagerは、即座にUIを更新
let optimisticLikes = $state.eager(0);
async function handleLike() {
optimisticLikes++; // 即座にUI更新
await fetch('/api/like', { method: 'POST' });
}
</script>
fork API
forkAPIは、状態変更を「オフスクリーン」でテストできる低レベルAPIです。
import { fork } from 'svelte';
const cleanup = fork(() => {
// この中での状態変更は画面に反映されない
someState = newValue;
// 非同期処理の結果を確認
});
注意: SvelteKitはSvelte 5のRunes構文が前提です。Svelte 4からの移行時は$:構文から$state/$derivedへの変換が必要になります。
React/Vue/Angularとの比較
パフォーマンスベンチマーク(2025年)
js-framework-benchmarkの結果に基づく比較:
| 指標 | Svelte 5 | React 19 | Vue 3.5 | Angular 18 |
|---|---|---|---|---|
| 起動時間 | 1.0x(基準) | 1.8x | 1.4x | 2.2x |
| メモリ使用量 | 1.0x(基準) | 1.5x | 1.3x | 1.8x |
| DOM更新速度 | 1.0x(基準) | 1.3x | 1.2x | 1.4x |
| バンドルサイズ | 3-10KB | 40-100KB | 20-50KB | 100-200KB |
開発者満足度(Stack Overflow 2024)
| フレームワーク | 満足度 | 使用率 |
|---|---|---|
| Svelte | 72.8% | 6.5% |
| React | 62.2% | 39.5% |
| Vue | 60.2% | 15.2% |
| Angular | 53.4% | 17.1% |
フレームワーク選定ガイド
flowchart TD
Start["プロジェクト開始"] --> Q1{"チームの経験は?"}
Q1 -->|"Reactエコシステムに精通"| React["React を検討"]
Q1 -->|"学習コストを最小化したい"| Q2{"パフォーマンス要件は?"}
Q1 -->|"企業標準がある"| Angular["Angular を検討"]
Q2 -->|"最高のパフォーマンスが必要"| Svelte["Svelte/SvelteKit を選択"]
Q2 -->|"バランスを重視"| Vue["Vue を検討"]
React --> Q3{"バンドルサイズは重要?"}
Q3 -->|"はい"| Svelte
Q3 -->|"いいえ"| ReactFinal["React で進める"]
Vue --> Q4{"SSR/フルスタックが必要?"}
Q4 -->|"はい"| Nuxt["Nuxt を使用"]
Q4 -->|"いいえ"| VueFinal["Vue で進める"]
各フレームワークの特徴比較
| 特徴 | Svelte/SvelteKit | React/Next.js | Vue/Nuxt | Angular |
|---|---|---|---|---|
| 学習曲線 | 緩やか | 中程度 | 緩やか | 急峻 |
| 仮想DOM | なし | あり | あり | なし(Signals) |
| 状態管理 | 組み込み(Runes) | 外部ライブラリ必要 | 組み込み(Composition API) | 組み込み(Signals) |
| TypeScript | 優秀 | 優秀 | 優秀 | ネイティブ |
| エコシステム | 成長中 | 最大規模 | 大規模 | 大規模 |
| SSR | 組み込み | Next.js必要 | Nuxt必要 | Angular Universal |
実践的なコード例:Todoアプリ
完全なSvelteKit Todoアプリケーション
// src/lib/server/db.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const db = {
todo: {
async findMany(userId: string) {
return prisma.todo.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
});
},
async create(data: { text: string; userId: string }) {
return prisma.todo.create({ data });
},
async update(id: string, data: { done?: boolean; text?: string }) {
return prisma.todo.update({ where: { id }, data });
},
async delete(id: string) {
return prisma.todo.delete({ where: { id } });
}
}
};
// src/routes/todos/+page.server.ts
import { db } from '$lib/server/db';
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
const todos = await db.todo.findMany(locals.userId);
return { todos };
};
export const actions: Actions = {
create: async ({ request, locals }) => {
const formData = await request.formData();
const text = formData.get('text')?.toString()?.trim();
if (!text || text.length < 1) {
return fail(400, { error: 'タスクを入力してください' });
}
await db.todo.create({ text, userId: locals.userId });
return { success: true };
},
toggle: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id')?.toString();
const done = formData.get('done') === 'true';
if (!id) return fail(400);
await db.todo.update(id, { done: !done });
return { success: true };
},
delete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id')?.toString();
if (!id) return fail(400);
await db.todo.delete(id);
return { success: true };
}
};
<!-- src/routes/todos/+page.svelte -->
<script>
import { enhance } from '$app/forms';
let { data, form } = $props();
let todos = $state(data.todos);
let newTodo = $state('');
let filter = $state('all');
// データが更新されたら同期
$effect(() => {
todos = data.todos;
});
// フィルタリングされたTodo
let filteredTodos = $derived.by(() => {
switch (filter) {
case 'active':
return todos.filter(t => !t.done);
case 'completed':
return todos.filter(t => t.done);
default:
return todos;
}
});
// 統計情報
let stats = $derived({
total: todos.length,
active: todos.filter(t => !t.done).length,
completed: todos.filter(t => t.done).length
});
</script>
<div class="todo-app">
<h1>Todoリスト</h1>
<!-- 新規追加フォーム -->
<form
method="POST"
action="?/create"
use:enhance={() => {
return async ({ update }) => {
await update();
newTodo = '';
};
}}
>
<input
type="text"
name="text"
bind:value={newTodo}
placeholder="新しいタスクを入力..."
/>
<button type="submit">追加</button>
</form>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<!-- フィルター -->
<div class="filters">
<button
class:active={filter === 'all'}
onclick={() => filter = 'all'}
>
すべて ({stats.total})
</button>
<button
class:active={filter === 'active'}
onclick={() => filter = 'active'}
>
未完了 ({stats.active})
</button>
<button
class:active={filter === 'completed'}
onclick={() => filter = 'completed'}
>
完了 ({stats.completed})
</button>
</div>
<!-- Todoリスト -->
<ul class="todo-list">
{#each filteredTodos as todo (todo.id)}
<li class:done={todo.done}>
<form method="POST" action="?/toggle" use:enhance>
<input type="hidden" name="id" value={todo.id} />
<input type="hidden" name="done" value={todo.done} />
<button type="submit" class="toggle">
{todo.done ? '✓' : '○'}
</button>
</form>
<span class="text">{todo.text}
<form method="POST" action="?/delete" use:enhance>
<input type="hidden" name="id" value={todo.id} />
<button type="submit" class="delete">削除</button>
</form>
</li>
{:else}
<li class="empty">タスクがありません</li>
{/each}
</ul>
</div>
<style>
.todo-app {
max-width: 500px;
margin: 2rem auto;
padding: 1rem;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.todo-list li.done .text {
text-decoration: line-through;
color: #999;
}
.filters {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
}
.filters button.active {
background: #007bff;
color: white;
}
</style>
2025年のSvelteKit採用動向
採用企業・プロジェクト
2025年、SvelteKitの採用は着実に増加しています:
- スタートアップ: 軽量さと開発速度を重視するスタートアップでの採用が増加
- メディアサイト: パフォーマンスが重要なニュースサイトやブログプラットフォーム
- Eコマース: Core Web Vitalsのスコア改善を目指すECサイト
- ダッシュボード: 社内ツールやデータ可視化アプリケーション
エコシステムの成長
| カテゴリ | 主要ライブラリ |
|---|---|
| UIコンポーネント | Skeleton, Melt UI, Bits UI |
| フォーム | Superforms, Formsnap |
| 認証 | Lucia, Auth.js |
| ORM | Drizzle, Prisma |
| テスト | Vitest, Playwright |
学習リソース
まとめ
SvelteKit 2025は、以下の点で他のフレームワークと一線を画しています:
- 圧倒的なパフォーマンス: 仮想DOMなし、コンパイル時最適化により最速のWeb体験を実現
- 直感的なリアクティビティ: Runesによる明示的で予測可能な状態管理
- フルスタック統合: Server Load Functions、Form Actions、Remote Functionsによるシームレスなサーバー連携
- 最小のバンドルサイズ: Reactの1/25、Vueの1/12という驚異的な軽さ
- 優れた開発者体験: 学習曲線が緩やかで、TypeScriptサポートも充実
2025年、パフォーマンスと開発者体験の両方を追求するプロジェクトにとって、SvelteKitは最有力の選択肢となっています。