T-CREATOR

SolidJS と Reactivity Graph:パフォーマンスの秘密を探る

SolidJS と Reactivity Graph:パフォーマンスの秘密を探る

SolidJS が他のフレームワークと比べて圧倒的なパフォーマンスを実現している秘密は、その革新的な Reactivity Graph システムにあります。

従来の React や Vue とは根本的に異なるアプローチを取る SolidJS は、コンパイル時に依存関係を解析し、ランタイムでの不要な再計算を完全に排除します。

この記事では、SolidJS の Reactivity Graph がどのように動作し、なぜこれほどの高速化を実現できるのかを詳しく解説していきます。

Reactivity Graph の基本概念

依存関係の自動追跡メカニズム

SolidJS の Reactivity Graph は、Signal と Effect の間の依存関係を自動的に追跡し、グラフ構造として管理します。

typescriptimport { createSignal, createEffect } from 'solid-js';

// Signal の作成
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('John');

// Effect で依存関係を自動追跡
createEffect(() => {
  console.log(`${name()} のカウント: ${count()}`);
});

このコードでは、Effect 内で name()count() を呼び出すことで、自動的に依存関係が構築されます。

重要なポイント:

  • Signal の値を読み取るだけで依存関係が自動生成される
  • 読み取られなかった Signal は依存関係に含まれない
  • 動的に依存関係が変化する

グラフ構造による最適化

Reactivity Graph は有向グラフとして実装されており、各ノードが Signal や Effect を表します。

typescript// 複雑な依存関係の例
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const [age, setAge] = createSignal(25);

// 計算された Signal
const fullName = createMemo(
  () => `${firstName()} ${lastName()}`
);
const isAdult = createMemo(() => age() >= 18);

// 複数の依存関係を持つ Effect
createEffect(() => {
  if (isAdult()) {
    console.log(`${fullName()} は大人です`);
  } else {
    console.log(`${fullName()} は未成年です`);
  }
});

この構造により、firstName が変更された場合、fullName とその Effect のみが更新され、ageisAdult は再計算されません。

SolidJS の Reactivity Graph 実装

Signal と Effect の関係性

Signal は値のコンテナであり、Effect はその値の変化を監視するリスナーです。

typescript// Signal の内部構造(概念的な実装)
class Signal<T> {
  private value: T;
  private subscribers: Set<Effect> = new Set();

  constructor(initialValue: T) {
    this.value = initialValue;
  }

  get(): T {
    // 現在実行中の Effect を依存関係に追加
    if (currentEffect) {
      this.subscribers.add(currentEffect);
    }
    return this.value;
  }

  set(newValue: T): void {
    this.value = newValue;
    // 依存している Effect を全て実行
    this.subscribers.forEach((effect) => effect.execute());
  }
}

実際のエラー例:

typescript// ❌ よくある間違い:Signal の値を直接変更
const [user, setUser] = createSignal({
  name: 'John',
  age: 25,
});
user().name = 'Jane'; // これは反応しない!

// ✅ 正しい方法:setter を使用
setUser((prev) => ({ ...prev, name: 'Jane' }));

依存関係の構築プロセス

SolidJS は Effect の実行時に、アクセスされた Signal を自動的に追跡します。

typescript// 依存関係の構築例
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
const [c, setC] = createSignal(3);

createEffect(() => {
  // この時点で a と b の依存関係が構築される
  const sum = a() + b();
  console.log(`合計: ${sum}`);

  // c は読み取られていないので依存関係に含まれない
});

パフォーマンスの秘密:

  • 読み取られなかった Signal は依存関係に含まれない
  • 条件分岐によって動的に依存関係が変化する
  • 不要な更新を完全に回避できる

グラフの更新アルゴリズム

SolidJS の更新アルゴリズムは、トポロジカルソートを使用して最適な更新順序を決定します。

typescript// 複雑な依存関係の例
const [price, setPrice] = createSignal(100);
const [tax, setTax] = createSignal(0.1);
const [discount, setDiscount] = createSignal(0.2);

// 計算された値
const subtotal = createMemo(
  () => price() * (1 - discount())
);
const total = createMemo(() => subtotal() * (1 + tax()));

// 表示用の Effect
createEffect(() => {
  console.log(`小計: ${subtotal()}, 合計: ${total()}`);
});

この場合の更新順序:

  1. price が変更 → subtotal を更新
  2. subtotal が変更 → total を更新
  3. total が変更 → Effect を実行

パフォーマンス最適化の仕組み

不要な再計算の回避

SolidJS は値が実際に変更された場合のみ更新を実行します。

typescript// 値の比較による最適化
const [user, setUser] = createSignal({
  id: 1,
  name: 'John',
});

// 同じ値でもオブジェクトが異なる場合
setUser({ id: 1, name: 'John' }); // 新しいオブジェクトなので更新される

// 値が実際に同じ場合
setUser((prev) => prev); // 同じ参照なので更新されない

実際のエラー例:

typescript// ❌ パフォーマンスが悪い例
const [items, setItems] = createSignal([1, 2, 3]);

// 毎回新しい配列を作成
setItems([...items(), 4]);

// ✅ パフォーマンスが良い例
setItems((prev) => [...prev, 4]);

メモリ効率の向上

SolidJS は不要になった依存関係を自動的にクリーンアップします。

typescript// メモリ効率の良い例
const [showDetails, setShowDetails] = createSignal(false);
const [user, setUser] = createSignal({
  name: 'John',
  details: '...',
});

createEffect(() => {
  if (showDetails()) {
    // showDetails が true の時のみ user.details に依存
    console.log(user().details);
  }
  // showDetails が false の時は user.details への依存関係が自動的に削除される
});

メモリリークを防ぐテクニック:

typescript// クリーンアップ関数の使用
createEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log('定期実行');
  }, 1000);

  // Effect が再実行される際に自動的にクリーンアップ
  onCleanup(() => clearInterval(timer));
});

レンダリング最適化

SolidJS は仮想 DOM を使用せず、直接 DOM を更新します。

typescript// レンダリング最適化の例
function Counter() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <p>カウント: {count()}</p>
      <button onClick={() => setCount((c) => c + 1)}>
        増加
      </button>
    </div>
  );
}

この場合、count が変更されると、{count()} の部分のみが更新され、他の DOM 要素は変更されません。

React との比較分析

仮想 DOM vs Reactivity Graph

React と SolidJS の根本的な違いを理解しましょう。

React のアプローチ:

typescript// React の場合
function ReactCounter() {
  const [count, setCount] = useState(0);

  // コンポーネント全体が再レンダリングされる
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        増加
      </button>
    </div>
  );
}

SolidJS のアプローチ:

typescript// SolidJS の場合
function SolidCounter() {
  const [count, setCount] = createSignal(0);

  // count の値のみが更新される
  return (
    <div>
      <p>カウント: {count()}</p>
      <button onClick={() => setCount((c) => c + 1)}>
        増加
      </button>
    </div>
  );
}

パフォーマンス測定結果

実際のベンチマーク結果を見てみましょう。

typescript// パフォーマンス測定の例
import { createSignal, createEffect } from 'solid-js';

// 大量の Signal を作成
const signals = Array.from({ length: 1000 }, (_, i) =>
  createSignal(i)
);

// パフォーマンス測定
const start = performance.now();

createEffect(() => {
  signals.forEach(([get]) => get());
});

const end = performance.now();
console.log(`依存関係構築時間: ${end - start}ms`);

一般的なベンチマーク結果:

  • React: 仮想 DOM の差分計算が必要
  • Vue: リアクティブシステム + 仮想 DOM
  • SolidJS: 直接更新のみ

メモリ使用量の違い

SolidJS はメモリ効率が優れています。

typescript// メモリ使用量の比較例
const [data, setData] = createSignal(
  new Array(10000).fill(0)
);

// SolidJS: 元のデータを直接更新
setData((prev) => {
  prev[0] = 1; // 新しい配列を作成しない
  return prev;
});

// React: 新しい配列を作成
setData((prev) => {
  const newData = [...prev];
  newData[0] = 1; // 新しい配列を作成
  return newData;
});

実践的な活用テクニック

効率的な Signal 設計

Signal の設計はパフォーマンスに大きく影響します。

typescript// ❌ 非効率な設計
const [user, setUser] = createSignal({
  firstName: 'John',
  lastName: 'Doe',
  age: 25,
  email: 'john@example.com',
});

// 一部の情報だけ更新したい場合でも全体を更新
setUser((prev) => ({ ...prev, age: 26 }));

// ✅ 効率的な設計
const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const [age, setAge] = createSignal(25);
const [email, setEmail] = createSignal('john@example.com');

// 必要な部分のみ更新
setAge(26);

複雑な状態管理の例:

typescript// 効率的な状態管理
const [todos, setTodos] = createSignal([]);
const [filter, setFilter] = createSignal('all');

// フィルタリングされた結果
const filteredTodos = createMemo(() => {
  const currentTodos = todos();
  const currentFilter = filter();

  switch (currentFilter) {
    case 'completed':
      return currentTodos.filter((todo) => todo.completed);
    case 'active':
      return currentTodos.filter((todo) => !todo.completed);
    default:
      return currentTodos;
  }
});

Effect の最適化手法

Effect の最適化は重要なテクニックです。

typescript// ❌ 非効率な Effect
const [user, setUser] = createSignal({
  name: 'John',
  age: 25,
});
const [theme, setTheme] = createSignal('light');

createEffect(() => {
  // theme が変更されても実行される(不要)
  console.log(
    `ユーザー: ${user().name}, 年齢: ${user().age}`
  );
});

// ✅ 効率的な Effect
createEffect(() => {
  const currentUser = user();
  console.log(
    `ユーザー: ${currentUser.name}, 年齢: ${currentUser.age}`
  );
});

バッチ処理の活用:

typescript// バッチ処理による最適化
import { batch } from 'solid-js';

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Doe');
const [age, setAge] = createSignal(25);

// 複数の更新を一度に実行
batch(() => {
  setFirstName('Jane');
  setLastName('Smith');
  setAge(30);
});
// Effect は一度だけ実行される

パフォーマンス監視ツール

SolidJS のパフォーマンスを監視するツールを活用しましょう。

typescript// 開発時のパフォーマンス監視
import { createRoot, createSignal } from 'solid-js';

// 開発モードでの詳細ログ
if (process.env.NODE_ENV === 'development') {
  createRoot((dispose) => {
    const [count, setCount] = createSignal(0);

    createEffect(() => {
      console.log('Effect 実行:', count());
    });

    // クリーンアップ時にログ
    dispose();
  });
}

実際のエラー例と解決策:

typescript// ❌ 無限ループの例
const [count, setCount] = createSignal(0);

createEffect(() => {
  setCount(count() + 1); // 無限ループ!
});

// ✅ 解決策:条件付き更新
createEffect(() => {
  if (count() < 10) {
    setCount(count() + 1);
  }
});

まとめ

SolidJS の Reactivity Graph は、従来のフレームワークとは根本的に異なるアプローチでパフォーマンスを実現しています。

重要なポイント:

  1. 自動依存関係追跡: Signal の読み取りだけで依存関係が自動構築される
  2. グラフ構造による最適化: 不要な更新を完全に回避
  3. 直接 DOM 更新: 仮想 DOM 不要で高速レンダリング
  4. メモリ効率: 不要な依存関係の自動クリーンアップ

SolidJS を学ぶことで、リアクティブプログラミングの本質を理解し、より効率的なアプリケーション開発が可能になります。

パフォーマンスが重要なプロジェクトでは、SolidJS の Reactivity Graph の仕組みを理解し、適切に活用することで、ユーザー体験を劇的に向上させることができるでしょう。

関連リンク