Preact アーキテクチャ超入門:VNode・Diff・Renderer を図解で理解
Preact は React の軽量代替として人気を集めていますが、その内部構造はどのように動いているのでしょうか。 本記事では、Preact のコア機能である VNode・Diff・Renderer の 3 つの要素を図解を交えながら徹底解説します。これらの仕組みを理解することで、Preact の設計思想やパフォーマンスの秘密が見えてくるはずです。
背景
Preact の誕生と設計思想
Preact は 2015 年に Jason Miller 氏によって開発された、React の API 互換を持つ JavaScript ライブラリです。 React と同じ開発体験を提供しながらも、わずか 3KB という小さなサイズを実現しています。
この小ささの秘密は、Preact の洗練されたアーキテクチャにあります。 React が複雑な抽象化レイヤーを持つのに対し、Preact は必要最小限の機能に絞り込み、効率的な実装を追求しました。
なぜアーキテクチャを理解する必要があるのか
フレームワークの内部構造を知ることで、以下のようなメリットが得られます。
- パフォーマンス問題の原因を特定できる
- 最適化の方針を立てられる
- デバッグが容易になる
- より良いコード設計ができる
Preact のアーキテクチャは、他のフレームワークと比べてもシンプルで理解しやすいため、学習教材としても最適です。
Preact の全体像を把握するため、主要な 3 つの要素の関係性を図で示します。
mermaidflowchart TB
jsx["JSX コード"] -->|変換| vnode["VNode<br/>(仮想 DOM ツリー)"]
vnode -->|新旧比較| diff["Diff アルゴリズム"]
diff -->|変更箇所を抽出| patches["変更パッチ"]
patches -->|DOM 操作| renderer["Renderer"]
renderer -->|更新| dom["実際の DOM"]
style vnode fill:#e1f5ff
style diff fill:#fff4e1
style renderer fill:#e8f5e9
図の要点: JSX から VNode が生成され、Diff で変更を検出し、Renderer が最小限の DOM 操作を実行する流れです。
課題
仮想 DOM ライブラリが解決すべき問題
モダンな Web アプリケーション開発では、以下のような課題があります。
DOM 操作のパフォーマンス問題
直接 DOM を操作すると、レンダリングコストが高く、アプリケーションが重くなります。 特に大量のデータを扱う場合、DOM 操作の回数が増えるほどパフォーマンスが劣化していきます。
状態管理の複雑化
UI の状態とデータの状態が一致しない「状態の不整合」が発生しやすく、バグの温床となります。 どの部分を更新すべきか、手動で管理するのは非常に困難です。
コードの保守性の低下
命令的な DOM 操作は、コードの見通しを悪くし、メンテナンスコストを上げてしまいます。
Preact が取り組む設計上の課題
Preact は、これらの問題を解決しつつ、さらに以下の課題にも取り組んでいます。
| # | 課題 | Preact のアプローチ |
|---|---|---|
| 1 | バンドルサイズの肥大化 | 最小限のコードで最大の機能を提供 |
| 2 | 初期ロード時間の遅延 | 軽量な実装により高速起動を実現 |
| 3 | メモリ使用量の増加 | 効率的なデータ構造で省メモリ化 |
| 4 | 学習コストの高さ | シンプルな API と明確な設計思想 |
課題解決のために、Preact がどのような処理フローを採用しているかを図で示します。
mermaidflowchart LR
state["状態変更"] -->|setState| schedule["更新スケジュール"]
schedule -->|非同期| render["再レンダリング"]
render -->|生成| newVNode["新 VNode"]
oldVNode["旧 VNode"] -->|比較| diffProcess["Diff 処理"]
newVNode -->|比較| diffProcess
diffProcess -->|最小変更| domUpdate["DOM 更新"]
style schedule fill:#fff4e1
style diffProcess fill:#fff4e1
style domUpdate fill:#e8f5e9
図の補足: 状態変更から DOM 更新までの流れで、Diff 処理が最小限の変更のみを抽出する役割を担います。
解決策
VNode:軽量な仮想 DOM 表現
VNode(Virtual Node)は、実際の DOM ツリーを JavaScript オブジェクトで表現したものです。 Preact では、この VNode を極限まで軽量化することで、高速な処理を実現しています。
VNode の基本構造
VNode は以下のようなシンプルなオブジェクトで構成されています。
typescript// VNode の型定義
interface VNode {
type: string | Function; // 要素タイプ("div" など)またはコンポーネント
props: Props | null; // プロパティ(属性や children を含む)
key: any; // リスト内での一意識別子
ref: any; // DOM 参照用
_children: VNode[] | null; // 子要素の配列
_parent: VNode | null; // 親 VNode への参照
_dom: PreactElement | null; // 実際の DOM 要素への参照
_component: Component | null; // コンポーネントインスタンス
_depth: number; // ツリーの深さ
}
VNode 生成の仕組み
JSX は Babel などのトランスパイラによって、h() 関数(または createElement())の呼び出しに変換されます。
JSX コードの例を見てみましょう。
jsx// JSX で書かれたコンポーネント
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
上記のコードは、以下のように変換されます。
javascript// トランスパイル後の JavaScript コード
function Greeting({ name }) {
return h('h1', null, 'Hello, ', name, '!');
}
h() 関数は VNode オブジェクトを生成します。
javascript// h() 関数の簡略化した実装イメージ
function h(type, props, ...children) {
return {
type, // 要素タイプ
props: { ...props, children }, // プロパティと子要素
key: props?.key, // キー
ref: props?.ref, // ref
_children: null, // 正規化前は null
_parent: null,
_dom: null,
_component: null,
_depth: 0,
};
}
VNode ツリーの構築
複数の VNode が親子関係を持つことで、ツリー構造を形成します。
ネストされた JSX の例です。
jsx// ネストされた構造の JSX
function App() {
return (
<div className='container'>
<h1>タイトル</h1>
<p>本文テキスト</p>
</div>
);
}
これが以下のような VNode ツリーに変換されます。
javascript// 生成される VNode ツリー(簡略版)
{
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: ['タイトル'] }
},
{
type: 'p',
props: { children: ['本文テキスト'] }
}
]
}
}
VNode がどのように DOM にマッピングされるかを図で示します。
mermaidflowchart TB
subgraph VNodeTree["VNode ツリー"]
vroot["VNode<br/>type: 'div'"]
vh1["VNode<br/>type: 'h1'"]
vp["VNode<br/>type: 'p'"]
vroot --> vh1
vroot --> vp
end
subgraph DOMTree["DOM ツリー"]
droot["<div>"]
dh1["<h1>タイトル</h1>"]
dp["<p>本文</p>"]
droot --> dh1
droot --> dp
end
VNodeTree -->|マッピング| DOMTree
style vroot fill:#e1f5ff
style droot fill:#e8f5e9
図の要点: VNode ツリーの構造が、そのまま DOM ツリーに対応していることがわかります。
Diff:効率的な変更検出アルゴリズム
Diff アルゴリズムは、旧 VNode と新 VNode を比較し、実際に変更が必要な部分だけを特定します。 Preact の Diff は、O(n) の時間複雑度で動作する高速なアルゴリズムです。
Diff の基本原則
Preact の Diff は、以下の 3 つの前提に基づいています。
| # | 前提 | 説明 |
|---|---|---|
| 1 | 異なるタイプの要素は異なるツリーを生成 | <div> と <span> は完全に置き換え |
| 2 | key プロパティで要素の同一性を判定 | リスト内での要素の追跡に使用 |
| 3 | 同じ階層のみ比較 | 深さ優先で順次処理 |
Diff のメインループ
Diff 処理の中心となるのが diff() 関数です。
typescript// diff() 関数の概要(簡略版)
function diff(
parentDom: PreactElement, // 親 DOM 要素
newVNode: VNode, // 新しい VNode
oldVNode: VNode, // 古い VNode
context: object // コンテキスト
) {
let newType = newVNode.type;
// タイプが異なる場合は完全置き換え
if (oldVNode.type !== newType) {
unmount(oldVNode); // 古い要素を削除
newVNode._dom = null; // 新規作成フラグ
}
}
タイプが同じ場合、属性と子要素の Diff を行います。
typescript// 要素タイプが同じ場合の処理
if (typeof newType === 'string') {
// ネイティブ要素(div, span など)の場合
diffElementNodes(
newVNode._dom,
newVNode,
oldVNode,
context
);
} else {
// コンポーネントの場合
diffComponent(parentDom, newVNode, oldVNode, context);
}
属性の Diff
属性(props)の差分を検出し、変更があった属性のみを更新します。
javascript// 属性の Diff 処理(簡略版)
function diffProps(dom, newProps, oldProps) {
// 削除された属性を処理
for (let name in oldProps) {
if (!(name in newProps)) {
setProperty(dom, name, null, oldProps[name]);
}
}
// 新規追加・変更された属性を処理
for (let name in newProps) {
if (oldProps[name] !== newProps[name]) {
setProperty(
dom,
name,
newProps[name],
oldProps[name]
);
}
}
}
属性設定の実装例です。
javascript// 属性を DOM に設定する関数
function setProperty(dom, name, value, oldValue) {
// イベントハンドラの場合
if (name[0] === 'o' && name[1] === 'n') {
let eventName = name.toLowerCase().substring(2);
if (value) {
dom.addEventListener(eventName, value);
}
if (oldValue) {
dom.removeEventListener(eventName, oldValue);
}
}
// style の場合
else if (name === 'style') {
if (typeof value === 'string') {
dom.style.cssText = value;
}
}
// 通常の属性
else if (name !== 'children') {
if (value == null) {
dom.removeAttribute(name);
} else {
dom.setAttribute(name, value);
}
}
}
子要素の Diff
子要素の配列を比較し、追加・削除・移動を効率的に処理します。
javascript// 子要素の Diff(簡略版)
function diffChildren(
parentDom,
newChildren,
oldChildren,
context
) {
let oldChildrenLength = oldChildren.length;
let newChildrenLength = newChildren.length;
// key でマッピングを作成
let oldKeyed = {};
for (let i = 0; i < oldChildrenLength; i++) {
let oldChild = oldChildren[i];
if (oldChild.key != null) {
oldKeyed[oldChild.key] = oldChild;
}
}
}
新しい子要素と対応する旧要素を探します。
javascript// 新しい子要素を処理
for (let i = 0; i < newChildrenLength; i++) {
let newChild = newChildren[i];
let oldChild = null;
// key が一致する要素を探す
if (newChild.key != null) {
oldChild = oldKeyed[newChild.key];
}
// key がない場合はタイプで探す
else {
for (let j = 0; j < oldChildrenLength; j++) {
if (oldChildren[j].type === newChild.type) {
oldChild = oldChildren[j];
break;
}
}
}
// 再帰的に Diff を実行
diff(parentDom, newChild, oldChild, context);
}
Diff アルゴリズムの処理フローを図で示します。
mermaidflowchart TD
start["Diff 開始"] --> typeCheck["タイプ比較"]
typeCheck -->|異なる| replace["要素を置き換え"]
typeCheck -->|同じ| checkType["要素タイプ判定"]
checkType -->|ネイティブ要素| diffProps["属性 Diff"]
checkType -->|コンポーネント| diffComp["コンポーネント Diff"]
diffProps --> diffChild["子要素 Diff"]
diffComp --> diffChild
diffChild --> keyCheck["key の有無"]
keyCheck -->|あり| keyMatch["key でマッチング"]
keyCheck -->|なし| typeMatch["タイプでマッチング"]
keyMatch --> recurse["再帰的に Diff"]
typeMatch --> recurse
recurse --> done["Diff 完了"]
replace --> done
style typeCheck fill:#fff4e1
style diffProps fill:#fff4e1
style diffChild fill:#fff4e1
図の補足: タイプチェックから始まり、属性・子要素の順に Diff を実行し、key の有無で最適な比較方法を選択します。
Renderer:最小限の DOM 更新
Renderer は、Diff で検出された変更を実際の DOM に反映させる役割を担います。 Preact の Renderer は、必要最小限の DOM 操作のみを実行することで、高速な更新を実現しています。
DOM の作成
新しい要素を DOM に追加する処理です。
javascript// DOM 要素の作成(簡略版)
function createDom(vnode, context) {
let dom;
// テキストノードの場合
if (
typeof vnode === 'string' ||
typeof vnode === 'number'
) {
dom = document.createTextNode(vnode);
}
// 通常の要素の場合
else {
dom = document.createElement(vnode.type);
// 属性を設定
if (vnode.props) {
diffProps(dom, vnode.props, {});
}
}
vnode._dom = dom;
return dom;
}
子要素も再帰的に作成します。
javascript// 子要素の作成と追加
function mountChildren(parentDom, children, context) {
for (let i = 0; i < children.length; i++) {
let child = children[i];
let childDom = createDom(child, context);
parentDom.appendChild(childDom);
}
}
DOM の更新
既存の DOM 要素を更新する処理です。
javascript// DOM 要素の更新(簡略版)
function updateDom(dom, newVNode, oldVNode) {
// 属性の差分を適用
diffProps(dom, newVNode.props, oldVNode.props);
// 子要素の差分を適用
diffChildren(
dom,
newVNode._children,
oldVNode._children,
context
);
newVNode._dom = dom;
}
DOM の削除
不要になった要素を削除する処理です。
javascript// DOM 要素の削除とクリーンアップ
function unmount(vnode) {
// コンポーネントのライフサイクルメソッド呼び出し
if (vnode._component) {
if (vnode._component.componentWillUnmount) {
vnode._component.componentWillUnmount();
}
}
// 子要素も再帰的に削除
if (vnode._children) {
for (let i = 0; i < vnode._children.length; i++) {
unmount(vnode._children[i]);
}
}
}
DOM から要素を実際に削除します。
javascript// DOM からの削除
function removeNode(node) {
let parentNode = node.parentNode;
if (parentNode) {
parentNode.removeChild(node);
}
}
バッチ更新とスケジューリング
Preact は、複数の状態更新を効率的にまとめて処理します。
javascript// レンダリングキューの管理
let renderQueue = [];
let scheduled = false;
function enqueueRender(component) {
// キューに追加
if (renderQueue.indexOf(component) === -1) {
renderQueue.push(component);
}
// まだスケジュールされていなければ実行
if (!scheduled) {
scheduled = true;
Promise.resolve().then(flushQueue);
}
}
キューに溜まった更新を一括処理します。
javascript// キューのフラッシュ(一括処理)
function flushQueue() {
let queue = renderQueue.slice();
renderQueue.length = 0;
scheduled = false;
// すべてのコンポーネントを再レンダリング
queue.forEach((component) => {
if (component._dirty) {
renderComponent(component);
}
});
}
Renderer の動作フローを図で示します。
mermaidstateDiagram-v2
[*] --> CheckVNode: VNode 受信
CheckVNode --> CreateDOM: 新規 VNode
CheckVNode --> UpdateDOM: 既存 VNode
CheckVNode --> RemoveDOM: 削除対象
CreateDOM --> SetProps: 要素作成
SetProps --> MountChildren: 属性設定
MountChildren --> AppendDOM: 子要素マウント
AppendDOM --> [*]: DOM に追加
UpdateDOM --> DiffProps: 属性 Diff
DiffProps --> DiffChildren: 子要素 Diff
DiffChildren --> ApplyChanges: 変更適用
ApplyChanges --> [*]
RemoveDOM --> Unmount: アンマウント処理
Unmount --> DetachDOM: DOM から削除
DetachDOM --> [*]
note right of CreateDOM: 新規要素の作成フロー
note right of UpdateDOM: 既存要素の更新フロー
note right of RemoveDOM: 要素の削除フロー
図の要点: VNode の状態に応じて、作成・更新・削除の 3 つの処理フローに分岐し、それぞれ最適な DOM 操作を実行します。
具体例
シンプルなカウンターアプリで理解する
実際にコードを動かしながら、VNode・Diff・Renderer の動作を追ってみましょう。
カウンターコンポーネントの実装
基本的なカウンターコンポーネントを作成します。
jsximport { h, Component } from 'preact';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
}
レンダリングメソッドを定義します。
jsxrender() {
return (
<div className="counter">
<h2>カウント: {this.state.count}</h2>
<button onClick={this.increment}>
増やす
</button>
</div>
);
}
初回レンダリング時の VNode
初回レンダリング時に生成される VNode ツリーを見てみましょう。
javascript// 初回レンダリング時の VNode(count: 0)
{
type: 'div',
props: {
className: 'counter',
children: [
{
type: 'h2',
props: {
children: ['カウント: ', 0]
},
_dom: <h2 要素への参照>
},
{
type: 'button',
props: {
onClick: [increment 関数],
children: ['増やす']
},
_dom: <button 要素への参照>
}
]
},
_dom: <div 要素への参照>,
_component: <Counter インスタンス>
}
ボタンクリック後の VNode
ボタンをクリックして count が 1 になった時の VNode です。
javascript// 2回目レンダリング時の VNode(count: 1)
{
type: 'div',
props: {
className: 'counter',
children: [
{
type: 'h2',
props: {
children: ['カウント: ', 1] // ← 変更箇所
},
_dom: <h2 要素への参照>
},
{
type: 'button',
props: {
onClick: [increment 関数],
children: ['増やす']
},
_dom: <button 要素への参照>
}
]
},
_dom: <div 要素への参照>,
_component: <Counter インスタンス>
}
Diff の実行過程
新旧の VNode を比較する過程を追います。
javascript// Diff 処理のステップ(擬似コード)
// ステップ1: ルート要素の比較
diff(<div 新>, <div 旧>)
// → タイプが同じ('div') → 更新処理へ
// ステップ2: 属性の比較
diffProps({ className: 'counter' }, { className: 'counter' })
// → 変更なし
// ステップ3: 子要素の比較(h2 要素)
diff(<h2 新>, <h2 旧>)
// → タイプが同じ('h2') → 更新処理へ
テキストノードの比較を行います。
javascript// ステップ4: h2 の子要素(テキスト)の比較
diffChildren(
['カウント: ', 1], // 新
['カウント: ', 0] // 旧
);
// → 2番目の要素が変更 (0 → 1)
// → テキストノードの更新をスケジュール
button 要素は変更なしです。
javascript// ステップ5: button 要素の比較
diff(<button 新>, <button 旧>)
// → すべて同じ → スキップ
Renderer による DOM 更新
Diff で検出された変更を DOM に適用します。
javascript// Renderer の実行(擬似コード)
// 変更箇所: h2 内のテキストノード
let textNode = h2Element.childNodes[1]; // "0" のテキストノード
textNode.nodeValue = '1'; // "1" に更新
// ← これだけ!他の DOM 操作は不要
カウンター更新時の処理フローを図で示します。
mermaidsequenceDiagram
participant User as ユーザー
participant Button as Button 要素
participant Component as Counter コンポーネント
participant VNode as VNode 生成
participant Diff as Diff エンジン
participant Renderer as Renderer
participant DOM as 実際の DOM
User->>Button: クリック
Button->>Component: increment() 呼び出し
Component->>Component: setState({ count: 1 })
Component->>VNode: render() 実行
VNode->>VNode: 新 VNode 生成
VNode->>Diff: 新旧 VNode を渡す
Diff->>Diff: ツリーを比較
Diff->>Diff: テキスト変更を検出
Diff->>Renderer: 変更パッチを渡す
Renderer->>DOM: textNode.nodeValue = "1"
DOM->>User: 画面更新
図の補足: ユーザーのクリックから画面更新までの一連の流れで、各コンポーネントがどのように連携しているかが分かります。
リストのレンダリングで key の重要性を知る
key プロパティがないと、Diff が非効率になる例を見てみましょう。
key なしのリスト
key を指定しない場合の実装です。
jsxfunction TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li>{todo.text}</li>
))}
</ul>
);
}
初期状態のデータです。
javascript// 初期状態
const todos = [
{ id: 1, text: 'タスク A' },
{ id: 2, text: 'タスク B' },
{ id: 3, text: 'タスク C' },
];
先頭に新しいタスクを追加した状態です。
javascript// タスクを先頭に追加
const newTodos = [
{ id: 4, text: 'タスク D' }, // ← 新規追加
{ id: 1, text: 'タスク A' },
{ id: 2, text: 'タスク B' },
{ id: 3, text: 'タスク C' },
];
Diff の処理(非効率な例)を示します。
javascript// key がない場合の Diff
// すべての <li> が変更されたと判断される
// 1番目: "タスク A" → "タスク D" に更新
// 2番目: "タスク B" → "タスク A" に更新
// 3番目: "タスク C" → "タスク B" に更新
// 4番目: 新規作成 "タスク C"
// → 4つの DOM 更新が発生(非効率)
key ありのリスト
key を指定した正しい実装です。
jsxfunction TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Diff の処理(効率的な例)を示します。
javascript// key がある場合の Diff
// key で要素を追跡できる
// key=4: 新規作成 "タスク D"
// key=1: 移動(変更なし)
// key=2: 移動(変更なし)
// key=3: 移動(変更なし)
// → 1つの DOM 作成 + 3つの移動のみ(効率的)
key による最適化の仕組み
key がある場合の Diff アルゴリズムです。
javascript// key を使った Diff(簡略版)
function diffKeyedChildren(newChildren, oldChildren) {
// 旧要素を key でマッピング
let oldKeyed = new Map();
oldChildren.forEach((child) => {
if (child.key != null) {
oldKeyed.set(child.key, child);
}
});
// 新要素を処理
newChildren.forEach((newChild) => {
let oldChild = oldKeyed.get(newChild.key);
if (oldChild) {
// 既存要素が見つかった → 再利用
if (oldChild.props.text === newChild.props.text) {
// 内容も同じ → DOM 操作不要
reuseNode(oldChild);
} else {
// 内容が変わった → 更新のみ
updateNode(oldChild, newChild);
}
} else {
// 新規要素 → 作成
createNode(newChild);
}
});
}
key の有無による Diff の違いを図で比較します。
mermaidflowchart LR
subgraph NoKey["key なし(非効率)"]
direction TB
old1["li: タスクA"] -.更新.-> new1["li: タスクD"]
old2["li: タスクB"] -.更新.-> new2["li: タスクA"]
old3["li: タスクC"] -.更新.-> new3["li: タスクB"]
none["(なし)"] -.作成.-> new4["li: タスクC"]
end
subgraph WithKey["key あり(効率的)"]
direction TB
none2["(なし)"] -.作成.-> newA["li key=4: タスクD"]
oldA["li key=1: タスクA"] -.再利用.-> newB["li key=1: タスクA"]
oldB["li key=2: タスクB"] -.再利用.-> newC["li key=2: タスクB"]
oldC["li key=3: タスクC"] -.再利用.-> newD["li key=3: タスクC"]
end
style new1 fill:#ffcccc
style new2 fill:#ffcccc
style new3 fill:#ffcccc
style new4 fill:#ffcccc
style newA fill:#ccffcc
style newB fill:#ccffcc
style newC fill:#ccffcc
style newD fill:#ccffcc
図で理解できる要点:
- key なしの場合、すべての要素が更新扱いとなり、4 つの DOM 操作が発生します
- key ありの場合、既存要素を再利用し、1 つの DOM 作成のみで済みます
- key により、要素の同一性を正確に追跡できることがパフォーマンス向上の鍵です
条件付きレンダリングでの最適化
条件によって表示内容を切り替える際の挙動を見てみましょう。
ログイン状態の切り替え
ログイン状態に応じて異なる UI を表示するコンポーネントです。
jsxfunction App({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<div className='dashboard'>
<h1>ダッシュボード</h1>
<p>ようこそ!</p>
</div>
) : (
<div className='login'>
<h1>ログイン</h1>
<button>ログインする</button>
</div>
)}
</div>
);
}
ログアウト状態の VNode です。
javascript// isLoggedIn = false の VNode
{
type: 'div',
props: {
children: [
{
type: 'div',
props: {
className: 'login',
children: [
{ type: 'h1', props: { children: ['ログイン'] } },
{ type: 'button', props: { children: ['ログインする'] } }
]
}
}
]
}
}
ログイン後の VNode です。
javascript// isLoggedIn = true の VNode
{
type: 'div',
props: {
children: [
{
type: 'div',
props: {
className: 'dashboard', // ← className が変更
children: [
{ type: 'h1', props: { children: ['ダッシュボード'] } }, // ← テキスト変更
{ type: 'p', props: { children: ['ようこそ!'] } } // ← button から p に変更
]
}
}
]
}
}
Diff の実行
条件切り替え時の Diff 処理を追います。
javascript// ログイン時の Diff
// ルート div は同じ → 更新
diff(<div 新>, <div 旧>)
// 子の div も同じ → 更新
diff(<div.dashboard 新>, <div.login 旧>)
// className 属性が変更
diffProps({ className: 'dashboard' }, { className: 'login' })
// → dom.className = 'dashboard'
子要素の Diff を実行します。
javascript// h1 要素: タイプは同じ、テキストが変更
diff(<h1 新>, <h1 旧>)
// → テキストノード更新: "ログイン" → "ダッシュボード"
// 2番目の子要素: button から p に変更
diff(<p 新>, <button 旧>)
// → タイプが異なる → 完全置き換え
// → button を削除、p を新規作成
効率的な条件分岐の書き方
不要な DOM 再作成を避けるため、構造を統一します。
jsx// 改善版: 外側の構造を統一
function App({ isLoggedIn }) {
return (
<div className={isLoggedIn ? 'dashboard' : 'login'}>
<h1>{isLoggedIn ? 'ダッシュボード' : 'ログイン'}</h1>
{isLoggedIn ? (
<p>ようこそ!</p>
) : (
<button>ログインする</button>
)}
</div>
);
}
この実装により、以下のメリットが得られます。
| # | メリット | 説明 |
|---|---|---|
| 1 | div の再利用 | 外側の div が再利用され、className のみ更新 |
| 2 | h1 の再利用 | h1 要素が再利用され、テキストのみ更新 |
| 3 | 最小限の DOM 操作 | p と button のみ作成・削除が発生 |
まとめ
本記事では、Preact のコアアーキテクチャである VNode・Diff・Renderer の 3 つの要素を、図解と具体例を交えて解説しました。
VNode は JavaScript オブジェクトで DOM を表現し、軽量で高速な処理を可能にします。
JSX から h() 関数を経て VNode ツリーが構築され、実際の DOM と対応関係を持ちます。
Diff アルゴリズムは、旧 VNode と新 VNode を O(n) の時間複雑度で比較し、変更箇所を効率的に検出します。 タイプ比較・属性 Diff・子要素 Diff という段階的な処理により、必要最小限の更新を特定できます。
Renderer は、Diff で検出された変更を実際の DOM に反映し、バッチ更新とスケジューリングにより効率的に処理します。 作成・更新・削除の 3 つの操作を適切に使い分け、パフォーマンスを最適化しています。
これらの仕組みを理解することで、以下のような実践的なスキルが身につきます。
- key プロパティを適切に使い、リストレンダリングを最適化できる
- 条件分岐時の DOM 再作成を最小化できる
- パフォーマンス問題の原因を特定し、改善できる
- より良いコンポーネント設計ができる
Preact のシンプルで洗練されたアーキテクチャは、他のフレームワークの学習にも役立つ知識です。 ぜひ実際にコードを書きながら、これらの仕組みを体感してみてください。
関連リンク
articlePreact アーキテクチャ超入門:VNode・Diff・Renderer を図解で理解
articlePreact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI
articlePreact で Hydration mismatch が出る原因と完全解決チェックリスト
articlePreact で埋め込みウィジェット配布:他サイトに設置できる軽量 UI の作り方
articlePreact でミニブログを 1 日で公開:ルーティング・MDX・SEO まで一気通貫
articlePreact でスケーラブルな状態管理:Signals/Context/外部ストアの責務分離
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来