SvelteKit 2025 - コンパイル時最適化の軽量フレームワーク

2026.01.12

公式ドキュメント

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約20KBComposition API含む
React 19約42KBReactDOM含む
Angular 18約130KBZone.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 5React 19Vue 3.5Angular 18
起動時間1.0x(基準)1.8x1.4x2.2x
メモリ使用量1.0x(基準)1.5x1.3x1.8x
DOM更新速度1.0x(基準)1.3x1.2x1.4x
バンドルサイズ3-10KB40-100KB20-50KB100-200KB

開発者満足度(Stack Overflow 2024)

フレームワーク満足度使用率
Svelte72.8%6.5%
React62.2%39.5%
Vue60.2%15.2%
Angular53.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/SvelteKitReact/Next.jsVue/NuxtAngular
学習曲線緩やか中程度緩やか急峻
仮想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
ORMDrizzle, Prisma
テストVitest, Playwright

学習リソース

まとめ

SvelteKit 2025は、以下の点で他のフレームワークと一線を画しています:

  1. 圧倒的なパフォーマンス: 仮想DOMなし、コンパイル時最適化により最速のWeb体験を実現
  2. 直感的なリアクティビティ: Runesによる明示的で予測可能な状態管理
  3. フルスタック統合: Server Load Functions、Form Actions、Remote Functionsによるシームレスなサーバー連携
  4. 最小のバンドルサイズ: Reactの1/25、Vueの1/12という驚異的な軽さ
  5. 優れた開発者体験: 学習曲線が緩やかで、TypeScriptサポートも充実

2025年、パフォーマンスと開発者体験の両方を追求するプロジェクトにとって、SvelteKitは最有力の選択肢となっています。

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR