Next.jsとSvelteKitのコーディング的な違い

Alcogyはもともと React / Next.js を主軸に開発していましたが、Svelte 5になってからの開発体験の向上を受けて、現在はSvelteKitに完全に移行しています。仮想DOMとコンパイラという実行モデルの違いや、Vercelのゼロコンフィグデプロイのようなインフラ面の話は世の中に情報が多いので、ここでは実際にコードを書く上での違いに絞って整理します。

サーバー/クライアントの区別: ディレクティブかファイルか

Next.js(App Router)は、コンポーネントファイルの先頭に'use client' / 'use server'というディレクティブを書くことで、そのモジュールがどちらの環境で実行されるかを宣言します。同じ.tsxの中に両方の書き方が混在しうるため、あるファイルがサーバー専用なのかクライアントに送られるのかは、ディレクティブを見ないと判断できません。

SvelteKitはこれをファイル名で区別します。+page.svelteはクライアントに届くビューで、+page.server.tsはサーバーだけで実行されるload関数やフォームのactionsを書く場所です。実行環境の境界がファイルシステム上に表れているため、プロジェクトに慣れていないメンバーでも「このファイルはサーバー専用」と一目で判断できます。

src/routes/orders/
├── +page.svelte       # クライアントに届くビュー
├── +page.server.ts    # サーバー専用のload・actions
└── +page.ts           # サーバー/クライアント両方で実行されるload

メモ化: 手動で書くか、コンパイラに任せるか

Reactは状態が変わるとコンポーネント関数全体が再実行され、返されたJSXと前回の出力を仮想DOM上で比較(diff)します。この再計算コストを抑えるために、useMemouseCallbackReact.memoといったAPIを使って、開発者が明示的にメモ化のポイントを指定する必要があります。

// React: 依存配列を自分で管理する必要がある
const total = useMemo(() => items.reduce((a, b) => a + b.price, 0), [items]);
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);

Svelteは$state / $derivedによるコンパイラベースの細粒度リアクティビティを採用しているため、totalのような派生値は依存関係を自動的に追跡し、実際に依存する値が変わったときだけ再計算されます。

<script>
  let items = $state([...]);
  // 依存配列を書く必要がない。itemsが変わったときだけ再計算される
  let total = $derived(items.reduce((a, b) => a + b.price, 0));
</script>

useMemoを書き忘れて無駄な再計算が走る、依存配列の書き漏れでバグる、といったReactでよくあるハマりどころが、SvelteKitではそもそも発生しません。

グローバルなstate管理

Next.jsでコンポーネントをまたいで状態を共有する場合、React標準のContextだけでは再レンダリングの範囲を細かく制御しづらいため、ZustandやJotai、Reduxといった外部の状態管理ライブラリに頼ることが多くなります。

SvelteKitでは、.svelte.tsファイルで$stateを使ってオブジェクトをエクスポートするだけで、アプリ全体から参照できるリアクティブな状態を作れます。

// lib/cart.svelte.ts
export const cart = $state({ items: [] as string[] });

export function addItem(id: string) {
  cart.items.push(id);
}
<script>
  import { cart } from '$lib/cart.svelte.ts';
</script>

<p>カート内: {cart.items.length}件</p>

外部ライブラリを追加する必要がなく、フレームワーク標準の機能だけでグローバルなstateが完結する点は、コーディング体験として大きな違いです(ただしサーバーサイドで共有変数として使うとユーザー間でデータが漏れるため、サーバー上での扱いには注意が必要です)。

フォームの送信: Server Actionsか、Form Actionsか

Next.jsのServer Actionsは、'use server'ディレクティブを付けた非同期関数を、フォームのactionpropに直接渡す書き方です。

// Next.js
async function createOrder(formData: FormData) {
  'use server';
  await db.orders.create({ ... });
}

export default function Page() {
  return <form action={createOrder}>...</form>;
}

SvelteKitでは、+page.server.tsactionsオブジェクトをexportし、+page.svelte側は素の<form method="POST">を書くだけで動きます。

// +page.server.ts
export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    // ...
  }
};
<!-- +page.svelte -->
<form method="POST" use:enhance>
  <input name="title" />
  <button>送信</button>
</form>

use:enhanceを付けるとfetchベースの送信に自動で切り替わりますが、付けなくてもブラウザ標準のフォーム送信として機能します。JavaScriptが読み込まれる前でもフォームが動く、という前提でコードが書けるのはSvelteKitらしい部分です。

スタイルのスコープ

Reactにはコンポーネント単位でCSSをスコープする仕組みが標準では無いため、CSS Modules・styled-components・Tailwindといった別のツールを選定して導入する必要があります。

Svelteは.svelteファイル内の<style>ブロックが、ビルド時に自動でそのコンポーネントだけに効くクラス(ハッシュ付きのクラス名)に変換されます。

<style>
  /* このコンポーネント内のpタグにしか効かない */
  p {
    color: burlywood;
  }
</style>

追加のライブラリなしで、素のCSSを書くだけでスコープが手に入るのは、セットアップの手間という意味でもコーディング体験の違いとして大きいです。

さらにsass-embeddedを導入すれば、<style lang="scss">と書くだけでSCSSの記法もそのままスコープの恩恵を受けながら使えます。

<style lang="scss">
  .card {
    padding: 1rem;

    /* ネストや変数など、SCSSの書き方をそのまま持ち込める */
    &:hover {
      background: var(--color-surface);
    }
  }
</style>

CSS Modulesのようにクラス名の衝突を気にする必要がなく、スコープの仕組みの上にそのままSCSSの書き方を重ねられるのは、既存のSCSS資産があるチームにとって移行のハードルを下げてくれます。

フォーム入力の双方向バインディング

Reactの制御コンポーネントは、valueonChangeをセットで書く必要があります。

const [text, setText] = useState('');
<input value={text} onChange={(e) => setText(e.target.value)} />

Svelteはbind:valueで同じことを1行で書けます。

<script>
  let text = $state('');
</script>
<input bind:value={text} />

入力欄が多いフォームほど、この差はコード量に直結します。

型: 自動生成されるか、手で書くか

SvelteKitは各ルートのload関数・actionsの戻り値の型を解析し、./$typesとしてPageData / PageServerLoad / Actionsなどを自動生成します。+page.svelte側はlet { data }: PageProps = $props();と書くだけで、loadが返したデータの型がそのまま反映されます。Next.jsでも型は使えますが、paramssearchParams、Server Actionsの戻り値などを一致させる型定義は、基本的に自分で書いて揃える必要があります。

Next.jsが勝る部分

ここまでコーディング面でのSvelteKitの優位性を中心に書いてきましたが、Next.jsに分がある部分もあります。

Vercelとの相性: Next.jsはVercel社が開発しているフレームワークで、Vercelにデプロイすることを前提にした機能(Incremental Static Regeneration、revalidatePath / revalidateTagによるきめ細かいキャッシュ制御、next/imageの最適化など)が、コードを書くだけでそのままインフラ側の恩恵につながるように作り込まれています。SvelteKitもCloudflareやVercelなど複数のプラットフォームにアダプター経由でデプロイできますが、特定プラットフォームへの垂直統合の深さという点では、自社製フレームワークであるNext.js/Vercelの組み合わせに一日の長があります。

大規模システムへの対応: 数百人規模のチームが同じコードベースを触るような大規模プロジェクトでは、キャッシュ戦略の細かい制御手段や、Turborepoのようなモノレポツールとの連携など、長年の運用の中で積み上げられたパターンやベストプラクティスの蓄積がものを言います。この点でもNext.js/Reactエコシステムの方が事例・知見ともに豊富です。SvelteKitも実用上十分な規模のシステムを支えられますが、「数百ページ・数十チーム」級の超大規模運用における実績の厚みでは、まだNext.jsに分があると感じています。

採用事例: Netflix・TikTok・Twitchなど、大規模なトラフィックを持つサービスでの採用例が豊富にあり、それらの知見がブログや登壇資料として公開されているため、大規模運用のリファレンスに困りません。SvelteKitも採用企業は着実に増えていますが、事例の絶対数ではまだNext.jsに及びません。

開発者・コミュニティの規模: Reactのエコシステムは世界的に見ても最大級の規模があり、Next.js固有の疑問についても検索すればたいてい答えが見つかります。外部ライブラリのNext.js対応も早く、採用市場でReact/Next.js経験者を見つけやすいという点も、プロジェクトを長期で運営する上では無視できない強みです。SvelteKitのコミュニティも活発に成長していますが、エコシステムの厚みという意味ではまだReact/Next.jsが優位です。

こうした強みは主に「フレームワークを取り巻く環境」の話であり、今回スコープにしているコーディングそのものの快適さとは別軸の評価になります。Alcogyがそれでもコーディング体験を重視してSvelteKitを選んでいるのは、日々書くコードの量とハマりどころの少なさが、日常の開発速度に直結すると考えているためです。

まとめ

ディレクティブかファイルか、手動メモ化かコンパイラ任せか、外部ライブラリか標準機能か——挙げていくとSvelteKitの方がフレームワーク標準の機能でカバーできる範囲が広く、追加のライブラリ選定や設定に使う時間が少なくて済む場面が多いというのが実感です。一方で、Vercelとの統合の深さや大規模運用の実績、コミュニティの厚みといった面では、まだNext.js/Reactに分があります。AlcogyがNext.jsからSvelteKitに完全移行した理由は、こうした周辺環境の強みよりも、日々のコーディング体験の快適さを優先した結果だと捉えています。