JavaScript

一歩先行く開発者必見!SvelteKitで実現する高速SSR×SPA【完全ガイド】

1️⃣ はじめに:なぜSvelteKitが来るのか

JavaScriptフレームワーク戦国時代、ReactやVueの次に脚光を浴びるのがSvelteKitです。従来のSSRSPAは相反するアーキテクチャとされていましたが、SvelteKitはその両立を軽量かつ高速に実現します。

  • バンドル前コンパイル:余計なランタイムが不要
  • ファイルベースルーティング:設定不要で簡潔
  • load関数:組み込みでSSRデータフェッチ

国内情報はまだ少なく、アーリーアダプター向けの先行者利益を狙える技術です。


2️⃣ 開発環境の準備

必要なもの

  • Node.js 16以上
  • Git
  • 任意のコードエディタ (VSCode推奨)

SvelteKit プロジェクト作成

npm init svelte@next my-app
cd my-app
npm install
npm install -D @types/node
npm run dev -- --open

次のようなディレクトリが自動生成されます:

my-app/
├ src/
│  ├ lib/
│  ├ routes/
│  ├ app.html
├ static/
└ svelte.config.js

3️⃣ Hello World:最速スターティング

src/routes/+page.svelte

<script lang="ts">
  let name = '世界';
</script>

<h1>こんにちは、{name}!</h1>
<input bind:value={name} placeholder="名前を入力" />

解説

  • <script lang="ts"> で TypeScript対応
  • bind:value による双方向バインディング
  • ファイル名 +page.svelte がルート / にマッピング

ブラウザでフォームに文字を打つと、即時にUIが更新されることを確認しましょう。


4️⃣ ファイルベースルーティング

ネストされたルート

src/routes/
├ +page.svelte      --> /
└ about/
   └ +page.svelte   --> /about

アクセスすると、それぞれ違うコンポーネントが表示されます。

動的ルート

src/routes/blog/[slug]/+page.svelte

<script lang="ts">
  export let params: { slug: string };
</script>
<h1>記事: {params.slug}</h1>

このように [] で囲むと動的URLを簡単実装できます。


5️⃣ SSR入門:load 関数

サーバー側でデータを取得し、初期HTML に埋め込む方法です。

src/routes/users/+page.ts

import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch }) => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();
  return { users };
};

src/routes/users/+page.svelte

<script lang="ts">
  export let data: { users: { id: number; name: string }[] };
</script>
<ul>
  {#each data.users as user}
    <li>{user.name}</li>
  {/each}
</ul>

解説

  • load でフェッチ → SSR
  • ブラウザに届くHTMLに既にユーザー一覧がレンダリングされる
  • SEO・初期表示速度 激的に向上

6️⃣ SPA化:クライアントサイドナビゲーション

ページ間移動が フルページリロードなし に。

標準リンク

<script>
  import { goto } from '$app/navigation';
</script>
<button on:click={() => goto('/about')}>Aboutへ</button>

または

<a href="/about" sveltekit:prefetch>Aboutへ</a>

解説

  • goto はプログラム的に遷移
  • sveltekit:prefetch でホバー時に事前取得
  • ページ遷移が即時かつスムーズ

7️⃣ APIフェッチの使い分け

  • SSR (load 関数) → SEO重視・初期表示
  • CSR (onMount) → インタラクティブ要素の更新

CSR例

<script lang="ts">
  import { onMount } from 'svelte';
  let post;
  onMount(async () => {
    const res = await fetch('/api/random');
    post = await res.json();
  });
</script>
{#if post}
  <article>{post.title}</article>
{:else}
  <p>読み込み中…</p>
{/if}

8️⃣ デプロイ:Vercel / Netlify

Vercel

npm i -g vercel
vercel login
vercel --prod

Netlify

GitHub連携で Automatic Deploy を設定。


9️⃣ 応用編:認証付きSSR/DB接続

+page.server.ts

import { redirect } from '@sveltejs/kit';
export const load = async ({ locals }) => {
  if (!locals.user) throw redirect(302, '/login');
  const todos = await db.getTodos(locals.user.id);
  return { todos };
};

解説

  • localsセッション情報 を SSR 時に受け取る
  • 未ログイン ユーザーをクライアント前に リダイレクト

🔚 まとめ & 次のステップ

  • SvelteKit は SSR×SPA を両立 する次世代フレームワーク
  • load, goto, prefetch など独自 API が強力
  • デプロイも数コマンドで完了 ⇒ 開発〜運用 の効率化

次は Edge RuntimePrerenderingGraphQL 接続などを解説予定です!

おまけ:リアルタイム Todo アプリ構築

SvelteKit の SSR × SPA 機能 を活かして、リアルタイム同期付きの Todo アプリを作成します。

  • SSR で初期データを配信
  • SPA でクライアント側操作を即時反映
  • WebSocket (Socket.IO) で多人数同時編集対応

🏗️ 構成概要

Client (Browser) SvelteKit SSR & SPA Socket.IO Client HTTP (REST/SWR) API Layer SvelteKit Endpoints CRUD (Prisma) SQLite (Prisma) Database SQLite + Prisma WebSocket (Socket.IO) Socket.IO Server Broadcast Events
技術
フロントエンドSvelteKit (+ TypeScript)
API 層SvelteKit Endpoints (src/routes/api)
リアルタイム同期Socket.IO (server + client)
データベースSQLite + Prisma ORM

データベース設定 (Prisma)

npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite

prisma/schema.prisma:

datasource db { provider = "sqlite" url = "file:./dev.db" }

generator client { provider = "prisma-client-js" }

model Todo {
  id        Int      @id @default(autoincrement())
  text      String
  done      Boolean  @default(false)
  createdAt DateTime @default(now())
}
npx prisma migrate dev --name init

API エンドポイント (CRUD)

src/routes/api/todos/+server.ts

import { json } from '@sveltejs/kit';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function GET() {
  const todos = await prisma.todo.findMany({ orderBy: { createdAt: 'desc' } });
  return json({ todos });
}

export async function POST({ request }) {
  const { text } = await request.json();
  const todo = await prisma.todo.create({ data: { text } });
  return json({ todo });
}

export async function PUT({ request }) {
  const { id, done } = await request.json();
  const todo = await prisma.todo.update({ where: { id }, data: { done } });
  return json({ todo });
}

export async function DELETE({ request }) {
  const { id } = await request.json();
  await prisma.todo.delete({ where: { id } });
  return json({ success: true });
}

解説:

  • +server.ts ファイルベースで REST API を実装
  • GET で初回 SSR 配信用データ取得
  • POST/PUT/DELETE でデータ操作後クライアントに JSON を返却

Socket.IO サーバー統合

src/hooks.server.ts

import { sequence } from '@sveltejs/kit/hooks';
import { Server } from 'socket.io';
import type { Handle } from '@sveltejs/kit';

let io: Server;

export const handle: Handle = sequence(async ({ event, resolve }) => {
  if (!io) {
    const server = await event.platform?.node?.httpServer; // Vercel 以外
    io = new Server(server);
    io.on('connection', socket => {
      socket.on('todo:update', data => {
        socket.broadcast.emit('todo:update', data);
      });
    });
  }
  return resolve(event);
});

解説:

  • hooks.server.ts で初回リクエスト時に Socket.IO サーバーを起動
  • todo:update イベントをクライアント間でブロードキャスト

フロントエンド実装

src/routes/+page.svelte

<script lang="ts">
  import { onMount } from 'svelte';
  import io from 'socket.io-client';
  let todos = [];
  let text = '';
  const socket = io();

  async function fetchTodos() {
    const res = await fetch('/api/todos');
    todos = (await res.json()).todos;
  }

  async function addTodo() {
    const res = await fetch('/api/todos', { method:'POST', body:JSON.stringify({ text }) });
    const { todo } = await res.json();
    todos = [todo, ...todos];
    socket.emit('todo:update', { action: 'add', todo });
    text = '';
  }

  function toggleDone(id) {
    const todo = todos.find(t => t.id === id);
    fetch('/api/todos', { method:'PUT', body:JSON.stringify({ id, done: !todo.done }) });
    todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t);
    socket.emit('todo:update', { action: 'toggle', id });
  }

  onMount(() => {
    fetchTodos();
    socket.on('todo:update', data => {
      if (data.action === 'add') todos = [data.todo, ...todos];
      if (data.action === 'toggle') todos = todos.map(t => t.id === data.id ? { ...t, done: !t.done } : t);
    });
  });
</script>

<input bind:value={text} placeholder="新しいTODOを入力" />
<button on:click={addTodo}>追加</button>
<ul>
  {#each todos as { id, text, done }}
    <li on:click={() => toggleDone(id)} class:done={done}>{text}</li>
  {/each}
</ul>

<style>
  .done { text-decoration: line-through; color: #888; }
</style>

解説:

  • SSR で取得した todosonMount で初期化し、画面に描画
  • CRUD API コール後に Socket.IO で他クライアントへ変更通知
  • リアクティブ変数 todos の更新で即時 UI 反映