Solid.js 2025 - 真のリアクティビティを実現するUIフレームワーク

2026.01.12

Solid.jsとは

Solid.jsは、Ryan Carniatoによって開発された高性能なUIフレームワークです。Reactに似た開発体験を提供しながら、仮想DOMを使用せず、真のfine-grained reactivity(細粒度リアクティビティ)によって驚異的なパフォーマンスを実現しています。2025年現在、Solid.jsは成熟したエコシステムと共に、パフォーマンスが重要なプロジェクトでの採用が急速に増加しています。

flowchart TB
    subgraph Solidjs["Solid.js コア機能"]
        subgraph Reactivity["リアクティビティシステム"]
            S1["Signals - リアクティブな状態"]
            S2["Computed/Memo - 派生値"]
            S3["Effects - 副作用処理"]
            S4["Resources - 非同期データ"]
        end

        subgraph Rendering["レンダリング"]
            R1["仮想DOMなし"]
            R2["コンパイル時最適化"]
            R3["Fine-grained更新"]
            R4["最小限のDOM操作"]
        end

        subgraph DX["開発者体験"]
            D1["JSX構文"]
            D2["TypeScript完全対応"]
            D3["React風のAPI"]
            D4["直感的な状態管理"]
        end
    end

Signalsによるリアクティビティ

Solid.jsの核心はSignals(シグナル)です。Signalsは、値の変更を自動的に追跡し、依存関係にあるコンポーネントやエフェクトのみを更新する仕組みを提供します。

基本的なSignalの使用

import { createSignal, createEffect } from "solid-js";

function Counter() {
  // Signal: [ゲッター関数, セッター関数]を返す
  const [count, setCount] = createSignal(0);

  // 複数の更新方法
  const increment = () => setCount(count() + 1);
  const decrement = () => setCount(c => c - 1);  // 関数形式
  const reset = () => setCount(0);

  // Effectは依存関係を自動追跡
  createEffect(() => {
    console.log(`現在のカウント: ${count()}`);
    // count()が変更されるたびに実行される
  });

  return (
    <div class="counter">
      <button onClick={decrement}>-</button>
      <span>{count()}</span>
      <button onClick={increment}>+</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
}

オブジェクトと配列のリアクティビティ

import { createSignal, createStore, produce } from "solid-js/store";

function TodoApp() {
  // シンプルな状態にはcreateSignal
  const [filter, setFilter] = createSignal<"all" | "active" | "completed">("all");

  // 複雑なオブジェクトや配列にはcreateStore
  const [todos, setTodos] = createStore([
    { id: 1, text: "Solid.jsを学ぶ", completed: false },
    { id: 2, text: "アプリを作成", completed: false },
  ]);

  const addTodo = (text: string) => {
    setTodos(todos.length, {
      id: Date.now(),
      text,
      completed: false,
    });
  };

  const toggleTodo = (id: number) => {
    setTodos(
      todo => todo.id === id,
      "completed",
      completed => !completed
    );
  };

  // Immer風のproduce関数も使用可能
  const toggleWithProduce = (id: number) => {
    setTodos(produce(draft => {
      const todo = draft.find(t => t.id === id);
      if (todo) todo.completed = !todo.completed;
    }));
  };

  // 派生値(自動的にメモ化される)
  const filteredTodos = () => {
    switch (filter()) {
      case "active":
        return todos.filter(t => !t.completed);
      case "completed":
        return todos.filter(t => t.completed);
      default:
        return todos;
    }
  };

  return (
    <div class="todo-app">
      <input
        placeholder="新しいTodo"
        onKeyPress={(e) => {
          if (e.key === "Enter") {
            addTodo(e.currentTarget.value);
            e.currentTarget.value = "";
          }
        }}
      />
      <div class="filters">
        <button onClick={() => setFilter("all")}>全て</button>
        <button onClick={() => setFilter("active")}>未完了</button>
        <button onClick={() => setFilter("completed")}>完了</button>
      </div>
      <ul>
        <For each={filteredTodos()}>
          {(todo) => (
            <li
              class={todo.completed ? "completed" : ""}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </li>
          )}
        </For>
      </ul>
    </div>
  );
}

Fine-Grained Reactivity の仕組み

Solid.jsのリアクティビティシステムは、他のフレームワークとは根本的に異なります。

flowchart LR
    subgraph React["React (仮想DOM)"]
        R1["状態更新"] --> R2["コンポーネント再実行"]
        R2 --> R3["仮想DOM差分計算"]
        R3 --> R4["実DOM更新"]
    end

    subgraph Solid["Solid.js (Fine-Grained)"]
        S1["Signal更新"] --> S2["依存関係追跡"]
        S2 --> S3["該当箇所のみ直接更新"]
    end

コンパイル後の比較

// Solid.jsのソースコード
function Counter() {
  const [count, setCount] = createSignal(0);
  return <div>Count: {count()}</div>;
}

// コンパイル後(概念的な表現)
function Counter() {
  const [count, setCount] = createSignal(0);
  const div = document.createElement("div");
  const text = document.createTextNode("");

  // Signalの変更を直接DOMに反映
  createEffect(() => {
    text.data = `Count: ${count()}`;
  });

  div.appendChild(text);
  return div;
}

パフォーマンス比較コード

import { createSignal, For, batch } from "solid-js";

function PerformanceDemo() {
  const [items, setItems] = createSignal<number[]>([]);
  const [renderTime, setRenderTime] = createSignal(0);

  const addThousandItems = () => {
    const start = performance.now();

    // batch: 複数の更新を1回にまとめる
    batch(() => {
      const newItems = Array.from(
        { length: 1000 },
        (_, i) => items().length + i
      );
      setItems([...items(), ...newItems]);
    });

    // DOMが更新された後に時間を計測
    requestAnimationFrame(() => {
      setRenderTime(performance.now() - start);
    });
  };

  const updateEveryTenth = () => {
    const start = performance.now();

    // 10個ごとの要素のみ更新(Fine-grainedの真価)
    setItems(items().map((item, i) =>
      i % 10 === 0 ? item * 2 : item
    ));

    requestAnimationFrame(() => {
      setRenderTime(performance.now() - start);
    });
  };

  return (
    <div>
      <button onClick={addThousandItems}>1000件追加</button>
      <button onClick={updateEveryTenth}>10件ごとに更新</button>
      <p>レンダリング時間: {renderTime().toFixed(2)}ms</p>
      <p>アイテム数: {items().length}</p>
      <div class="items">
        <For each={items()}>
          {(item) => <span class="item">{item}</span>}
        </For>
      </div>
    </div>
  );
}

他フレームワークとの比較

React vs Solid.js

特徴ReactSolid.js
仮想DOMありなし
リアクティビティCoarse-grainedFine-grained
コンポーネント再実行状態変更時に全体初回のみ
メモ化useMemo, useCallback自動(不要)
バンドルサイズ~40KB~7KB
学習曲線緩やかReact経験者は容易

同じコードの比較

// React
function ReactCounter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("React");

  // countが変わるとコンポーネント全体が再実行
  // nameの表示部分も再計算される
  console.log("Render"); // 毎回実行

  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

// Solid.js
function SolidCounter() {
  const [count, setCount] = createSignal(0);
  const [name, setName] = createSignal("Solid");

  // コンポーネント関数は初回のみ実行
  console.log("Setup"); // 1回だけ実行

  return (
    <div>
      <h1>Hello, {name()}</h1>
      <p>Count: {count()}</p>  {/* ここだけ更新 */}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

Vue vs Solid.js

// Vue 3 Composition API
const VueCounter = {
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);

    watch(count, (newVal) => {
      console.log(`Count: ${newVal}`);
    });

    return { count, doubled };
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <p>Doubled: {{ doubled }}</p>
      <button @click="count++">+1</button>
    </div>
  `
};

// Solid.js
function SolidCounter() {
  const [count, setCount] = createSignal(0);
  const doubled = createMemo(() => count() * 2);

  createEffect(() => {
    console.log(`Count: ${count()}`);
  });

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

Svelte vs Solid.js

特徴Svelte 5Solid.js
構文独自構文 + RunesJSX
ランタイム最小小さい (~7KB)
リアクティビティFine-grainedFine-grained
TypeScript良好優秀
エコシステム成長中成長中

SolidStart - フルスタックフレームワーク

SolidStartは、Solid.jsの公式メタフレームワークです。Next.jsやNuxtに相当し、SSR、ルーティング、API ルートなどを提供します。

プロジェクトのセットアップ

# 新規プロジェクト作成
npm create solid@latest my-app

# オプション選択
# - SolidStart (SSR framework)
# - TypeScript
# - TailwindCSS

cd my-app
npm install
npm run dev

ファイルベースルーティング

src/
├── routes/
│   ├── index.tsx          # /
│   ├── about.tsx          # /about
│   ├── blog/
│   │   ├── index.tsx      # /blog
│   │   └── [slug].tsx     # /blog/:slug
│   └── api/
│       └── posts.ts       # /api/posts
└── components/
    └── Header.tsx

ページコンポーネント

// src/routes/index.tsx
import { createSignal } from "solid-js";
import { Title, Meta } from "@solidjs/meta";

export default function Home() {
  const [message, setMessage] = createSignal("Hello, SolidStart!");

  return (
    <main>
      <Title>ホーム | My Solid App</Title>
      <Meta name="description" content="SolidStartで構築されたサイト" />

      <h1>{message()}</h1>
      <p>SolidStartへようこそ!</p>
    </main>
  );
}

データフェッチング

// src/routes/blog/[slug].tsx
import { createAsync, cache } from "@solidjs/router";
import { Show } from "solid-js";

// キャッシュされたデータフェッチ関数
const getPost = cache(async (slug: string) => {
  "use server";
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (!res.ok) throw new Error("Post not found");
  return res.json();
}, "post");

export const route = {
  load: ({ params }) => getPost(params.slug),
};

export default function BlogPost() {
  const params = useParams();
  const post = createAsync(() => getPost(params.slug));

  return (
    <Show when={post()} fallback={<div>Loading...</div>}>
      {(p) => (
        <article>
          <h1>{p().title}</h1>
          <p>{p().content}</p>
          <time>{new Date(p().createdAt).toLocaleDateString()}</time>
        </article>
      )}
    </Show>
  );
}

Server Actions

// src/routes/contact.tsx
import { action, useAction, useSubmission } from "@solidjs/router";
import { createSignal } from "solid-js";

// サーバーアクション定義
const sendMessage = action(async (formData: FormData) => {
  "use server";
  const name = formData.get("name");
  const email = formData.get("email");
  const message = formData.get("message");

  // バリデーション
  if (!name || !email || !message) {
    throw new Error("全ての項目を入力してください");
  }

  // メール送信やDB保存など
  await saveToDatabase({ name, email, message });

  return { success: true };
});

export default function Contact() {
  const submit = useAction(sendMessage);
  const submission = useSubmission(sendMessage);

  return (
    <div class="contact">
      <h1>お問い合わせ</h1>

      <form action={sendMessage} method="post">
        <div>
          <label for="name">お名前</label>
          <input type="text" name="name" id="name" required />
        </div>

        <div>
          <label for="email">メールアドレス</label>
          <input type="email" name="email" id="email" required />
        </div>

        <div>
          <label for="message">メッセージ</label>
          <textarea name="message" id="message" required />
        </div>

        <button type="submit" disabled={submission.pending}>
          {submission.pending ? "送信中..." : "送信"}
        </button>

        <Show when={submission.result?.success}>
          <p class="success">送信完了しました!</p>
        </Show>

        <Show when={submission.error}>
          <p class="error">{submission.error.message}</p>
        </Show>
      </form>
    </div>
  );
}

高度なパターン

Context API

import { createContext, useContext, ParentProps } from "solid-js";
import { createStore } from "solid-js/store";

// 型定義
interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthContextValue {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Context作成
const AuthContext = createContext<AuthContextValue>();

// Provider コンポーネント
export function AuthProvider(props: ParentProps) {
  const [state, setState] = createStore<{ user: User | null }>({
    user: null,
  });

  const value: AuthContextValue = {
    get user() { return state.user; },
    get isAuthenticated() { return state.user !== null; },

    async login(email: string, password: string) {
      const res = await fetch("/api/login", {
        method: "POST",
        body: JSON.stringify({ email, password }),
      });
      const user = await res.json();
      setState("user", user);
    },

    logout() {
      setState("user", null);
    },
  };

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  );
}

// カスタムフック
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}

// 使用例
function UserProfile() {
  const auth = useAuth();

  return (
    <Show when={auth.isAuthenticated} fallback={<LoginForm />}>
      <div class="profile">
        <h2>ようこそ、{auth.user?.name}さん</h2>
        <button onClick={auth.logout}>ログアウト</button>
      </div>
    </Show>
  );
}

Resource - 非同期データ管理

import { createResource, Suspense, ErrorBoundary } from "solid-js";

interface Post {
  id: number;
  title: string;
  body: string;
}

// 非同期データソース
const fetchPosts = async (): Promise<Post[]> => {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!res.ok) throw new Error("Failed to fetch");
  return res.json();
};

function PostList() {
  // createResource: Suspenseと連携する非同期データ
  const [posts, { refetch, mutate }] = createResource(fetchPosts);

  return (
    <div>
      <button onClick={refetch}>再読み込み</button>

      <ErrorBoundary fallback={(err) => <p>エラー: {err.message}</p>}>
        <Suspense fallback={<p>読み込み中...</p>}>
          <ul>
            <For each={posts()}>
              {(post) => (
                <li>
                  <h3>{post.title}</h3>
                  <p>{post.body}</p>
                </li>
              )}
            </For>
          </ul>
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

// パラメータ付きResource
function PostDetail(props: { id: number }) {
  const fetchPost = async (id: number) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
    return res.json();
  };

  // props.idが変わると自動的に再フェッチ
  const [post] = createResource(() => props.id, fetchPost);

  return (
    <Suspense fallback={<p>読み込み中...</p>}>
      <Show when={post()}>
        {(p) => (
          <article>
            <h1>{p().title}</h1>
            <p>{p().body}</p>
          </article>
        )}
      </Show>
    </Suspense>
  );
}

パフォーマンスベンチマーク

js-framework-benchmarkでの比較結果(2025年最新):

フレームワークDOM操作メモリ起動時間総合スコア
Solid.js 1.81.021.041.011.02
Vue 3.41.211.151.081.15
Svelte 51.051.081.031.05
React 191.351.221.121.23
Angular 181.281.181.151.20

数値が小さいほど高速(1.0 = 最速実装との比較)

バンドルサイズ比較

フレームワークHello WorldTodoMVC
Solid.js6.4KB12.8KB
Svelte 52.8KB9.2KB
Vue 334KB42KB
React 1942KB51KB

2025年のエコシステム

主要ライブラリ

// UIコンポーネント: Solid UI / Kobalte
import { Button, Dialog, Select } from "@kobalte/core";

// ルーティング: @solidjs/router (組み込み)
import { Router, Route, A } from "@solidjs/router";

// 状態管理: solid-js/store (組み込み)
import { createStore, produce } from "solid-js/store";

// フォーム: @modular-forms/solid
import { createForm, Field } from "@modular-forms/solid";

// アニメーション: solid-motionone
import { Motion, Presence } from "solid-motionone";

// テスト: @solidjs/testing-library
import { render, fireEvent } from "@solidjs/testing-library";

// 国際化: @solid-primitives/i18n
import { I18nProvider, useI18n } from "@solid-primitives/i18n";

Solid Primitives

Solid Primitivesは、Solid.js用の高品質なユーティリティ集です。

import { createMediaQuery } from "@solid-primitives/media";
import { createGeolocation } from "@solid-primitives/geolocation";
import { createLocalStorage } from "@solid-primitives/storage";
import { createWebSocket } from "@solid-primitives/websocket";
import { createIntersectionObserver } from "@solid-primitives/intersection-observer";

function AdvancedExample() {
  // レスポンシブ
  const isMobile = createMediaQuery("(max-width: 768px)");

  // ローカルストレージ
  const [settings, setSettings] = createLocalStorage("app-settings", {
    theme: "light",
    language: "ja",
  });

  // 位置情報
  const [location] = createGeolocation();

  // Intersection Observer
  let ref: HTMLDivElement;
  const [isVisible] = createIntersectionObserver(() => ref);

  return (
    <div>
      <p>モバイル: {isMobile() ? "はい" : "いいえ"}</p>
      <p>テーマ: {settings.theme}</p>
      <Show when={location()}>
        <p>位置: {location()?.latitude}, {location()?.longitude}</p>
      </Show>
      <div ref={ref!} class={isVisible() ? "visible" : "hidden"}>
        スクロールで表示
      </div>
    </div>
  );
}

Solid.jsを始めるには

推奨環境

# Node.js 18以上推奨
node --version

# 新規プロジェクト作成(シンプルなSPA)
npm create vite@latest my-solid-app -- --template solid-ts

# SolidStart(フルスタック)
npm create solid@latest my-full-app

VS Code拡張機能

  • Solid - シンタックスハイライト、補完
  • Solid Snippets - コードスニペット
  • ESLint + eslint-plugin-solid - リンティング

学習リソース

  1. 公式ドキュメント: https://www.solidjs.com/docs
  2. SolidStart: https://start.solidjs.com
  3. Solid Playground: https://playground.solidjs.com
  4. Solid Primitives: https://primitives.solidjs.community

まとめ

Solid.jsは2025年において、以下の理由から注目に値するフレームワークです:

  1. 最高クラスのパフォーマンス: Fine-grained reactivityにより、仮想DOMのオーバーヘッドなしで最小限のDOM操作を実現
  2. React風の開発体験: JSXとHooks風のAPIで、React開発者が容易に移行可能
  3. 小さなバンドルサイズ: ランタイムが軽量で、初期ロードが高速
  4. TypeScriptファーストサポート: 型安全な開発が可能
  5. SolidStartの成熟: フルスタック開発に必要な機能が揃っている
  6. 成長するエコシステム: Solid Primitivesや各種ライブラリが充実

パフォーマンスが重要なプロジェクト、リアルタイムアプリケーション、モバイルデバイス向けWebアプリなど、Solid.jsは最適な選択肢の一つです。

参考リンク

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

LINEで無料相談する
← 一覧に戻る