SolidJS vs React: コンポーネントモデルとパフォーマンスの比較

フロントエンドフレームワークを選択する際、コンポーネントのレンダリングとパフォーマンスの処理方法を理解することは非常に重要です。SolidJSを検討しているReact開発者にとって、コンポーネントモデルとリアクティブシステムの違いは、アプリケーションのパフォーマンスと開発体験に直接影響します。
この記事では、SolidJSとReactのコンポーネントモデルを比較し、レンダリングメカニズム、パフォーマンス特性、実際のコードの違いに焦点を当てます。
重要なポイント
- Reactは状態変更のたびにコンポーネントが再実行されるレンダーベースモデルを使用し、一方SolidJSのコンポーネントは一度だけ実行されてリアクティブバインディングを作成します
- ReactはVirtual DOMを使用した粗粒度のリアクティビティを採用し、SolidJSは直接DOM更新による細粒度のリアクティビティを使用します
- SolidJSは一般的にReactよりも優れたパフォーマンスを発揮し、特に頻繁な更新や大規模なデータセットを扱うアプリケーションで顕著です
- Reactは明示的なメモ化技術が必要ですが、SolidJSはリアクティブシステムのおかげで最適化の必要性が少なくなります
- SolidJSのコアライブラリサイズ(約7KB)は、React(約40KB)と比較して大幅に小さくなっています
コンポーネントモデル:根本的な違い
ReactとSolidJSは、似たようなJSX構文を持ちながらも、コンポーネントレンダリングに対して根本的に異なるアプローチを取っています。
Reactのコンポーネントモデル
Reactは以下の主要な特徴を持つレンダーベースモデルを使用します:
- コンポーネントは状態変更のたびに実行される関数です
- 実際のDOM更新を最小限に抑えるためにVirtual DOMを使用します
- 変更点を特定するためにdiffingに依存します
- コンポーネントはデフォルトでサブツリー全体を再レンダリングします
function Counter() {
const [count, setCount] = useState(0);
// この関数は毎回のレンダリングで実行される
console.log("Component rendering");
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
この例では、count
が変更されるたびにコンポーネント関数全体が再実行され、Reactの調整プロセスが必要なDOM更新を決定します。
SolidJSのコンポーネントモデル
SolidJSは以下の特徴を持つリアクティブコンパイルモデルを使用します:
- コンポーネントは初期化時に一度だけ実行されます
- Virtual DOMやdiffingアルゴリズムは使用しません
- 依存関係のリアクティブグラフを作成します
- 状態変更によって影響を受ける特定のDOMノードのみを更新します
function Counter() {
const [count, setCount] = createSignal(0);
// この関数は一度だけ実行される
console.log("Component initializing");
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>Increment</button>
</div>
);
}
SolidJSでは、コンポーネント関数は一度だけ実行されます。リアクティブシステムは、DOMのどの部分がどのシグナルに依存しているかを追跡し、値が変更されたときにその特定のノードのみを更新します。
リアクティビティシステムの比較
ReactとSolidJSのリアクティビティシステムは、状態変更がUIにどのように伝播するかを決定します。
ReactのHookベースリアクティビティ
Reactは粗粒度のリアクティビティシステムを使用します:
- 状態は
useState
やuseReducer
などのhookを通じて管理されます - 状態変更はコンポーネントの再レンダリングをトリガーします
- Reactは不要な再レンダリングを防ぐためにメモ化(
useMemo
、useCallback
、React.memo
)に依存します - データフローはpropsとcontextを通じて管理されます
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Build app", completed: false }
]);
// todosが変更されるとこのコンポーネント全体が再レンダリングされる
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? {...todo, completed: !todo.completed} : todo
));
};
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
/>
))}
</ul>
);
}
// このコンポーネントはpropsが変更されると再レンダリングされる
function TodoItem({ todo, onToggle }) {
console.log(`Rendering: ${todo.text}`);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
<span>{todo.text}</span>
</li>
);
}
SolidJSの細粒度リアクティビティ
SolidJSは細粒度のリアクティビティシステムを使用します:
- 状態は
createSignal
やcreateStore
などのリアクティブプリミティブを通じて管理されます - 更新は粒度が細かく、影響を受けるDOMノードのみを対象とします
- 広範囲なメモ化は不要です
- リアクティブ依存関係は自動的に追跡されます
function TodoList() {
const [todos, setTodos] = createStore([
{ id: 1, text: "Learn SolidJS", completed: false },
{ id: 2, text: "Build app", completed: false }
]);
const toggleTodo = (id) => {
setTodos(id, "completed", completed => !completed);
};
return (
<ul>
<For each={todos}>
{(todo) => (
<TodoItem todo={todo} onToggle={[toggleTodo, todo.id]} />
)}
</For>
</ul>
);
}
function TodoItem(props) {
// これは初期化時に一度だけログ出力される
console.log(`Creating: ${props.todo.text}`);
return (
<li>
<input
type="checkbox"
checked={props.todo.completed}
onChange={() => props.onToggle[0](props.onToggle[1])}
/>
<span>{props.todo.text}</span>
</li>
);
}
パフォーマンスベンチマークと分析
異なるコンポーネントモデルにより、ReactとSolidJSの間で大きなパフォーマンスの違いが生じます。
レンダリングパフォーマンス
JS Framework Benchmarkによると、SolidJSはほとんどの指標でReactを一貫して上回っています:
- 初期レンダリング: SolidJSは通常30-40%高速
- 更新パフォーマンス: SolidJSは部分更新で2-5倍高速
- メモリ使用量: SolidJSはコンパイルアプローチにより大幅に少ないメモリを使用
DOM操作
主要なパフォーマンス差は、DOM操作の処理方法に起因します:
- React: DOMの仮想表現を作成し、前のバージョンと比較してから変更を適用
- SolidJS: コンポーネントを中間表現なしで直接DOM操作にコンパイル
この違いは以下の場合により顕著になります:
- 大きなアイテムリスト
- 頻繁な状態更新
- 複雑なコンポーネントツリー
実際のコンポーネント例
これらの違いが実際の例でどのように現れるかを、フィルタリング可能なリストで検証してみましょう。
React実装
function FilterableList() {
const [items] = useState([
"Apple", "Banana", "Cherry", "Date", "Elderberry"
]);
const [filter, setFilter] = useState("");
// この計算は毎回のレンダリングで実行される
const filteredItems = items.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
Reactでは、フィルターが変更されると:
- コンポーネント全体が再レンダリングされる
- フィルタリングロジックが再実行される
- Reactが前のリストと新しいリストを比較する
- 変更されたDOMノードのみが更新される
SolidJS実装
function FilterableList() {
const [items] = createSignal([
"Apple", "Banana", "Cherry", "Date", "Elderberry"
]);
const [filter, setFilter] = createSignal("");
// これは依存関係が変更されたときのみ再計算される派生シグナルを作成
const filteredItems = createMemo(() =>
items().filter(item =>
item.toLowerCase().includes(filter().toLowerCase())
)
);
return (
<div>
<input
type="text"
value={filter()}
onInput={e => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<ul>
<For each={filteredItems()}>
{item => <li>{item}</li>}
</For>
</ul>
</div>
);
}
SolidJSでは、フィルターが変更されると:
filter
シグナルのみが更新されるfilteredItems
メモが再計算されるFor
コンポーネントが変更が必要なリストアイテムのみを効率的に更新する- コンポーネントの再レンダリングは発生しない
メモリ使用量とバンドルサイズ
コンポーネントモデルはメモリ使用量とバンドルサイズにも影響します:
メモリフットプリント
- React: コンポーネントインスタンス、fiberノード、Virtual DOMによる高いメモリ使用量
- SolidJS: 初期化後にコンポーネントがコンパイルされるため低いメモリ使用量
バンドルサイズ
- React: コアライブラリは約40KB(minified + gzipped)
- SolidJS: コアライブラリは約7KB(minified + gzipped)
この違いは、特にモバイルデバイスでのパフォーマンス重視アプリケーションにとって重要です。
最適化技術
各フレームワークは、そのコンポーネントモデルにより異なる最適化アプローチが必要です。
React最適化
React開発者は複数の最適化技術に精通する必要があります:
- 不要なコンポーネント再レンダリングを防ぐ
React.memo
- 高コストな計算をキャッシュする
useMemo
- 関数参照を安定化する
useCallback
- 大きなコンポーネントツリーの再レンダリングを避ける慎重な状態管理
// 最適化されたReactコンポーネント
const ExpensiveComponent = React.memo(({ data, onAction }) => {
// コンポーネントロジック
});
function ParentComponent() {
const [data, setData] = useState([]);
// 関数参照を安定化
const handleAction = useCallback((id) => {
// アクションロジック
}, []);
// 高コストな計算をキャッシュ
const processedData = useMemo(() => {
return data.map(item => expensiveProcess(item));
}, [data]);
return <ExpensiveComponent data={processedData} onAction={handleAction} />;
}
SolidJS最適化
SolidJSは明示的な最適化がより少なく済みます:
- 高コストな計算をキャッシュする
createMemo
- 粒度の細かい更新を確保する適切なシグナル使用
// 最小限の最適化が必要なSolidJSコンポーネント
function ParentComponent() {
const [data, setData] = createSignal([]);
// 関数の安定化は不要
const handleAction = (id) => {
// アクションロジック
};
// 高コストな計算をキャッシュ
const processedData = createMemo(() => {
return data().map(item => expensiveProcess(item));
});
return <ExpensiveComponent data={processedData()} onAction={handleAction} />;
}
まとめ
ReactとSolidJSの根本的な違いは、そのコンポーネントモデルにあります。Reactのレンダーベースアプローチはよりシンプルなメンタルモデルを提供しますが、パフォーマンス重視のアプリケーションではより多くの最適化が必要です。SolidJSのリアクティブコンパイルモデルは、より少ない最適化努力で優れたパフォーマンスを提供しますが、リアクティブプログラミングの概念を理解する必要があります。
これらのフレームワーク間の選択は、アプリケーションのパフォーマンス要件、チームの専門知識、エコシステムのニーズを考慮すべきです。Reactは成熟度と豊富なエコシステムを提供し、SolidJSはパフォーマンス上の利点とより効率的なリアクティビティモデルを提供します。
両フレームワークにはそれぞれの強みがあり、そのコンポーネントモデルを理解することで、次のプロジェクトについて十分な情報に基づいた決定を下すことができます。
よくある質問
SolidJSは通常ベンチマークでReactを上回りますが、実際のパフォーマンスはアプリケーションの特定のニーズに依存します。Reactは適切な最適化により、多くのアプリケーションで十分なパフォーマンスを発揮する可能性があります。
移行にはSolidJSのリアクティビティモデルを学ぶ必要がありますが、似たようなJSX構文により他のフレームワークよりも移行が容易です。ただし、React hooksの代わりにSolidJSのリアクティブプリミティブを使用するようにコンポーネントを書き直す必要があります。
SolidJSはReactのコア機能の大部分をカバーしていますが、エコシステムは小さくなっています。context、fragments、portals、suspenseなどの主要なReact機能に対する代替手段を提供しています。
成熟したエコシステム、豊富なコミュニティサポートが必要な場合、または確立されたチームで大規模なアプリケーションを構築する場合はReactを選択してください。パフォーマンスが重要、パフォーマンス重視のアプリケーションを構築する場合、またはより効率的なリアクティビティモデルを好む場合はSolidJSを選択してください。
SolidJSは以下を通じてより良いパフォーマンスを実現します:1) コンポーネントを効率的なDOM操作に変換するコンパイルベースアプローチ、2) 変更された部分のみを更新する細粒度のリアクティビティ、3) Virtual DOMのオーバーヘッドなし、4) より小さなメモリフットプリントを持つ最小限のランタイム。