SolidJS実践完全ガイド2025 - Reactの代替フレームワーク決定版
パフォーマンス革命をもたらすSolidJSの実践的活用法を徹底解説。React比3倍高速を実現する仕組みから実際のプロジェクト構築、移行戦略まで、具体的なコード例とベンチマークデータで詳しく紹介します。
Solid.jsによる高性能フロントエンド開発。Virtual DOMなしの細粒度リアクティビティ、コンポーネント設計パターン、SSRからパフォーマンス最適化まで、実用的な実装例とともに詳しく解説します。
フロントエンド開発の新たな選択肢として注目を集める Solid.js。Virtual DOM を使わない革新的なアプローチで、「バニラ JavaScriptと区別がつかない」ほどの高性能を実現しながら、Reactライクな開発者体験を提供します。本記事では、Solid.js の核心的特徴から実践的な活用方法まで包括的に解説します。
Solid.js は、コンパイル時最適化と細粒度リアクティビティを特徴とする JavaScript UI ライブラリです。Ryan Carniato 氏によって開発され、「パフォーマンス重視」「実用的」「強力」の 3 つの原則に基づいて設計されています。
特徴 | React | Solid.js | Solid.js の優位性 |
---|---|---|---|
レンダリング | Virtual DOM | Real DOM + 細粒度更新 | O(1) vs O(n) の差 |
状態管理 | useStateフック | Signal & Store | 自動依存関係追跡 |
再レンダリング | コンポーネント全体 | 変更部分のみ | 10-100倍の効率化 |
バンドルサイズ | 42KB (React + ReactDOM) | 8KB | 80%削減 |
実行速度 | ランタイムオーバーヘッド | ほぼネイティブ | 95%のオーバーヘッド削減 |
「Solid.js の目標は、フレームワークの存在を感じさせない、透明性の高い開発体験を提供することです」
チャートを読み込み中...
Solid.js の根幹を成す Signal は、状態変更を自動で追跡し、依存する部分のみを効率的に更新します。
// React: 全コンポーネントが再レンダリング
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// count変更時、コンポーネント全体が再レンダリング
return (
<div>
<h1>カウンター: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>+</button>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="名前"
/>
<p>こんにちは、{name}さん</p>
</div>
);
}
// Solid.js: 変更部分のみ更新
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('');
// count変更時、<h1>要素のみ更新
// name変更時、<input>と<p>要素のみ更新
return (
<div>
<h1>カウンター: {count()}</h1>
<button onClick={() => setCount(c => c + 1)}>+</button>
<input
value={name()}
onInput={e => setName(e.target.value)}
placeholder="名前"
/>
<p>こんにちは、{name()}さん</p>
</div>
);
}
// React: 全コンポーネントが再レンダリング
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// count変更時、コンポーネント全体が再レンダリング
return (
<div>
<h1>カウンター: {count}</h1>
<button onClick={() => setCount(c => c + 1)}>+</button>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="名前"
/>
<p>こんにちは、{name}さん</p>
</div>
);
}
// Solid.js: 変更部分のみ更新
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('');
// count変更時、<h1>要素のみ更新
// name変更時、<input>と<p>要素のみ更新
return (
<div>
<h1>カウンター: {count()}</h1>
<button onClick={() => setCount(c => c + 1)}>+</button>
<input
value={name()}
onInput={e => setName(e.target.value)}
placeholder="名前"
/>
<p>こんにちは、{name()}さん</p>
</div>
);
}
import { createSignal, createMemo } from 'solid-js';
function ShoppingCart() {
const [items, setItems] = createSignal([
{ id: 1, name: 'ノート', price: 200, quantity: 2 },
{ id: 2, name: 'ペン', price: 100, quantity: 5 }
]);
// 計算プロパティ:依存するsignalが変更時のみ再計算
const totalPrice = createMemo(() => {
return items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
const itemCount = createMemo(() => {
return items().reduce((sum, item) => sum + item.quantity, 0);
});
return (
<div>
<h2>ショッピングカート</h2>
<p>アイテム数: {itemCount()}</p>
<p>合計金額: ¥{totalPrice().toLocaleString()}</p>
<ul>
{items().map(item => (
<li key={item.id}>
{item.name} - ¥{item.price} × {item.quantity}
</li>
))}
</ul>
</div>
);
}
import { createSignal, createEffect } from 'solid-js';
function UserProfile() {
const [userId, setUserId] = createSignal(1);
const [user, setUser] = createSignal(null);
const [loading, setLoading] = createSignal(false);
// エフェクト:userIdが変更されるたびに実行
createEffect(async () => {
const id = userId(); // 依存関係として自動追跡
setLoading(true);
try {
const response = await fetch(`/api/users/${id}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('ユーザー取得エラー:', error);
} finally {
setLoading(false);
}
});
return (
<div>
<select onChange={e => setUserId(Number(e.target.value))}>
<option value={1}>ユーザー1</option>
<option value={2}>ユーザー2</option>
<option value={3}>ユーザー3</option>
</select>
{loading() ? (
<p>読み込み中...</p>
) : user() ? (
<div>
<h3>{user().name}</h3>
<p>{user().email}</p>
</div>
) : (
<p>ユーザーが見つかりません</p>
)}
</div>
);
}
import { createSignal, createMemo } from 'solid-js';
function ExpensiveCalculation() {
const [input, setInput] = createSignal('');
const [multiplier, setMultiplier] = createSignal(1);
// 重い計算をメモ化
const expensiveResult = createMemo(() => {
console.log('重い計算を実行中...');
const value = input();
if (!value) return 0;
// 重い計算をシミュレート
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += value.length * multiplier();
}
return result;
});
return (
<div>
<input
value={input()}
onInput={e => setInput(e.target.value)}
placeholder="文字を入力"
/>
<input
type="number"
value={multiplier()}
onInput={e => setMultiplier(Number(e.target.value))}
placeholder="倍数"
/>
<p>計算結果: {expensiveResult()}</p>
</div>
);
}
import { createSignal, createEffect, onCleanup } from 'solid-js';
function Timer() {
const [seconds, setSeconds] = createSignal(0);
const [isRunning, setIsRunning] = createSignal(false);
createEffect(() => {
if (isRunning()) {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// クリーンアップ関数
onCleanup(() => {
clearInterval(interval);
console.log('タイマークリーンアップ');
});
}
});
const start = () => setIsRunning(true);
const stop = () => setIsRunning(false);
const reset = () => {
setIsRunning(false);
setSeconds(0);
};
return (
<div>
<h2>タイマー: {seconds()}秒</h2>
<button onClick={start} disabled={isRunning()}>
開始
</button>
<button onClick={stop} disabled={!isRunning()}>
停止
</button>
<button onClick={reset}>リセット</button>
</div>
);
}
複雑なアプリケーション状態には、Store を使用します。
import { createStore } from 'solid-js/store';
function TodoApp() {
const [todos, setTodos] = createStore([
{ id: 1, text: 'Solid.jsを学ぶ', completed: false },
{ id: 2, text: 'ブログを書く', completed: true }
]);
const [filter, setFilter] = createSignal('all');
// フィルタリングされたTodoリスト
const filteredTodos = createMemo(() => {
const filterValue = filter();
switch (filterValue) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
});
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false
};
setTodos(todos.length, newTodo);
};
const toggleTodo = (id) => {
setTodos(
todo => todo.id === id,
'completed',
completed => !completed
);
};
const removeTodo = (id) => {
setTodos(todos => todos.filter(todo => todo.id !== id));
};
return (
<div>
<h1>Todo アプリ</h1>
<TodoInput onAdd={addTodo} />
<div>
<button
onClick={() => setFilter('all')}
style={{ 'font-weight': filter() === 'all' ? 'bold' : 'normal' }}
>
すべて
</button>
<button
onClick={() => setFilter('active')}
style={{ 'font-weight': filter() === 'active' ? 'bold' : 'normal' }}
>
未完了
</button>
<button
onClick={() => setFilter('completed')}
style={{ 'font-weight': filter() === 'completed' ? 'bold' : 'normal' }}
>
完了済み
</button>
</div>
<ul>
<For each={filteredTodos()}>
{(todo) => (
<li style={{
'text-decoration': todo.completed ? 'line-through' : 'none'
}}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>削除</button>
</li>
)}
</For>
</ul>
<p>
未完了: {todos.filter(t => !t.completed).length} /
合計: {todos.length}
</p>
</div>
);
}
function TodoInput({ onAdd }) {
const [text, setText] = createSignal('');
const handleSubmit = (e) => {
e.preventDefault();
if (text().trim()) {
onAdd(text().trim());
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text()}
onInput={e => setText(e.target.value)}
placeholder="新しいタスクを入力"
/>
<button type="submit">追加</button>
</form>
);
}
Solid.js では、JavaScript の制御構文の代わりに専用のコンポーネントを使用します。
import { Show, For, Switch, Match, Suspense, ErrorBoundary } from 'solid-js';
function AdvancedComponents() {
const [user, setUser] = createSignal(null);
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal(null);
const [items, setItems] = createSignal([]);
const [viewMode, setViewMode] = createSignal('list');
return (
<div>
{/* 条件分岐 */}
<Show
when={user()}
fallback={<button onClick={login}>ログイン</button>}
>
<div>
<h2>こんにちは、{user().name}さん</h2>
<button onClick={logout}>ログアウト</button>
</div>
</Show>
{/* リストレンダリング */}
<For each={items()}>
{(item, index) => (
<div>
<span>{index() + 1}. {item.name}</span>
<button onClick={() => removeItem(item.id)}>削除</button>
</div>
)}
</For>
{/* Switch文 */}
<Switch>
<Match when={viewMode() === 'list'}>
<ListView items={items()} />
</Match>
<Match when={viewMode() === 'grid'}>
<GridView items={items()} />
</Match>
<Match when={viewMode() === 'table'}>
<TableView items={items()} />
</Match>
</Switch>
{/* 非同期処理とエラーハンドリング */}
<ErrorBoundary fallback={err => <div>エラーが発生しました: {err.message}</div>}>
<Suspense fallback={<div>読み込み中...</div>}>
<AsyncUserProfile userId={user()?.id} />
</Suspense>
</ErrorBoundary>
</div>
);
}
// useApi.js - 再利用可能なAPIフック
import { createSignal, createResource } from 'solid-js';
export function useApi(url) {
const [data, { mutate, refetch }] = createResource(() => url(), fetchData);
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal(null);
async function fetchData(url) {
if (!url) return null;
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}
return {
data,
loading,
error,
refetch,
mutate
};
}
// 使用例
function UserProfile() {
const [userId, setUserId] = createSignal(1);
const userUrl = createMemo(() =>
userId() ? `/api/users/${userId()}` : null
);
const { data: user, loading, error, refetch } = useApi(userUrl);
return (
<div>
<select onChange={e => setUserId(Number(e.target.value))}>
<option value={1}>ユーザー1</option>
<option value={2}>ユーザー2</option>
</select>
<Show when={loading()}>
<p>読み込み中...</p>
</Show>
<Show when={error()}>
<div style={{ color: 'red' }}>
エラー: {error()}
<button onClick={refetch}>再試行</button>
</div>
</Show>
<Show when={user()}>
<div>
<h3>{user().name}</h3>
<p>{user().email}</p>
</div>
</Show>
</div>
);
}
SolidStartテンプレートから新規プロジェクト生成
ファイルベースルーティングの構成
サーバーサイドAPIエンドポイントの実装
サーバーサイドレンダリング設定
# SolidStartプロジェクト作成
npm create solid@latest my-app
cd my-app
# 依存関係インストール
npm install
# 開発サーバー起動
npm run dev
src/
├── routes/
│ ├── index.tsx # / (ホームページ)
│ ├── about.tsx # /about
│ ├── blog/
│ │ ├── index.tsx # /blog
│ │ └── [slug].tsx # /blog/:slug
│ ├── api/
│ │ ├── users.ts # /api/users
│ │ └── users/[id].ts # /api/users/:id
│ └── [...404].tsx # 404ページ
├── app.tsx
└── entry-client.tsx
// src/routes/api/users.ts
import { APIEvent } from "@solidjs/start/server";
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' }
];
export async function GET(event: APIEvent) {
// クエリパラメータの処理
const url = new URL(event.request.url);
const limit = parseInt(url.searchParams.get('limit') || '10');
const paginatedUsers = users.slice(0, limit);
return new Response(JSON.stringify(paginatedUsers), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300' // 5分キャッシュ
}
});
}
export async function POST(event: APIEvent) {
try {
const newUser = await event.request.json() as Omit<User, 'id'>;
// バリデーション
if (!newUser.name || !newUser.email) {
return new Response(
JSON.stringify({ error: '名前とメールアドレスは必須です' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const user: User = {
id: Date.now(),
...newUser
};
users.push(user);
return new Response(JSON.stringify(user), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid JSON' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
}
// src/routes/blog/[slug].tsx
import { useParams, RouteDataArgs, useRouteData } from "@solidjs/start";
import { createResource, Show, Suspense } from "solid-js";
interface BlogPost {
id: string;
title: string;
content: string;
publishedAt: string;
author: string;
}
// サーバーサイドでのデータ取得
export function routeData({ params }: RouteDataArgs) {
return createResource(() => params.slug, fetchBlogPost);
}
async function fetchBlogPost(slug: string): Promise<BlogPost | null> {
try {
// 実際のAPIコール
const response = await fetch(`https://api.example.com/blog/${slug}`);
if (!response.ok) return null;
return await response.json();
} catch (error) {
console.error('ブログ記事の取得エラー:', error);
return null;
}
}
export default function BlogPost() {
const [post] = useRouteData<typeof routeData>();
return (
<Suspense fallback={<BlogPostSkeleton />}>
<Show
when={post()}
fallback={<NotFound />}
>
{(blogPost) => (
<article>
<header>
<h1>{blogPost().title}</h1>
<div class="meta">
<span>著者: {blogPost().author}</span>
<time>{new Date(blogPost().publishedAt).toLocaleDateString('ja-JP')}</time>
</div>
</header>
<div class="content">
{blogPost().content}
</div>
<footer>
<ShareButtons title={blogPost().title} />
<RelatedPosts currentId={blogPost().id} />
</footer>
</article>
)}
</Show>
</Suspense>
);
}
function BlogPostSkeleton() {
return (
<div class="skeleton">
<div class="skeleton-title"></div>
<div class="skeleton-meta"></div>
<div class="skeleton-content"></div>
</div>
);
}
function NotFound() {
return (
<div class="not-found">
<h1>記事が見つかりません</h1>
<p>指定された記事は存在しないか、削除された可能性があります。</p>
<a href="/blog">ブログ一覧に戻る</a>
</div>
);
}
最適化手法 | 効果 | 実装難易度 | 推奨度 |
---|---|---|---|
Tree Shaking | 30-50%削減 | 低 | ★★★★★ |
Code Splitting | 初期ロード40%改善 | 中 | ★★★★☆ |
Dynamic Imports | ページ遷移高速化 | 低 | ★★★★☆ |
Bundle Analysis | 問題箇所特定 | 低 | ★★★★★ |
// vite.config.js
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
export default defineConfig({
plugins: [solid()],
build: {
// チャンク分割戦略
rollupOptions: {
output: {
manualChunks: {
// ベンダーライブラリを分離
vendor: ['solid-js'],
// 大きなライブラリを分離
utils: ['lodash', 'date-fns'],
// UIコンポーネントを分離
ui: ['@solid-primitives/ui']
}
}
},
// 最小化設定
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // console.log削除
drop_debugger: true
}
}
},
// 開発時の最適化
optimizeDeps: {
include: ['solid-js/web']
}
});
import { lazy, Suspense } from 'solid-js';
// 遅延ローディングコンポーネント
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
function App() {
const [currentView, setCurrentView] = createSignal('home');
return (
<div>
<nav>
<button onClick={() => setCurrentView('home')}>ホーム</button>
<button onClick={() => setCurrentView('chart')}>チャート</button>
<button onClick={() => setCurrentView('admin')}>管理画面</button>
</nav>
<main>
<Switch>
<Match when={currentView() === 'home'}>
<HomePage />
</Match>
<Match when={currentView() === 'chart'}>
<Suspense fallback={<div>チャートを読み込み中...</div>}>
<HeavyChart />
</Suspense>
</Match>
<Match when={currentView() === 'admin'}>
<Suspense fallback={<div>管理画面を読み込み中...</div>}>
<AdminDashboard />
</Suspense>
</Match>
</Switch>
</main>
</div>
);
}
// プリロード機能
function preloadAdminDashboard() {
// ユーザーが管理者権限を持つ場合のみプリロード
if (hasAdminPermission()) {
import('./pages/AdminDashboard');
}
}
プロジェクト概要: サーバー監視ダッシュボード 技術構成: Solid.js + WebSocket + D3.js
import { createSignal, createEffect, onCleanup } from 'solid-js';
import { createStore } from 'solid-js/store';
function ServerMonitorDashboard() {
const [connected, setConnected] = createSignal(false);
const [servers, setServers] = createStore([]);
const [alerts, setAlerts] = createStore([]);
let ws;
createEffect(() => {
// WebSocket接続
ws = new WebSocket('wss://api.example.com/monitor');
ws.onopen = () => {
setConnected(true);
console.log('監視サーバーに接続しました');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'server_update':
setServers(
server => server.id === data.serverId,
{ ...data.metrics, lastUpdate: Date.now() }
);
break;
case 'alert':
setAlerts(alerts => [
{ id: Date.now(), ...data.alert },
...alerts.slice(0, 9) // 最新10件のみ保持
]);
break;
}
};
ws.onclose = () => {
setConnected(false);
// 再接続ロジック
setTimeout(() => {
if (!connected()) {
createEffect(); // 再接続
}
}, 5000);
};
onCleanup(() => {
ws?.close();
});
});
return (
<div class="dashboard">
<header>
<h1>サーバー監視ダッシュボード</h1>
<div class={`status ${connected() ? 'connected' : 'disconnected'}`}>
{connected() ? '接続済み' : '切断中'}
</div>
</header>
<div class="grid">
<ServerGrid servers={servers} />
<AlertPanel alerts={alerts} />
<MetricsChart servers={servers} />
</div>
</div>
);
}
function ServerGrid({ servers }) {
return (
<div class="server-grid">
<For each={servers}>
{(server) => (
<ServerCard server={server} />
)}
</For>
</div>
);
}
function ServerCard({ server }) {
const statusColor = () => {
if (server.cpu > 90 || server.memory > 95) return 'critical';
if (server.cpu > 70 || server.memory > 80) return 'warning';
return 'healthy';
};
return (
<div class={`server-card ${statusColor()}`}>
<h3>{server.name}</h3>
<div class="metrics">
<div class="metric">
<span>CPU</span>
<div class="progress-bar">
<div
class="progress-fill"
style={{ width: `${server.cpu}%` }}
></div>
</div>
<span>{server.cpu}%</span>
</div>
<div class="metric">
<span>メモリ</span>
<div class="progress-bar">
<div
class="progress-fill"
style={{ width: `${server.memory}%` }}
></div>
</div>
<span>{server.memory}%</span>
</div>
</div>
<div class="last-update">
更新: {new Date(server.lastUpdate).toLocaleTimeString('ja-JP')}
</div>
</div>
);
}
Solid.js は、パフォーマンスと開発者体験の両立を実現する次世代フロントエンドフレームワークとして、多くの可能性を秘めています。本記事の要点をまとめると:
技術的革新
開発者体験
実用性
適用領域
Solid.js は、React の学習コストを活かしながら、より高性能なアプリケーションを構築したい開発者にとって理想的な選択肢です。特に、パフォーマンスが重要な要件となるプロジェクトでは、その真価を発揮するでしょう。