Signalsの紹介
Signalsは、アプリの複雑さに関係なく高速さを維持する状態表現の方法です。Signalsはリアクティブな原則に基づいており、優れた開発者エクスペリエンスを提供し、仮想DOM向けに最適化された独自のインプリメンテーションを備えています。
本質的に、Signalは` .value`プロパティに値を保持するオブジェクトです。コンポーネント内でSignalの`value`プロパティにアクセスすると、そのSignalの値が変更されたときに、そのコンポーネントが自動的に更新されます。
シンプルで書きやすいだけでなく、アプリのコンポーネント数に関係なく、状態の更新が高速に維持されることを保証します。Signalsはデフォルトで高速であり、バックグラウンドで更新を自動的に最適化します。
import { signal, computed } from "@preact/signals";
const count = signal(0);
const double = computed(() => count.value * 2);
function Counter() {
return (
<button onClick={() => count.value++}>
{count} x 2 = {double}
</button>
);
}
REPLで実行Signalsは、フックとは異なり、コンポーネントの内外で使用できます。Signalsはフックとクラスコンポーネントの両方と連携して動作するため、自分のペースで導入し、既存の知識を活用できます。いくつかのコンポーネントで試してみて、徐々に導入していきましょう。
そして、可能な限り最小限のライブラリを提供するという私たちの理念を貫いています。PreactでSignalsを使用しても、バンドルサイズにわずか **1.6kB** しか追加されません。
すぐに始めたい場合は、ドキュメントを参照して、Signalsについて詳しく学習してください。
Signalsによって解決される問題
過去数年間にわたって、小規模なスタートアップから、数百人の開発者が同時にコミットする巨大なモノリスまで、幅広いアプリとチームに取り組んできました。その間、コアチームの全員が、アプリケーションの状態管理方法に関する繰り返し発生する問題に気づきました。
これらの問題に対処するために機能する素晴らしいソリューションが作成されてきましたが、最良のソリューションでも、フレームワークへの手動統合が必要です。その結果、開発者はこれらのソリューションを採用することにためらいを感じ、代わりにフレームワークが提供する状態プリミティブを使用して構築することを好んでいました。
私たちは、最適なパフォーマンスと開発者エクスペリエンスをシームレスなフレームワーク統合と組み合わせた魅力的なソリューションとしてSignalsを構築しました。
グローバル状態の課題
アプリケーションの状態は、通常、シンプルで少量から始まり、いくつかの単純な`useState`フックがあるかもしれません。アプリが成長し、より多くのコンポーネントが同じ状態にアクセスする必要があるようになると、その状態は最終的に共通の祖先コンポーネントまで持ち上げられます。このパターンは何度も繰り返され、最終的に状態の大部分がコンポーネントツリーのルート付近に存在することになります。
このシナリオは、状態の無効化によって影響を受けるツリー全体を更新する必要がある従来の仮想DOMベースのフレームワークにとって課題となります。本質的に、レンダリングのパフォーマンスは、そのツリー内のコンポーネント数に依存します。`memo`または`useMemo`を使用してコンポーネントツリーの一部をメモ化することで、フレームワークが同じオブジェクトを受け取るようにすることができます。何も変更されていない場合、これによりフレームワークはツリーの一部のレンダリングをスキップできます。
理論上は妥当に聞こえますが、現実ははるかに複雑です。実際には、コードベースが成長するにつれて、これらの最適化をどこに配置するべきかを判断することが困難になります。頻繁に、意図的に行われたメモ化でさえ、不安定な依存値によって無効になります。フックには分析できる明示的な依存関係ツリーがないため、ツールは開発者が依存関係が不安定である **理由** を診断するのに役立ちません。
コンテキストの混乱
チームが状態共有のために使用するもう1つの一般的な回避策は、状態をコンテキストに配置することです。これにより、コンテキストプロバイダーとコンシューマー間のコンポーネントのレンダリングをスキップすることで、レンダリングを短絡させることができます。ただし、注意点があります。コンテキストプロバイダーに渡される値のみを、全体として更新できます。コンテキスト経由で公開されているオブジェクトのプロパティを更新しても、そのコンテキストのコンシューマーは更新されません。粒度の細かい更新はできません。これに対処するための利用可能なオプションは、状態を複数のコンテキストに分割するか、プロパティのいずれかが変更されたときにコンテキストオブジェクトを複製して無効にすることです。
最初に値をコンテキストに移動することは有益なトレードオフのように見えますが、値を共有するだけでコンポーネントツリーのサイズが増加するという欠点は、最終的に問題になります。ビジネスロジックは必然的に複数のコンテキスト値に依存するようになり、ツリー内の特定の場所に実装することを強制する可能性があります。ツリーの途中でコンテキストを購読するコンポーネントを追加することはコストがかかります。コンテキストの更新時にスキップできるコンポーネントの数が減少するためです。さらに、サブスクライバーの下にあるコンポーネントはすべて再レンダリングする必要があります。この問題に対する唯一の解決策は、メモ化を多用することですが、これにより、メモ化に固有の問題に戻ってしまいます。
より良い状態管理方法の模索
次世代の状態プリミティブを模索するために、私たちは設計図に戻りました。現在のソリューションの問題を同時に解決するものを作りたかったのです。手動によるフレームワーク統合、メモ化への過剰な依存、コンテキストの最適ではない使用、およびプログラムによる可観測性の欠如は、逆行しているように感じました。
開発者はこれらの戦略を使用してパフォーマンスを「オプトイン」する必要があります。それを逆転させ、**デフォルトで高速** なシステムを提供し、最良のパフォーマンスをオプトアウトするために努力する必要があるようにしたらどうでしょうか?
これらの質問に対する私たちの答えがSignalsです。アプリ全体でメモ化やトリックを必要とせずに、デフォルトで高速なシステムです。Signalsは、状態がグローバルであるか、プロパティまたはコンテキストを介して渡されるか、コンポーネントにローカルであるかに関係なく、細かい状態更新の利点を提供します。
未来へのSignals
Signalsの背後にある主なアイデアは、値をコンポーネントツリーを介して直接渡すのではなく、値を含むSignalオブジェクト(`ref`に似ています)を渡すことです。Signalの値が変更されると、Signal自体は変わりません。その結果、コンポーネントはSignalを見てその値を見ないため、Signalは通過したコンポーネントを再レンダリングせずに更新できます。これにより、コンポーネントをレンダリングするという高価な作業をすべてスキップし、Signalの値に実際にアクセスするツリー内の特定のコンポーネントにすぐにジャンプできます。
アプリケーションの状態グラフは、一般的にコンポーネントツリーよりもはるかに浅いという事実を利用しています。これにより、コンポーネントツリーと比較して状態グラフを更新するために必要な作業がはるかに少なくなると、レンダリングが高速化されます。この違いは、ブラウザで測定した場合に最も顕著です。以下のスクリーンショットは、同じアプリを2回測定したDevToolsプロファイラートレースを示しています。1回目はフックを状態プリミティブとして使用し、2回目はSignalsを使用しています。
Signalsバージョンは、従来の仮想DOMベースのフレームワークの更新メカニズムをはるかに上回っています。テストした一部のアプリでは、Signalsは非常に高速であるため、フレイトグラフで見つけることが非常に困難になります。
Signalsはパフォーマンスのピッチを反転させます。メモ化やセレクターを介してパフォーマンスをオプトインする代わりに、Signalsはデフォルトで高速です。Signalsを使用すると、パフォーマンスはオプトアウト(Signalsを使用しない)になります。
このレベルのパフォーマンスを実現するために、Signalsはこれらの重要な原則に基づいて構築されました。
- **デフォルトで遅延:** 現在どこで使用されているSignalsのみが監視および更新されます。接続されていないSignalsはパフォーマンスに影響しません。
- **最適な更新:** Signalの値が変更されていない場合、そのSignalの値を使用するコンポーネントと効果は、Signalの依存関係が変更されていても更新されません。
- **最適な依存関係追跡:** フレームワークは、すべてが依存するSignalsを自動的に追跡します。フックのような依存関係配列は必要ありません。
- **直接アクセス:** コンポーネントでSignalの値にアクセスすると、セレクターやフックを使用する必要がなく、自動的に更新を購読します。
これらの原則により、Signalsは、ユーザーインターフェースのレンダリングとは関係のないシナリオも含め、幅広いユースケースに適しています。
PreactへのSignalsの導入
適切な状態プリミティブを特定した後、それをPreactに接続しました。フックについて常に気に入っていた点は、コンポーネント内で直接使用できることです。これは、通常、「セレクター」関数に依存するか、状態更新を購読するために特別な関数でコンポーネントをラップするサードパーティの状態管理ソリューションと比較して、人間工学的な利点です。
// Selector based subscription :(
function Counter() {
const value = useSelector(state => state.count);
// ...
}
// Wrapper function based subscription :(
const counterState = new Counter();
const Counter = observe(props => {
const value = counterState.count;
// ...
});
どちらのアプローチも満足のいくものではありませんでした。セレクターアプローチでは、すべての状態アクセスをセレクターでラップする必要があり、複雑またはネストされた状態では面倒になります。コンポーネントを関数でラップするアプローチでは、コンポーネントを手動でラップする必要があるため、コンポーネント名や静的プロパティの欠落など、多くの問題が発生します。
過去数年、多くの開発者と緊密に協力する機会がありました。特にPreactを初めて使用する開発者にとってよくある課題は、セレクターやラッパーといった概念が、各ステート管理ソリューションで生産性を発揮する前に学習する必要がある追加のパラダイムであることです。
理想的には、セレクターやラッパー関数について知る必要はなく、コンポーネント内で直接ステートにアクセスできれば良いでしょう。
// Imagine this is some global state and the whole app needs access to:
let count = 0;
function Counter() {
return (
<button onClick={() => count++}>
value: {count}
</button>
);
}
コードは明確で、何が起こっているのか理解しやすいですが、残念ながら動作しません。ボタンをクリックしてもコンポーネントは更新されません。なぜなら、count
が変更されたことを知る方法がないからです。
しかし、このシナリオを頭から離すことができませんでした。この明確なモデルを現実にするにはどうすればよいでしょうか?Preactのプラグ可能なレンダラーを使用して、さまざまなアイデアと実装のプロトタイプを作成し始めました。時間がかかりましたが、最終的に実現する方法を見つけ出しました。
// Imagine this is some global state that the whole app needs access to:
const count = signal(0);
function Counter() {
return (
<button onClick={() => count.value++}>
Value: {count.value}
</button>
);
}
REPLで実行セレクターもラッパー関数も何もありません。シグナルの値にアクセスするだけで、コンポーネントはそのシグナルの値が変更されたことを認識し、更新する必要があります。いくつかのアプリでプロトタイプをテストした後、私たちは何かを発見したことは明らかでした。このようにコードを書くことは直感的で、最適に動作させるために特別な工夫は必要ありませんでした。
もっと高速化できますか?
ここでシグナルのリリースを止めても良かったのですが、これはPreactチームです。Preactとの統合をどこまで推し進められるかを確認する必要がありました。上記のカウンターの例では、count
の値はテキストの表示にのみ使用され、コンポーネント全体を再レンダリングする必要はありません。シグナルの値が変更されたときにコンポーネントを自動的に再レンダリングする代わりに、テキストのみを再レンダリングしたらどうでしょうか?さらに、仮想DOMを完全にバイパスして、テキストをDOMに直接更新したらどうでしょうか?
const count = signal(0);
// Instead of this:
<p>Value: {count.value}</p>
// … we can pass the signal directly into JSX:
<p>Value: {count}</p>
// … or even passing them as DOM properties:
<input value={count} onInput={...} />
はい、それも行いました。通常文字列を使用する場所に、JSXにシグナルを直接渡すことができます。シグナルの値はテキストとしてレンダリングされ、シグナルが変更されると自動的に更新されます。これはプロップにも有効です。
次のステップ
興味があり、すぐに始めたい場合は、シグナルに関するドキュメントをご覧ください。どのように使用するかについてのご意見をお待ちしております。
シグナルに切り替える必要はないことを覚えておいてください。フックは引き続きサポートされ、シグナルとも連携して動作します!いくつかのコンポーネントから始めて概念に慣れることから始め、徐々にシグナルを試してみることをお勧めします。