Svelte入門 - コンパイラで実現する次世代フロントエンド開発
はじめに
Svelte(スベルト)は、Webアプリケーションを構築するためのUIフレームワークです。公式サイトでは「web development for the rest of us」(私たちのためのWeb開発)と謳っており、GitHubでは85,000以上のスターを獲得している人気プロジェクトです。
ReactやVue.jsと何が違うのかというと、Svelteは「コンパイラ」であるという点なんですよね。従来のフレームワークはブラウザ上で仮想DOMを使って差分を計算しますが、Svelteはビルド時にコンポーネントを最適化されたJavaScriptにコンパイルしてしまう。結果として、ランタイムのオーバーヘッドがほぼゼロになるという話です。
個人的には、2024年のStack OverflowやState of JavaScriptの調査で「最も使いたいフレームワーク」として高評価を得ているのが印象的でした。Spotify、The New York Times、IKEA、Appleといった大手企業でも採用されているので、実績は十分です。
特徴・メリット
1. コンパイラ駆動型アーキテクチャ
Svelteの最大の特徴は、ビルド時にコンポーネントを最適化済みのJavaScriptに変換することです。
- 仮想DOMなし: ランタイムでの差分計算が不要
- バンドルサイズが小さい: フレームワークのランタイムコードを含まない
- 高速な初期表示: コンパイル済みコードがそのまま実行される
正直なところ、Reactのバンドルサイズに悩まされたことがある人なら、この恩恵は大きいと思います。
2. 直感的な構文
HTML、CSS、JavaScriptをそのまま書けるので、学習コストが低いです。
- 単一ファイルコンポーネント:
.svelteファイルにすべてまとめて記述 - 特別な記法が少ない: JSX不要、テンプレート構文もシンプル
- CSSのスコープ化: 自動的にコンポーネントスコープになる
3. 真のリアクティビティ
Svelteのリアクティブシステムは、言語レベルで組み込まれています。
$:構文: リアクティブな宣言を簡潔に記述- ストアの組み込み: 状態管理が標準で用意されている
- 自動的な更新: 変数への代入だけでUIが更新される
4. 優れた開発体験
- 充実したツールチェーン: SvelteKitによるフルスタック開発
- TypeScriptサポート: 型安全な開発が可能
- 活発なコミュニティ: 823人以上のコントリビューター
インストール方法
SvelteKitを使った新規プロジェクト(推奨)
フルスタックアプリケーションを構築する場合は、SvelteKitを使うのが一般的です。
# npmの場合
npx sv create my-app
# pnpmの場合
pnpm create svelte@latest my-app
# yarnの場合
yarn create svelte my-app
対話形式でプロジェクト設定を選択できます:
┌ Welcome to SvelteKit!
│
◇ Which Svelte app template?
│ Skeleton project
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ Select additional options
│ ◼ Add ESLint for code linting
│ ◼ Add Prettier for code formatting
│ ◼ Add Playwright for browser testing
│ ◼ Add Vitest for unit testing
│
└ Your project is ready!
プロジェクト作成後:
cd my-app
npm install
npm run dev
Viteを使った軽量なセットアップ
SvelteKitほどの機能が必要ない場合は、Viteで直接Svelteプロジェクトを作成できます。
npm create vite@latest my-svelte-app -- --template svelte-ts
cd my-svelte-app
npm install
npm run dev
基本的な使い方
コンポーネントの基本構造
Svelteコンポーネントは.svelteファイルに記述します。
<script lang="ts">
// ここにJavaScript/TypeScriptを記述
let count = 0;
function increment() {
count += 1;
}
</script>
<!-- ここにHTMLを記述 -->
<button onclick={increment}>
クリック数: {count}
</button>
<style>
/* ここにCSSを記述(自動的にスコープ化される) */
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
これ、意外とシンプルなんですよね。ReactのJSXやVueのテンプレート構文を覚える必要がなく、ほぼ素のHTMLを書く感覚で進められます。
リアクティブな宣言
変数の値に基づいて自動計算される値を定義できます。
<script lang="ts">
let count = 0;
// countが変更されると自動的に再計算される
$: doubled = count * 2;
$: quadrupled = doubled * 2;
// リアクティブなステートメントも可能
$: if (count >= 10) {
console.log('10回を超えました');
}
</script>
<button onclick={() => count += 1}>
{count} × 2 = {doubled}
</button>
Props(プロパティ)の受け渡し
親コンポーネント(App.svelte)
<script lang="ts">
import Greeting from './Greeting.svelte';
</script>
<Greeting name="Svelte" />
子コンポーネント(Greeting.svelte)
<script lang="ts">
// exportすることでPropsとして受け取れる
export let name: string;
</script>
<h1>Hello, {name}!</h1>
条件分岐とループ
<script lang="ts">
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos: Todo[] = [
{ id: 1, text: 'Svelteを学ぶ', done: true },
{ id: 2, text: 'アプリを作る', done: false },
{ id: 3, text: 'デプロイする', done: false }
];
let showCompleted = true;
</script>
<label>
<input type="checkbox" bind:checked={showCompleted} />
完了済みを表示
</label>
<ul>
{#each todos as todo (todo.id)}
{#if showCompleted || !todo.done}
<li class:done={todo.done}>
{todo.text}
</li>
{/if}
{/each}
</ul>
{#if todos.length === 0}
<p>タスクがありません</p>
{/if}
<style>
.done {
text-decoration: line-through;
color: #999;
}
</style>
イベントハンドリング
<script lang="ts">
let text = '';
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
console.log('送信:', text);
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && event.ctrlKey) {
console.log('Ctrl+Enter が押されました');
}
}
</script>
<form onsubmit={handleSubmit}>
<input
type="text"
bind:value={text}
onkeydown={handleKeydown}
placeholder="テキストを入力"
/>
<button type="submit">送信</button>
</form>
実践的なユースケース
1. ストアを使った状態管理
Svelteには組み込みのストア機能があります。外部ライブラリ不要で状態管理ができるのはコスパ的にも良いですね。
// stores/counter.ts
import { writable, derived } from 'svelte/store';
// 書き込み可能なストア
export const count = writable(0);
// 派生ストア(他のストアから計算される)
export const doubled = derived(count, ($count) => $count * 2);
// カスタムストア
function createCounter() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const counter = createCounter();
<!-- コンポーネントでの使用 -->
<script lang="ts">
import { count, doubled, counter } from './stores/counter';
</script>
<!-- $をつけると自動的にsubscribeされる -->
<p>カウント: {$count}</p>
<p>2倍: {$doubled}</p>
<button onclick={() => $count += 1}>+1</button>
<!-- カスタムストアの使用 -->
<p>カスタムカウンター: {$counter}</p>
<button onclick={counter.increment}>+1</button>
<button onclick={counter.decrement}>-1</button>
<button onclick={counter.reset}>リセット</button>
2. APIデータの取得
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [];
let loading = true;
let error: string | null = null;
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) throw new Error('データの取得に失敗しました');
users = await response.json();
} catch (e) {
error = e instanceof Error ? e.message : '不明なエラー';
} finally {
loading = false;
}
}
// コンポーネントマウント時に実行
fetchUsers();
</script>
{#if loading}
<div class="loading">読み込み中...</div>
{:else if error}
<div class="error">{error}</div>
{:else}
<ul>
{#each users as user (user.id)}
<li>{user.name} ({user.email})</li>
{/each}
</ul>
{/if}
3. フォームバリデーション
<script lang="ts">
let email = '';
let password = '';
let errors: { email?: string; password?: string } = {};
function validate() {
errors = {};
if (!email) {
errors.email = 'メールアドレスは必須です';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = '有効なメールアドレスを入力してください';
}
if (!password) {
errors.password = 'パスワードは必須です';
} else if (password.length < 8) {
errors.password = 'パスワードは8文字以上で入力してください';
}
return Object.keys(errors).length === 0;
}
function handleSubmit() {
if (validate()) {
console.log('送信:', { email, password });
}
}
</script>
<form onsubmit|preventDefault={handleSubmit}>
<div class="field">
<label for="email">メールアドレス</label>
<input
id="email"
type="email"
bind:value={email}
class:error={errors.email}
/>
{#if errors.email}
<span class="error-message">{errors.email}</span>
{/if}
</div>
<div class="field">
<label for="password">パスワード</label>
<input
id="password"
type="password"
bind:value={password}
class:error={errors.password}
/>
{#if errors.password}
<span class="error-message">{errors.password}</span>
{/if}
</div>
<button type="submit">ログイン</button>
</form>
<style>
.field {
margin-bottom: 16px;
}
input.error {
border-color: red;
}
.error-message {
color: red;
font-size: 12px;
}
</style>
4. トランジションとアニメーション
Svelteには組み込みのトランジション機能があります。これが地味に便利なんですよね。
<script lang="ts">
import { fade, fly, slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = true;
let items = ['Item 1', 'Item 2', 'Item 3'];
</script>
<button onclick={() => visible = !visible}>
{visible ? '非表示' : '表示'}
</button>
{#if visible}
<div transition:fade={{ duration: 300 }}>
フェードイン/アウト
</div>
<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}>
フライイン/アウト
</div>
{/if}
<ul>
{#each items as item (item)}
<li transition:slide>
{item}
<button onclick={() => items = items.filter(i => i !== item)}>
削除
</button>
</li>
{/each}
</ul>
まとめ
Svelteは、コンパイラ駆動型という独自のアプローチで、高速かつ軽量なWebアプリケーション開発を実現するフレームワークです。30代になって思うのは、技術選定って「流行っているから」だけじゃなく、実際のプロジェクトに合うかどうかが大事なんですよね。
主なポイントをまとめると:
- コンパイラ型: ビルド時に最適化され、ランタイムオーバーヘッドがほぼゼロ
- 学習しやすい: HTML、CSS、JavaScriptの知識があればすぐに始められる
- シンプルな構文: JSX不要、直感的なテンプレート構文
- 組み込みの機能: ストア、トランジション、アニメーションが標準装備
- TypeScript対応: 型安全な開発が可能
- SvelteKit: フルスタック開発のための公式フレームワーク
パフォーマンス重視のプロジェクトや、バンドルサイズを小さく抑えたい場合には一択ですね。まずは公式チュートリアルから始めてみてはいかがでしょうか。