2つのフレームワーク、2つの哲学

SvelteとReactは、どちらも「UIを宣言的に書く」という同じゴールを目指していますが、 そこに辿り着くまでのアプローチが真逆と言っていいほど違います。 React は「仮想DOMとランタイムでUI更新を吸収する」設計、 Svelte は「コンパイラがビルド時に必要なDOM操作を生成する」設計です。

この記事では、Svelte 5(runes)と React 19 を比較対象に、リアクティビティの仕組み・ コンパイル戦略・コンポーネントの書き方・実務での選定基準を順番に解きほぐしていきます。 「どちらが優れているか」ではなく、「どこで設計判断が分かれているか」を理解することが目的です。

リアクティビティモデル — どう変更を伝播させるか

両者を分かつ最大の違いが「状態が変わったとき、UIをどう更新するか」の設計です。

Reactの再レンダリングモデル

Reactは「コンポーネント関数を再実行する」ことで状態とUIを同期させます。 useStateのセッターが呼ばれると、Reactは該当コンポーネント以下を再実行し、 返却されたJSX(仮想DOM)を前回分とdiffして、差分だけを実DOMに反映します。 つまり「全部書き直して、変わったところだけ反映する」戦略です。

React 19から同梱されたReact Compilerは、依存配列を自動推論してuseMemouseCallback相当の メモ化を裏で挿入するツールです。「再レンダリングは起きるが、計算結果はキャッシュされる」状態を、 開発者が手で書かなくても済むようになりました。プログラミングモデル自体は変わっていません。

Svelte 5のrunesモデル

Svelte 5はシグナルベースのリアクティビティを採用しています。 $stateで宣言した値は依存関係を自動追跡し、変更があるとその値を使っている箇所だけがピンポイントで更新されます。 コンポーネント関数の再実行は発生しません。

<script>
  // Svelte 5 — runes記法
  let count = $state(0);
  let doubled = $derived(count * 2);

  $effect(() => {
    // count または doubled が変わったとき、ここだけが再評価される
    console.log('count changed:', count);
  });
</script>

<button onclick={() => count++}>
  {count} / doubled: {doubled}
</button>

同じ機能をReactで書くとこうなります。

import { useState, useEffect, useMemo } from 'react';

// React 19 — React Compilerがあればメモ化は自動
export function Counter() {
  const [count, setCount] = useState(0);
  const doubled = count * 2; // Compilerが必要に応じてメモ化

  useEffect(() => {
    console.log('count changed:', count);
  }, [count]);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count} / doubled: {doubled}
    </button>
  );
}

更新フローの可視化

flowchart TB
    subgraph React["React: 再レンダリングモデル"]
        R1[setState 呼び出し] --> R2[コンポーネント関数を再実行]
        R2 --> R3[新しい仮想DOMを生成]
        R3 --> R4[前回の仮想DOMとdiff]
        R4 --> R5[差分のみを実DOMに反映]
    end
    subgraph Svelte["Svelte 5: シグナルモデル"]
        S1["$state 値の更新"] --> S2[依存追跡された場所を特定]
        S2 --> S3[該当DOMノードを直接書き換え]
    end

コンパイル戦略 — ブラウザに何が届くか

リアクティビティモデルの違いは、ブラウザに送られるコードの形にも直結します。

Reactは「ランタイムを送る」

Reactは仮想DOMの差分計算・スケジューラ・フック管理など、UI更新ロジックの大半を実行時のライブラリとして持ちます。 あなたが書いたJSXはBabel/SWCでReact.createElement呼び出しに変換され、 そのオブジェクトツリーをReactランタイムが解釈してDOMを更新します。 この方式の代償がmin+gz でおよそ45KBのランタイムです。

Svelteは「コンパイルしてDOM操作を生成する」

Svelteのコンパイラは.svelteファイルを解析し、 「どのstateがどのDOMノードに使われているか」をビルド時に静的解析します。 出力されるのはその特定のコンポーネント専用のDOM更新コードです。 つまりフレームワーク本体ではなく「あなたのコンポーネントの更新ロジックそのもの」がバンドルに入ります。

Svelte 5ではrunesのリアクティビティを支えるための小さなランタイムが入りましたが、 それでもmin+gzで2〜5KB程度に収まっています(CSR-onlyならさらに小さい)。

観点 React 19 Svelte 5
リアクティビティ方式 コンポーネント関数の再実行 + 仮想DOM diff シグナル(runes)による直接DOM更新
コンパイラの役割 JSX → createElement変換 + 自動メモ化(任意) コンポーネント → 専用DOM操作コード生成(必須)
ランタイムサイズ(min+gz) 約45KB 約2〜5KB
依存追跡 依存配列を開発者が明示(Compilerで自動化可) rune評価時に自動追跡
副作用の書き方 useEffect + 依存配列 $effect(依存自動推論)
ファイル形式 .tsx / .jsx(JS構文の拡張) .svelte(HTML/CSS/JSを統合した独自形式)

コンポーネントの書き方 — どこに何を書くか

両者は「ファイル1つに何を含めるか」の哲学も対照的です。

ReactのJSX

Reactは「JavaScriptの中にHTMLを書く」発想です。 コンポーネントは関数で、戻り値としてJSXを返します。スタイルはCSS-in-JS、Tailwind、CSS Modulesなど選択肢が無数にあるのが特徴で、 これはエコシステムの豊かさでもあり、技術選定の負荷でもあります。

import { useState } from 'react';
import styles from './Counter.module.css';

export function Counter({ initial = 0 }: { initial?: number }) {
  const [count, setCount] = useState(initial);

  return (
    <div className={styles.wrapper}>
      <p>カウント: {count}</p>
      <button
        className={styles.button}
        onClick={() => setCount(c => c + 1)}
      >
        +1
      </button>
    </div>
  );
}

Svelteの.svelteファイル

Svelteは「HTMLファイルの中にscriptとstyleを書く」発想です。 1ファイルに<script>、マークアップ、<style>を並べる構造で、 スタイルはそのコンポーネントにスコープが閉じるのがデフォルト挙動です。

<script lang="ts">
  let { initial = 0 } = $props();
  let count = $state(initial);
</script>

<div class="wrapper">
  <p>カウント: {count}</p>
  <button onclick={() => count++}>+1</button>
</div>

<style>
  /* この .wrapper はこのコンポーネント内だけに適用される */
  .wrapper { display: flex; gap: 0.5rem; }
  button { padding: 0.5rem 1rem; }
</style>

状態管理の考え方

状態をコンポーネント外に出すときの作法も両者で異なります。

Reactの場合

小規模ならuseStateContext API、 中〜大規模になるとZustand / Jotai / Redux Toolkit / TanStack Storeなどの外部ライブラリを選定するのが一般的です。 React本体は「グローバル状態の入れ物」を持たないため、エコシステムから選ぶ前提になっています。

Svelte 5の場合

Svelte 5では$state普通のJSモジュールにそのまま書いてexportすれば、 それがそのままグローバルな共有ストアとして機能します。

// store.svelte.ts (ファイル名に .svelte をつけると runes が使える)
export const counter = $state({ value: 0 });

export function increment() {
  counter.value++;
}

// 別コンポーネントから:
// <script>
//   import { counter, increment } from './store.svelte';
// </script>
// <p>{counter.value}</p>
// <button onclick={increment}>+1</button>

Reactで同じことをするには通常Zustandのようなライブラリが必要ですが、 Svelteは言語機能の延長で状態共有が完結します。これはrunesがコンパイル時に依存追跡できることの恩恵です。

エコシステムと採用市場

技術選定で見落とせないのが「周辺ライブラリの厚み」と「採用市場」です。

観点 React Svelte
npm週間ダウンロード(概算) 3000万+ 100万+
公式SSRフレームワーク Next.js(Vercel製、最も成熟) SvelteKit(公式・SSR/SSG/エッジ対応)
UIライブラリ shadcn/ui, MUI, Mantine, Chakra, Ant Design 等 shadcn-svelte, Skeleton, Flowbite, Melt UI 等
ネイティブ展開 React Native(成熟) NativeScript-Svelte(限定的)
採用市場 圧倒的に豊富(求人・採用しやすい) 伸長中だが規模はまだReactの数分の一
学習リソース 日本語含めて膨大 英語中心、日本語は限定的だが質は高い
LLM/AIコード生成の精度 訓練データ豊富で高精度 訓練データ少なめで.svelte生成は荒れがち

選定基準のチェックリスト

最後に、実プロジェクトでの選定基準を整理しておきます。 「速いから」「バンドルサイズが小さいから」だけで決めるのは危険です。

Reactを選ぶべき場面

  • チームの大半がReact経験者。学習コストを払う理由が見つからないなら無理に変える必要はない
  • 大規模なエンタープライズ案件。採用市場・サポート体制・第三者ライブラリの厚みが効く
  • React Nativeでネイティブアプリも展開する計画がある
  • Next.jsの機能(PPR、Server Components、Cache Components、Server Actions等)を最大限活用したい
  • AIコーディング支援を前提に開発速度を最大化したい

Svelteを選ぶべき場面

  • パフォーマンス・バンドルサイズが直接KPIに効くプロダクト(ランディングページ、Eコマース、組込み向けWebUI、低スペック端末向け等)
  • 小〜中規模のチームで、フレームワーク自体の学習コストを払ってでもコード量を減らしたい
  • SSRフレームワークとしてSvelteKitの単純さに価値を見出せる(Next.jsより設定がシンプル)
  • CSSスコープ・アニメーション・トランジションを言語機能として欲しい
  • 「書いて楽しい」DXを重視する個人開発・スタートアップMVP

まとめ

Reactは「関数として書ける宣言的UI、ランタイムで賢く差分を吸収する」設計を、 Svelteは「コンパイラが解析した結果として最小限のDOM操作を生成する」設計を選びました。 どちらも宣言的UIという同じ問題に対する別解であり、優劣ではなくトレードオフの取り方が違います。

Reactの強さはエコシステムと汎用性、Svelteの強さはパフォーマンスと開発体験。 実プロジェクトでは「どちらが速いか」より「チームと用途に合うか」で決めるのが、 2026年の現場感覚としては妥当なところでしょう。両方触ってみると、お互いの設計判断が 立体的に見えてきます。それぞれのフレームワークを正しく理解した上で選び取れることが、 フロントエンドエンジニアの腕の見せどころです。

理解度チェック

問題 0 / 50%
Q1

Svelte 5で導入された新しいリアクティビティシステムは「___」と呼ばれ、$state・$derived・$effectなどの記号で始まる関数として提供される。