ブラウザ
 Computer >> コンピューター >  >> ソフトウェア >> ブラウザ

Reactのキャッシュ・タイムスライシング・同期APIフェッチの仕組みを徹底解説

著者:Marvin Frachet

今年はまさに「Reactの年」と言えるかもしれません。16.7-alpha.0とともに登場した注目の新機能「Hooks」の話は、すでに耳にしたことがあるでしょう。それ以外にも、Time Slicing(タイムスライシング)やSuspenseといった魅力的な機能についても聞いたことがあるはずです。

この記事の目的は、これらの新機能の使い方を解説することではなく、それらがどのように実装されているのかを探ることにあります。「自分が触れている技術の中身を理解したい」——その純粋な動機から書かれたものです。

また、この記事は筆者が実際に機能を発見していった順序で構成しています。必ずしも開発チームの設計思想そのものではないかもしれませんが、筆者が理解に至った道筋としてお読みください。

この記事で学べる内容は以下の通りです。

  • 非同期JavaScriptとイベントループ
  • Reactにおける代数的効果(Algebraic Effects)とその実例
  • FiberとReactのフェーズ

なぜこの記事を書いたのか?

筆者が執筆を決意したきっかけは、同期APIを使って非同期処理を行えるという実験的な特殊機能でした。

const bulbasaur = ApiResource.read()……え?同期だって!?

react-cacheライブラリは、非同期処理を同期APIで扱うことを可能にします。まさにこの機能こそが、「Reactは内部でどう動いているのか」を学びたいと思わせた原動力です。Dan Abramov氏とAndrew Clark氏によるこのライブラリのプレゼンテーションをご覧になった方もいるでしょう。

そもそも、どうやって同期呼び出しでリモートデータを取得できるのでしょうか?常識的に考えれば不可能に思えます。

この例を深く掘り下げ、react-cacheがどのような仕組みでこの機能を実現しているのかを解明していきましょう。物語はFiberアーキテクチャから始まります。

JavaScriptの処理を制御する

Fiberアーキテクチャにより、Reactはタスクの実行を制御できるようになりました。これは、Reactが抱えていた複数の問題を解決するために作られたものです。特に筆者の注目を集めたのは次の2点です。

  • ユーザー入力のような特定のイベントを、データフェッチよりも優先させること
  • Reactの計算処理を非同期的に分割し、メインスレッドの可用性を保ち、長時間のレンダリング処理でブロックされるのを防ぐこと

JavaScriptアプリケーション内で状態変化を引き起こすものはすべて——Reactに限らず——非同期処理によるものです。setTimeoutfetch、イベントリスナーなどがその代表例です。

非同期処理は、JavaScriptのコア概念によって管理されています。

  • タスク(micro task、macro task、render taskなど)
  • イベントループ
  • コールスタック

これらの概念に馴染みがない方は、Jake Archibald氏の動画を見ることをおすすめします。

Fiberのおかげで、ユーザー入力はfetchなどの他の非同期処理より先に解決されます。

しかし、これは一体どうやって実現されているのでしょうか?

先ほどのArchibald氏の講演は、筆者がイベントループの仕組みを学ぶ最初の敷石となりました。彼によれば、Promise APIなどで生成されるマイクロタスクは、次のマクロタスク(setTimeoutのようなコールバックベースのメソッド)が実行される前に処理され、フラッシュされます。

では、「ユーザー入力 vs データフェッチ」の比較に戻ると、チームはどうやってonChangeの解決をfetchの解決より先に行ったのでしょうか?

実は、これらの概念はWhatWG / HTML5 / Ecma-262という同一の仕様には収まっておらず、ブラウザやJSエンジンなど異なる場所から提供されています。

つまり、setTimeoutの後にPromiseを解決するには、どうすればいいのでしょうか?

筆者にとってこれは完全に狂った話に思え、その仕組みを想像するのは本当に難しいことでした。事実、この制御はもっと上位のレイヤーで行われているのです。

その後、筆者はReact RallyでのBrandon Dail氏の素晴らしい講演を目にしました。そこでは、React Fiberアーキテクチャによって実現されたTime SlicingとSuspenseの新機能が紹介されていました。

Dail氏によれば、fiberとは通常のJavaScriptのコールスタックのようなもので、スタック内の各項目がfiberと呼ばれます。ただし、従来のコールスタックが関数(+メタデータ)を表すフレームに依存しているのに対し、fiberはコンポーネント(+メタデータ)を表します。fiberを「コンポーネントに関するあらゆる情報を知っている巨大な箱」と考えてみてください。

この2つの概念には重要な違いがあります。

まず、コールスタックはJavaScriptコードを駆動するネイティブ部分の上に構築された機能です。すべてのJavaScript関数呼び出しをスタックし、それぞれを実行します。関数を呼び出すたびにスタックに追加されます。コールスタックがなければ、明確で詳細なエラースタックトレースを得ることはできません。そして、コールスタックはJavaScriptコードからアクセスできないため、それを制御することは非常に困難であり、事実上不可能です。

一方、fiber(fiberのスタック)は同じ概念を表しながらも、JavaScriptコード内に構築されています。最小単位は関数ではなくコンポーネントです。つまり、fiberはJavaScriptの世界の中で実際に動作しているのです。

Fiberアーキテクチャが完全にJavaScriptで構築されているということは、私たちがそれを使用し、アクセスし、変更できることを意味します。標準的なJavaScriptを使って作業できるのです。

筆者が誤解していたのは、「ReactはJavaScriptの内部動作を回避するためのワークアラウンドを使っている」と考えていた点です。それは違います。fiberは単に、Reactコンポーネントに関する情報を持ち、そのライフサイクルと相互作用できるJavaScriptオブジェクトなのです。作用できるのはReactの内部機能のみです。

狙いは、「fetchのマイクロタスク解決をコールバックタスクより先に実行せよ」といったJavaScriptの動作自体を再定義することではありません。むしろ、特定のコンテキストにおいて、どのReactメソッドを呼び出すべきか/呼び出さないべきかを判断すること、たとえばライフサイクルメソッドの呼び出しを中断することにあります。

ちょっと待ってください!fiberはReactアプリ内のあらゆるものを制御できると言いましたよね?でも、コンポーネントはどうやってReactに「何もしないで」と伝えるのでしょうか?

代数的効果(Algebraic Effects)、でもJavaScriptで

ReactはFiberアーキテクチャのおかげでコンポーネントを制御し、その実行状態を把握できます。次に必要なのは、特定のコンポーネントに何か変化が起きたことをReactに伝え、その変化を処理してもらう方法です。

ここで代数的効果(Algebraic Effects)が登場します。

代数的効果はJavaScriptに存在しない概念です。ここでは、もう少し抽象度の高い説明を試みます。

代数的効果とは、ある場所へ情報を送信するための概念で、ディスパッチャーのようなものです。特定の関数を呼び出すと、現在実行中の関数が正確な位置で中断され、親の関数が計算を引き受けます。親の計算が完了すると、プログラムは情報が送信された元の位置から再開されます。

OCamlやEffなどの一部の言語は、この機能をネイティブに備えています。実装の詳細がすべて親側に委ねられるという、非常に興味深い抽象化です。

こんな機能がJavaScriptにあったら素晴らしいと思いませんか?

Reactチームは、Reactの文脈において、JavaScriptのtry/catchブロックを使って似たようなアプローチを作り出しました。Dail氏によれば、これはJavaScriptで利用可能な最も近い概念だそうです。

何かをthrowすることで、どこかの親へ情報を送れます。その情報を最初にcatchした親が、それを処理し、計算を行うことができます。

百聞は一見に如かず――実例で見る

同期APIを使ってフシギダネ(Bulbasaur)をフェッチしようとする、以下のコードを想像してください。

同期APIでデータをフェッチするのは一般的ではないため、奇妙に見えるかもしれません。customFetch関数の実装を覗いてみましょう。

おっと待ってください!これは全然fetchに見えません!この関数が何をしようとしているのか全く分かりません……

では、コンポーネントの周りにあるもの、たとえば次のようなfiberを想像してみてください。

コードをじっくり読んでみてください。

次に、customFetchの実装を見てみましょう。

先ほどのスニペットで重要なのはtry/catchブロックです。

これらのコード片を通じて何が起きているのか、まとめてみましょう。

  • PokemonコンポーネントがcustomFetchメソッドを呼び出します。
  • customFetchメソッドは内部キャッシュを読み込もうとしますが、空なので、何かをどこかにthrowします——これが代数的効果です。
  • 親であるfiberがその情報をcatchし、処理して、データをフェッチします。そしてcustomFetchのキャッシュにデータを格納します。
  • Component(args)で再レンダリングが発生すると、今度はcustomFetchのキャッシュが満たされています。データは同期APIを通じてコンポーネント内で利用可能になります。

react-cacheの実装詳細を見て、さまざまなthrowを確認してみてください。

この過程で気づいたことがあるかもしれません:renderが2回呼ばれています。1回目はエラーをthrowするため——コンポーネントを一時停止——、2回目はデータを取得するため——コンポーネントを再開——です。Reactでは複数回のrender呼び出しが問題になりません。なぜならrenderは純粋な関数であり、それ自体には副作用がないからです。

待って……え?renderに副作用がない?DOMはどうなるんですか?

Reactのフェーズ

Reactを長く使ってきた方なら、「再レンダリングを何度も行うのは良くない」という話を聞いたことがあるでしょう。Fiberアーキテクチャ以前は、render関数を呼び出すたびに、Reactは内部計算を行い、その結果に応じてDOMを変更していました。たとえばsetState経由でrender関数を呼び出した場合、処理はインラインで実行されていました。

setStaterender → 仮想ノードの比較 → DOMノードの更新

Fiberでは、このプロセスが少し変わりました。キューとバッチの概念が導入され、高パフォーマンスなDOM変更が可能になりました。

アイデアはシンプルです。画面は毎秒約60フレームで動作すると仮定します。この前提をもとに、利用可能なJavaScriptの関数を使えば、約16.7ミリ秒ごとにのみ計算とDOM変更を行うことができます。Fiberにより、Reactは複数の変更をキューに入れ、毎秒約60回コミットできるのです。

この種の変更により、Reactは3つのフェーズに分割され、それぞれに利点と特性を持つようになりました。

Reactのキャッシュ・タイムスライシング・同期APIフェッチの仕組みを徹底解説

※画像:Dan Abramov氏によるReactフェーズに関するツイート(Twitter/@dan_abramov)

  • Renderフェーズ:純粋かつ決定的です。副作用がなく、構成する各関数は複数回呼び出されても問題ありません。Renderフェーズは中断可能です——一時停止モードになるのはrender関数ではなく、フェーズ全体です。
  • Pre-commitフェーズ:スクロールバーの位置など、実際のDOMの状態への読み取り専用アクセスを提供することを目的としています。
  • Commitフェーズ:実際にDOMを変更し、中断できません。Reactはこのフェーズ中に一時停止することができません。

この3つのフェーズのセットが、Time Slicingの能力をもたらしました。ReactはRenderフェーズ中、2つのコンポーネント関数呼び出しの間で一時停止し、必要に応じてそのフェーズを再開できます。

Fiberにおいて、renderはコンポーネントの内部状態に基づいて最新の利用可能な表現を取得し、比較を行い、ReactがDOMを変更すべきかどうかを判断することだけを目的としています。コミットによる変更が必要な場合は、その変更を「work in progress」キューに追加します。

Reactチームは、React Concurrent(Time Slicing + Suspense)とFiberアーキテクチャのおかげで、大幅なパフォーマンス向上を達成しました。イベントの優先順位付けや並行性といった、ブラウザのさまざまな問題に対抗するワークアラウンドを作り上げたのです。

一歩引いて考えてみれば、彼らが示したのはまさにこれだったのではないでしょうか。優先順位付け(prioritisation)こそが、ブラウザとフロントエンドフレームワークの新しい課題となっているようです。

他のチームも、現状の最先端技術の改善に取り組んでおり、将来のAPIまで提案しています。以下はGoogleによる取り組みです。


無料でプログラミングを学ぼう。freeCodeCampのオープンソースカリキュラムは、40,000人以上の人々を開発者としての仕事へと導いてきました。今すぐ始めましょう。

  1. Chromeでパスワードをインポート・エクスポートする方法【実験的機能の活用術】

    実は、ブラウザ自体がパスワード管理ツールとしての役割を果たしています。Chromeのパスワード管理機能も決して悪くはありませんが、さらに改善できる余地があります。 Chromeでは、保存したパスワードを手間なく表示・管理できますが、セキュリティ面ではいくつか不安な点も残ります。そのため、最近セキュリティ意識が高まった方の中には、保存済みのパスワードをサードパーティ製のパスワードマネージャーへ移行したいと考えている人も多いでしょう。あるいは、別のブラウザへの乗り換えを検討している場合もあるかもしれません。 ご安心ください。Chromeには「実験的機能」として隠されている設定があり、これを有効にす

  2. Vivaldiブラウザでプライバシーを守る方法:トラッキング対策のノウハウ(2022年版)

    ウェブブラウザには実に多くの選択肢がありますが、多くの人は「使いやすくて安全」という理由でGoogle Chromeを使い続けがちです。しかし、注目を集めつつある新鋭「Vivaldi」を使い始めると、その常識は少しずつ変わっていきます。 Vivaldiとは? Vivaldiは、Opera Softwareの創設メンバーによって設立されたVivaldi Technologies社が開発した、無料で使えるクロスプラットフォーム対応ブラウザです。 このブラウザ最大の特徴は、すべての設定をユーザー自身がコントロールできること。細部まで自由にカスタマイズしながら、高いセキュリティを維持できます。 近年