SolidJSとSignals API:VDOMのオーバーヘッドなしで高性能なUIを構築する

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

仮想DOMに隠されたコスト

この10年間、仮想DOM(VDOM)はパフォーマンスの黄金律(ゴールドスタンダード)だと言われてきました。Reactは、DOMを直接操作するよりもメモリ上のツリーで変更を計算する方が高速であるという考えを広めました。それはしばらくの間、優れた抽象化として機能していました。

しかし、モダンなWebアプリが複雑になるにつれ、その抽象化がボトルネックになってきました。特に状態(state)が変わるたびに、フレームワークはコンポーネントツリー全体を「比較(diff)」することを強いられます。速度を維持するために、エンジニアはエンジンが不要な処理をしないよう、useMemouseCallbackの調整に追われることになります。

私のチームは最近、リアルタイム分析ダッシュボードを構築している際にこの限界に直面しました。徹底的な最適化を行っても、50列のデータテーブルは60fpsを維持するのに苦労し、更新が激しい時には15fpsまで落ち込むこともありました。その不満から私たちはSolidJSに辿り着きました。そして、問題は私たちのコードではなく、VDOMベースのフレームワークの根本的なアーキテクチャにあることにすぐに気づきました。SolidJSはVDOMを完全に無視し、コードを直接的かつ精密なDOM更新へとコンパイルする手法を選択しています。

SolidJSの哲学:一度だけ実行し、永遠に更新する

ReactからSolidJSに乗り換えて最初に気づくのは、コンポーネントの振る舞いの違いです。Reactでは、コンポーネントは繰り返し実行される関数です。対照的に、SolidJSのコンポーネントは、一度だけ実行されるセットアップスクリプトです。リアクティブなグラフを初期化した後は、実質的に消滅します。状態が変化したとき、その状態に紐付いた特定の式だけが再評価されます。

このアプローチは単なるギミックではありません。本番環境で大きな成果をもたらします。ブラウザに重い差分検知エンジンを送る必要がなくなったため、メインのバンドルサイズは約30%削減されました。処理すべきVDOMが存在しないため、モバイルデバイスでのTime-to-Interactive(TTI)指標は400ms以上改善しました。SolidJSはJSXを効率的なDOM命令にコンパイルし、モダンなフレームワークの構文でありながらバニラJavaScriptのような速度を提供します。

Signals APIを理解する

SignalsはSolidJSの核となる仕組みです。createSignalはReactのuseStateに似て見えますが、その下のロジックは異なります。Signalは自身のサブスクライバー(購読者)を追跡する「監視可能な値」です。単なる値を得るのではなく、ゲッターとセッターを取得します。

import { createSignal } from "solid-js";

const [count, setCount] = createSignal(0);

// 関数として呼び出すことで値にアクセスします
console.log(count()); 

count()を呼び出すことで、Solidはその値がUIのどこに存在するかを正確に把握します。setCountを呼び出しても、フレームワークはコンポーネント全体を再実行しません。その Signal にリンクされた HTML 内の特定のテキストノードのみを更新します。この細粒度のリアクティビティ(fine-grained reactivity)こそが、SolidJSが速度とメモリ効率のベンチマークで常にトップを走り続ける理由です。

派生ステートとエフェクト

Solidは、重い計算用にcreateMemoを、副作用用にcreateEffectを提供しています。Reactで見られるような「依存配列」の悪夢を忘れることができます。SolidJSは実行中にどのSignalが使用されたかを自動的に追跡します。そして、それらの特定のSignalが変更されたときにのみ、エフェクトを再実行します。

import { createSignal, createMemo, createEffect } from "solid-js";

const Counter = () => {
  const [count, setCount] = createSignal(1);
  
  // Solidは自動的に count() の依存関係を「検知」します
  const doubleCount = createMemo(() => count() * 2);

  createEffect(() => {
    console.log("現在のカウント:", count());
  });

  return (
    <button onClick={() => setCount(count() + 1)}>
      カウント: {count()} | 2倍: {doubleCount()}
    </button>
  );
};

実践:リアクティブなリストの構築

パフォーマンスの差を実感するには、フレームワークがリストをどのように処理するかを見てください。VDOMフレームワークでは、1,000個のアイテムがあるリストの1つを更新する際、しばしば1,000個すべてのアイテムをチェックする必要があります(これはReact仮想化などの手法が推奨される大きな理由です)。Solidは<For>コンポーネントを使用します。これは、不要な比較を行わずにDOMノードを個別に処理するために専用設計されたものです。

プロジェクトのセットアップ

Viteテンプレートを使えば、数秒でプロジェクトを開始できます。ターミナルを開いて以下を実行してください。

npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev

効率的なデータテーブルの実装

行が毎秒更新されるリアルタイムの価格トラッカーを想像してみてください。行全体をちらつかせることなく、価格のセルだけを更新したいはずです。以下は、私たちの本番環境の実装を簡略化したものです:

import { createSignal, For, onCleanup } from "solid-js";

const PriceTracker = () => {
  const [stocks, setStocks] = createSignal([
    { id: 1, name: "AAPL", price: 150 },
    { id: 2, name: "TSLA", price: 700 },
    { id: 3, name: "GOOGL", price: 2800 },
  ]);

  // 1000ミリ秒ごとに価格を更新
  const interval = setInterval(() => {
    setStocks(prev => prev.map(s => ({
      ...s, 
      price: s.price + (Math.random() - 0.5) * 10
    })));
  }, 1000);

  onCleanup(() => clearInterval(interval));

  return (
    <div>
      <h2>ライブ株価</h2>
      <ul>
        <For each={stocks()}>
          {(stock) => (
            <li>
              {stock.name}: <strong>${stock.price.toFixed(2)}</strong>
            </li>
          )}
        </For>
      </ul>
    </div>
  );
};

export default PriceTracker;

<For>コンポーネントは、リストの順序が変わった場合に、SolidがDOMノードを再作成するのではなく移動させることを保証します。価格だけが変わった場合は、その特定の<strong>タグのみが更新されます。リストの残りの部分は完全に静的なままです。

なぜ次のプロジェクトにSolidJSが最適なのか

6ヶ月間本番運用して得られた最大の収穫は、単なる速度ではなく「思考の明快さ」です。古いクロージャ(stale closures)や複雑なフックのルール、あるいは堅牢なReactロジックを構築する際の苦労を心配する必要はもうありません。D3やLeafletのようなネイティブライブラリを使いたい場合も、フレームワークに邪魔されることなく、本物のDOMノードに直接アクセスできます。

エコシステムも整っています。SolidStartは強力なSSR機能を提供し、Solid Routerはナビゲーションをスムーズに処理します。データ量の多いアプリ、リアルタイムエディタ、複雑なダッシュボードを構築しているなら、仮想DOMは支払う必要のない「パフォーマンスの負債」です。

JSXの構文が馴染み深いため、Reactからの移行は簡単です。しかし、その根底にあるロジックは、ブラウザの実際の仕組みにはるかに近いです。クリーンで宣言的なバニラJavaScriptを書いているような感覚になります。

結論

SolidJSは、より効率的なWebへの転換を象徴しています。重い処理をコンパイル段階に移し、精密な更新のためにSignalsを使用することで、VDOMフレームワークでは到達できないパフォーマンスを提供します。私の経験上、これは単なるわずかな向上ではありません。より機敏なインターフェースと、予測可能な開発体験をもたらします。JavaScript疲れを解消し、ユーザーが受けるべきパフォーマンスを手に入れるためにフレームワークと戦うことに疲れたなら、今こそSolidJSを試すべき時です。

Share: