Closed23

React18の並行レンダーとは

Yug (やぐ)Yug (やぐ)

へぇ、それは知らなかった

Reactはバージョン16のリリース以来ここ数年大きな機能追加を行いませんでした。一つ前のメジャーバージョンである17では「新機能なし」とされています。

Yug (やぐ)Yug (やぐ)

へぇ

これまでのReactにおいて、一度始まったレンダリングは必ず最後まで行われてから次のレンダリングに移行していました。一度始まったレンダリングを中断することはできず、またそのレンダリングが完了するまで別のレンダリングを始めることはできません。
React 18ではレンダリングの実行中に別のレンダリングを始めたり、レンダリングを途中で停止して破棄することができるようになりました。

でもそれって並行かどうかという話ではなく、単に処理を中断できるかどうかという話では?

Yug (やぐ)Yug (やぐ)

ただし、最終的なレンダリング結果が状態(state)に正しく対応していることは保証されています。

これはソースコード読んでて確かに見つけた。ここだと思う。
https://github.com/facebook/react/blob/2bd1c756c6fffefb00cdb2986218fa2701ece82e/packages/react-reconciler/src/ReactFiberConcurrentUpdates.js#L146-L149

雑に言うと「並行レンダリングが既にされておらず終了している場合はfinishする」という処理が書かれている

Yug (やぐ)Yug (やぐ)

ほー、この<Offscreen>ってやつ面白そうだな

もう 1 つの例は、state の再利用です。React の並行処理機能により、画面から UI の一部分をいったん削除し、前回の state を再利用しながら後で戻す、ということが可能です。例えば、ユーザがタブを切り替えて画面から離れて戻ってきた場合、React は以前の画面を以前と同様の state で復帰させる必要があります。将来のマイナーリリースにおいて、このパターンを実装した <Offscreen> というコンポーネントを新たに加える予定です。同様に、<Offscreen> を使ってバックグラウンドで新しい UI を用意し、ユーザが表示させようとする前に準備完了にしておく、ということもできるようになるでしょう。

Yug (やぐ)Yug (やぐ)

元々バッチングはされてたんだけど、イベントハンドラ内のsetStateのみが対象だったから、その対象を拡大したって話。新しくバッチングが導入された訳ではない

新機能:自動バッチング

バッチングとは React がパフォーマンスのために複数のステート更新をグループ化して、単一の再レンダーにまとめることを指します。自動バッチング以前は、React のイベントハンドラ内での更新のみバッチ処理されていました。promise や setTimeout、ネイティブのイベントハンドラやその他あらゆるイベント内で起きる更新はデフォルトではバッチ処理されていませんでした。自動バッチングにより、これらの更新も自動でバッチ処理されるようになります:

// Before: only React events were batched.
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will render twice, once for each state update (no batching)
}, 1000);

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);
Yug (やぐ)Yug (やぐ)

ん-、であればこの記事の

React 18では複数の状態の更新による再レンダリングがまとめて実行されるようになり、パフォーマンスが向上しています。

という文章は適切ではないな。

https://qiita.com/Yuki_Oshima/items/b6ec2fb9f5b5d53381ad#レンダリングのバッチ化

なので公式の方を読み進めていこう(内容もほぼ同じっぽいし)
https://ja.react.dev/blog/2022/03/29/react-v18

並行レンダリングについてだけではなく、それを含む「React18の新機能」をバーッと見ていく感じ

Yug (やぐ)Yug (やぐ)

うん、やはりtransitionは「優先度の低い処理を表現する」ものだな。

新機能:トランジション

startTransition でラップした更新は緊急性の低いものとして扱われ、クリックやキー押下のような緊急性の高い更新がやってきた場合には中断されます。トランジションがユーザによって中断された場合(例えば素早く複数のタイプが起こった場合)、React は完了しないままに古くなったレンダーを破棄して、最後の更新のみレンダーします。

import { startTransition } from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

この記事に書かれてる通り
https://qiita.com/tsubasa_k0814/items/cab39ad7f47cac6491cf

Yug (やぐ)Yug (やぐ)

???

また、コンテンツが再サスペンドした場合、バックグラウンドでトランジション中のコンテンツをレンダーしつつ、現在のコンテンツを表示し続けるよう React に伝えます

Yug (やぐ)Yug (やぐ)

これは知ってる

サスペンスの新機能

サスペンスにより、コンポーネントツリーの一部がまだ表示できない場合に、ロード中という状態を宣言的に記述できるようになります

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

???

React 18 におけるサスペンスはトランジション API と併用した場合に能力を発揮します。トランジション中でサスペンドが発生すると、React は既に見えているコンテンツがフォールバックによって隠されてしまわないようにするのです。代わりに、十分なデータがロードされるまでレンダーを遅らせて、望ましくないロード中状態が見えないようにします。

Yug (やぐ)Yug (やぐ)

ていうか「優先度が低いという意思表示をする」という意味で、SuspenseとTransitionって何が違うんだ?

なるほどなぁ

SuspenseはAPIでデータを取得した際に使うものである。 トランジションは内部的な処理(stateの変化)に対して行うものだったので、この点が異なる。

https://baapuro.com/React/seven/

Yug (やぐ)Yug (やぐ)

え!マウント時のあの2回実行されるやつって、1回アンマウントされてるから2回実行されるのか!

Strict モードの新たな挙動

この問題に気付きやすくするために、React 18 は strict モードに新しい開発時専用のチェックを導入します。この新しいチェックは、コンポーネントが初めてマウントされるたびに、すべてのコンポーネントを自動的にアンマウント・再マウントし、かつ 2 回目のマウントで以前の state を復元します。

確かにそうか

mount -> unmount -> mountしてるのか、なるほどなぁ

んで2回目のマウントではいちいち再計算するのではなくちゃんと復元してるのか。そこのソースコードはいつか確認したいところ

かつ 2 回目のマウントで以前の state を復元します

* React がコンポーネントをマウント
    * レイアウト副作用を作成
    * 副作用を作成
* マウントされたコンポーネント内で副作用の破棄をシミュレート
    * レイアウト副作用を破棄
    * 副作用を破棄
* マウントされたコンポーネント内で以前の state を復元し副作用の再生成をシミュレート
    * レイアウト副作用を作成
    * 副作用の作成用コードの実行
Yug (やぐ)Yug (やぐ)

あー、1回だけ作ってその値を変えたくないみたいな乱数を作るときってどうすれば良いんだろうってXで呟いたことあるけど、このuseIdめっちゃ使えそうかも?

useId

useId はハイドレーション時の不整合を防ぎつつクライアントとサーバで一意な ID を生成するためのフックです。

https://x.com/clumsy_ug/status/1843939036379099249

Yug (やぐ)Yug (やぐ)

lazy loading(遅延読み込み)と違って、使われないやつも低優先とはいえローディングされる感じか

useDeferredValue

useDeferredValue により、ツリー内の緊急性の低い更新の再レンダーを遅延させることができます。デバウンス (debounce) に似ていますが、それと比べていくつかの利点があります。遅延時間が固定でないため、最初のレンダーが画面に反映された時点ですぐに遅延されていた方のレンダーを始められるのです。また遅延されたレンダーは中断可能であり、ユーザインプットをブロックしません

Yug (やぐ)Yug (やぐ)

アプリ内では使うな、これはライブラリ開発者用だ、て言ってるから上級フックっぽい

useSyncExternalStore

useSyncExternalStore は、外部ストアへの更新を強制的に同期的に行うことで、外部ストアが並行読み取りを行えるようにします。これにより外部のデータソースに購読する際に useEffect を使う必要性がなくなるので、React 外部の状態を扱うあらゆるライブラリにとって推奨されるものです。
useSyncExternalStore はアプリケーションコードではなくライブラリで使用されることを意図しています。

1回使ったことあるけど、最初のちらつきを防止できるというメリットがあったな。
ちゃんと計算が終わるまで画面描画を待つみたいな挙動になってるんだろうな

Yug (やぐ)Yug (やぐ)

へぇ、まぁ使わなそうだな

useInsertionEffect は、CSS-in-JS ライブラリがレンダー時にスタイルを注入する際のパフォーマンス上の問題に対処できるようにするための新しいフックです。すでに CSS-in-JS ライブラリを構築しているのでなければ、これを使うことはまずないでしょう。このフックは、DOM が書き換えられた後、レイアウト副作用 (layout effect) が新しいレイアウトを読み込む前に実行されます。
useInsertionEffect はアプリケーションコードではなくライブラリで使用されることを意図しています。

Yug (やぐ)Yug (やぐ)

あーcreateRootのおかげなのか

React 18以前では、Reactのイベントハンドラによる連続処理では、このようなレンダリングのまとめが行われていましたが、React 18では「createRoot」を用いることで、あらゆるステートの変更に対してこうした処理が行われるようになります。

Yug (やぐ)Yug (やぐ)

ほぇ、なんかすごいな

Server-Side Renderingでは、「Streaming HTML」と「Selective Hydration」によって速度向上が実現されます。

Streaming HTMLは、サーバサイドでHTMLを生成する際に、すべてのデータが揃わなくとも、データがかけているところはプレースホルダを置いてHTMLを生成してクライアントに投げてしまい、データが取得できたタイミングであとからプレースホルダを実際のデータに置き換える、という処理をReactが行ってくれる、というもの。
サーバがデータ取得の途中でもクライアントでHTMLを受け取って表示を開始できるため、見かけ上の性能が向上します。

Selective Hydrationは、クライアントのHTMLにイベントハンドラをアタッチする場合、アタッチする部分を見つけるために必要とされるHTMLのレンダリングを行うJavaScriptが全部読み込まれていなくとも、先にレンダリング可能なところからイベントハンドラをアタッチしていく処理を可能にする、というものです。
これも読み込みの遅いJavaScriptに処理をブロックされることが減るため、速度の向上につながります。

リアルタイム性を重視するWebsocketみたいな概念に通じるものがありそう(知らん)

Yug (やぐ)Yug (やぐ)

GPTに聞いてみたら、なるほどなぁとなる部分があった

このバックグランドで進行するということをしているなら確かに並行処理っぽいと納得できる。

優先度の高いタスク(たとえばアニメーション更新)を進めながら、低優先度のタスク(たとえば非同期データ取得)をバックグラウンドで進行させます。

確かに並行的に優先度高い処理と優先度低いものを保持しておかないと、優先度低い処理を中断したあとにまた再開するとき困りそうだしな

同時に2つの道というか、レンダーというか、プロセスというか、そういうものが並行して行われているのだろうという推測はできた。それが並行レンダーなのだろう

んで、だからこそ中断が容易にできるし、再開もできるという感じなのではないだろか

とりあえずそれで理解

このスクラップは19日前にクローズされました