🌃

Reactのトランジションの仕組みを理解する

2024/12/13に公開

はじめに

こんにちは。株式会社AI Shiftの安井です。
本記事はAI Shift Advent Calendar2024の13日目の記事となります。

今回はReactのトランジションがどのような仕組みで実装されているのかを深掘りし、その過程でReactがisInputPendingというAPIに期待したことを考察していけたらと思います。

構成は以下のとおりです。

  1. startTransitionのおさらい
  2. トランジション時のレンダリング処理の違い
  3. トランジション時の処理を深ぼる(図解)
  4. isInputPendingで解決しようとしたこと

前提

今回はReact18を前提にして解説をします。そのためRaect19からstartTransition関数に非同期関数も受け付けるようになりますが、本記事ではstartTransitionに渡す関数は同期的であるとします。

startTransitionのおさらい

まずはstartTransitionのおさらいです。react.devをもとに整理しましょう。

startTransitionは、UIをブロックせずにstateを更新するためのReactフックです。

https://ja.react.dev/reference/react/startTransition

Exampleで比較する

公式のExampleをそのまま掲載しました。このPostsのタブでは重たい計算処理がされており、遷移するまでに数秒の時間かかります。

トランジションなしの場合

トランジションとしてマークされていない場合、「About」->「Posts」->「Contact」と素早くクリックした際に、「Posts」へ遷移が完了されるまでUIがブロックされ、「Contact」のタブをクリックしても何も反応しない時間が発生してしまいます。これではユーザの体験を大幅に損ねてしまいます。

トランジションありの場合

ではstateの更新をトランジションとしてマークした場合はどうでしょうか?

function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}

先ほどのように「About」->「Posts」->「Contact」と素早くクリックした場合でも、UIがブロックされずにタブの遷移が実現できています。このようにReactのトランジションを使うとUIをブロックせずにstateを更新することができます。

トランジション時のレンダリング処理の違い

ではstateの更新をトランジションとしてマークしたケースとそうでない場合ではどのようなレンダリングにおける違いがあるのでしょうか?

Performanceタブから確認する

ここではChrome Dev ToolsのPerformanceタブからどのような処理が行われているかを確認します。

トランジションとしてマークされていない場合

こちらはトランジションとしてマークされていない場合です。ユーザのクリックイベントを起因に、「performSyncWorkOnRoot」->「renderRootSync」->「workLoopSync」と順に関数が実行されていることがわかります。

Reactのレンダリングの仕組みについて

performSyncWorkOnRootやrenderRootSyncといった処理はReactが内部でFiberTreeを構築するためのものであり、以前Reactの初回マウントでどのような処理がされているかを解説した記事があるので気になる方はぜひご覧ください。

https://zenn.dev/aishift/articles/d046335a98bc34

トランジションとしてマークした場合

では比較してトランジションとしてマークした場合はどうなるでしょうか?

先ほどとは違いユーザのクリックイベントを起因に、「PerformConcurrentWorkOnRoot」->「renderRootConcurrent」と実行されています。そして、実行されるタスクが細切れに分割されていることがわかります。

この違いがReactのトランジションによるUIをブロッキングしないための対策です。

双方の違いを図式化する

それぞれを図式化するとこのような形になります。

トランジションとしてマークされていない場合

トランジションとしてマークされていない場合は一連のレンダリングのタスクを同期的に実行します。

また、ユーザからのClickイベントは優先度の高いイベントとして処理されます。これはReactのLaneという仕組みによって管理されています。

トランジションとしてマークした場合

対してトランジションとしてマークした場合は次の図のようにタスクが細切れになりUIのブロッキングを防いでいます。

先ほど紹介したようにユーザのClickイベントなどによって発火されたレンダリングは基本的に優先度の高いタスクとして扱われます。

しかし、トランジションとしてマークするとレンダリングのタスクが発火する際に優先度の低いタスクとして開始することによりタスクを細切れにして割り込みを可能にしています。

https://github.com/facebook/react/blob/7670501b0dc1a97983058b5217a205b62e2094a1/packages/react-reconciler/src/ReactFiberLane.js#L57-L73

トランジション時の処理を深ぼる(図解)

では具体的にトランジション時にはどのような処理が行われているのでしょうか?これはReact Schedulerの仕組みによって成り立っています。

詳細な内容はこちらの記事が大変参考になりました。

https://jser.dev/react/2022/03/16/how-react-scheduler-works

またReact Schedulerのソースコードは非常にシンプルな構成になっています。

https://github.com/facebook/react/tree/18e220b618060bd05bf7d1079dbe6021d737749d/packages/scheduler

レンダリングのタスクを発行する

Reactのレンダリングのタスクは基本的にMacroTaskに詰められてから実行されます。MacroTaskに登録する際はsetImmediateなどのAPIを使用してすぐにTaskQueueから取り出すような実装になっています。


https://github.com/facebook/react/blob/18e220b618060bd05bf7d1079dbe6021d737749d/packages/scheduler/src/forks/Scheduler.js#L517-L547

この図は同期的なレンダリングのケースであり、一度TaskQueueから取り出されたレンダリングタスクはその処理が完了するまで中断されずに実行されます。

しかし、この同期的なレンダリング処理が原因で重たいstate更新の際にUIがブロッキングされてしまう課題がありました。

トランジションがマークされるとタスクの中断・再開が可能

そこでstateの更新をトランジションとしてマークすると下の図のように一定の間隔でタスクを中断し、中断した時点から再度MacroTaskに登録してタスクを再開します。この作業を繰り返すことによってReactはUIをブロッキングせずにレンダリングを行っています。

実際のコードはこちらです。このshouldYieldというFlagがtrueにならない限りはレンダリングを継続します。そして、一定の間隔でshouldYieldがtrueになることでレンダリングを一時中断しタスクを細かく分割しています。

https://github.com/facebook/react/blob/555ece0cd14779abd5a1fc50f71625f9ada42bef/packages/react-reconciler/src/ReactFiberWorkLoop.js#L2296-L2302

shouldYieldというFlagの管理は非常にシンプルで、タスクの実行時間がframeYieldMs(ここでは5ms)に達するとFlagがtrueになります。

https://github.com/facebook/react/blob/18e220b618060bd05bf7d1079dbe6021d737749d/packages/scheduler/src/forks/Scheduler.js#L458-L467

先ほどのPerformanceタブで確認した実行の処理を再度見てみましょう。すると確かに5msずつにタスクが分割されて実行されていることがわかります。

isInputPendingで解決しようとしたこと

これまででReactがトランジションを用いてどのようにUIをブロッキングせずにレンダリングを実行しているかを解説しました。

整理すると「トランジションとしてマークされたstateの更新は、一定の間隔(例:5ms)でレンダリングのタスクを中断し再開を繰り返すことでUIのブロッキングを防いでいた」という形になります。

しかし、このレンダリングの中断・再開というのは読み込みパフォーマンスとして決して最適なものではありませんでした。そこでMeta社が提案し期待されたAPIがisInputPendingです。

https://developer.chrome.com/docs/capabilities/web-apis/isinputpending?hl=ja

タスクを分割するトレードオフ

レンダリングのタスクを分割しブラウザへ制御を渡すことによってUIのブロッキングを防いでいましたが、当然それはレンダリングが完了するまでのパフォーマンスを低下させてしまいます。

*そのためReactは初回マウントなどの優先度の高いケースではタスクの分割をせずに同期的にレンダリングを実行するようにしています。


https://developer.chrome.com/docs/capabilities/web-apis/isinputpending?hl=ja

isInputPendingとは

isInputPending APIを使用すると、ユーザーがページの操作を試みたかどうかを確認できます。つまり、Reactがタスクを分割してブラウザへ制御を渡していたところが、ブラウザ側からユーザの操作が存在したかを伝えることができるのでより効率的に処理を中断できるようになります。

https://developer.mozilla.org/en-US/docs/Web/API/Scheduling/isInputPending


https://developer.chrome.com/docs/capabilities/web-apis/isinputpending?hl=ja

ReactはこのisInputPending APIを実験的に試してより効率的にReactのスケジュールができないか試みていました。

https://github.com/kassens/react/blob/0e29c9d30809ceff8f6a3686426aa695e238e78c/packages/scheduler/src/forks/Scheduler.js#L493-L516

isInputPendingを使用しない

しかし2024/2ごろのPRによってisInputPending APIを用いたSchedulerの改善は断念しているようです。PRのコメントにはisInputPending APIの処理を削除してよりSchedulerの実装をシンプルにするためと書かれています。

isInputPending is not in use. This PR cleans up the flags controlling its gating and parameters to simplify Scheduler.

https://github.com/facebook/react/commit/5ca65e143ae670132cc49de47e6c9cb909154d5b

別の記事でもisInputPending APIの問題点についていくつか言及されていました。

  • isInputPendingは、ユーザーが操作したにもかかわらず、falseを誤って返す場合があります。
  • タスクがyieldする必要があるのは入力の場合だけではありません。アニメーションやその他のユーザー インターフェースの定期的な更新は、レスポンシブなウェブページの提供と同様に重要です。
  • その後、scheduler.postTaskや scheduler.yieldなど、生成する懸念事項に対処する、より包括的な収益生成APIが導入されました。

https://web.dev/articles/optimize-long-tasks?hl=ja#isinputpending

まとめ

この記事ではReactのトランジションがどのように実装されているかを整理し、その過程でなぜisInputPending APIへの期待がされたかを解説しました。

Reactはトランジションがマークされたstate更新の場合は細かくレンダリングのタスクを分割しブラウザへ制御を渡すことによってUIのブロッキングを防いでいます。

しかし、タスクを分割して実行することはレンダリングが完了するまでのパフォーマンスとのトレードオフになります。そこで期待されたAPIがisInputPendingであり、ブラウザからユーザの操作が存在したかを知ることができるようになりました。

最終的にはisInputPending自体の不安定さなどから利用可能ではないという判断になったようですが、より効率的なレンダリングを実現するためにReactは改善を試みていました。

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion