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
| 特徴 | React | Solid.js |
|---|---|---|
| 仮想DOM | あり | なし |
| リアクティビティ | Coarse-grained | Fine-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 5 | Solid.js |
|---|---|---|
| 構文 | 独自構文 + Runes | JSX |
| ランタイム | 最小 | 小さい (~7KB) |
| リアクティビティ | Fine-grained | Fine-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.8 | 1.02 | 1.04 | 1.01 | 1.02 |
| Vue 3.4 | 1.21 | 1.15 | 1.08 | 1.15 |
| Svelte 5 | 1.05 | 1.08 | 1.03 | 1.05 |
| React 19 | 1.35 | 1.22 | 1.12 | 1.23 |
| Angular 18 | 1.28 | 1.18 | 1.15 | 1.20 |
数値が小さいほど高速(1.0 = 最速実装との比較)
バンドルサイズ比較
| フレームワーク | Hello World | TodoMVC |
|---|---|---|
| Solid.js | 6.4KB | 12.8KB |
| Svelte 5 | 2.8KB | 9.2KB |
| Vue 3 | 34KB | 42KB |
| React 19 | 42KB | 51KB |
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- リンティング
学習リソース
- 公式ドキュメント: https://www.solidjs.com/docs
- SolidStart: https://start.solidjs.com
- Solid Playground: https://playground.solidjs.com
- Solid Primitives: https://primitives.solidjs.community
まとめ
Solid.jsは2025年において、以下の理由から注目に値するフレームワークです:
- 最高クラスのパフォーマンス: Fine-grained reactivityにより、仮想DOMのオーバーヘッドなしで最小限のDOM操作を実現
- React風の開発体験: JSXとHooks風のAPIで、React開発者が容易に移行可能
- 小さなバンドルサイズ: ランタイムが軽量で、初期ロードが高速
- TypeScriptファーストサポート: 型安全な開発が可能
- SolidStartの成熟: フルスタック開発に必要な機能が揃っている
- 成長するエコシステム: Solid Primitivesや各種ライブラリが充実
パフォーマンスが重要なプロジェクト、リアルタイムアプリケーション、モバイルデバイス向けWebアプリなど、Solid.jsは最適な選択肢の一つです。