はじめに
テキストエリアの入力や、スクロールイベントなど一定時間内で多く発生する出来事に合わせて特定の処理を行う場合、デバウンスやスロットリングを介して特定の処理が発火する回数を制限することがあります。
デバウンスとスロットリングについては以前記事を書いたので、良ければ読んでください。
Reactでは、このように特定の処理が発火する回数を制限するためにuseDeferredValueと呼ばれるhooksを提供しています。
このhooksはデバウンスやスロットリングのように設定した秒数に依存した制限を行うのではなく、レンダリングの最適化によって制限を行います。
この記事ではそんなuseDeferredValueについて迫っていきます。
useDeferredValue
useDeferredValueとは、UIの一部の更新を遅延させるためのhooksです。
このhooksから提供された値は、引数の値が変更された時、まず変更前の値でレンダリングをしてから変更された値で再レンダリングを行います。
下の例ではtextとdeferredTextの変更履歴を表示しています。
テキストエリアで内容を変更すると、textはすぐに変更後の値に切り替わりますが、deferredTextはtextが切り替わった後に変更後の値になることがわかります。
useDeferredValueは以下のように利用します。引数に渡した値を元に遅延した値を吐き出します。
const [value, setValue] = useState(initialtValue);
const deferredValue = useDeferredValue(state);
useDeferredValueはvalueの変更をObject.isで比較して行います。そのため、valueがプリミティブな値でない時はレンダリング毎に異なる値が渡ってきたと判断してサイレンダリングを引き起こします。思わぬ不具合やパフォーマンスの低下が考えられるので気をつけましょう(オブジェクトがレンダー間で共通したものの場合は問題ないです)。
この機能は対象の状態の変化で重い計算を行うコンポーネントのレンダリングが必要となる時に便利です。
以下のコンポーネントはテキストエリアに入力した値の長さを表示するコンポーネントを500個表示します。そして、表示される500のコンポーネントはレンダリングに時間がかかるようにそれぞれ少なくとも1ms以上の計算時間がかかるようにしています。
計算に使う値は通常だとテキストエリアで利用している状態をそのまま、デバウンスだと500msのデバウンス処理を行う値、スロットリングだと500msのスロットリング処理を行う値、DeferredだとuseDeferredValueによって取り出された値になります。
通常のコンポーネントはテキストエリアに入力しようとすると、入力した値で500のコンポーネントの計算が終わって初めてテキストエリアに入力した値とその文字数が表示されます。
デバウンスされた値を用いたコンポーネントは計算が常に延長されます。テキストエリアに入力を続けても文字数は変わらず、入力が完了して500ms立ったら500のコンポーネントの計算を始まり文字数が表示されます。
スロットリングされた値を用いたコンポーネントは500msごとに500のコンポーネントの計算を行うので、一定時間ごとに通常のコンポーネントのような動きをするタイミングとテキストエリアへの入力を反映するタイミングが繰り返されます。
useDeferredValueから取り出した値を用いたコンポーネントはテキストエリアに入力を続けても文字数は変わらず、入力が完了して500のコンポーネントの計算が完了したタイミングで文字数が表示されます。
デバウンスとuseDeferredValueは途中で入力がブロックされなかったので使いやすかったと思います。さらに、デバウンスには入力後500ms待つ必要があるのに対し、useDeferredValueは入力後すぐに計算結果が出るのでより体験が良かったと思います。
デバウンスは500ms間に入力が止まらなければ計算を延期するようになっていました。それに対し、useDeferredValueは入力のたびに新しい値でコンポーネントのレンダリングを試みて、その途中に新たな入力があればそれを棄却して新たな値でレンダリングを初めるように、レンダリングのタイミングを制御しているのでこのような動きが可能となっています。
紹介した例は長さを表示するコンポーネントをメモ化していることによって成り立っていることに注意してください。useDeferredValueから新しい値でコンポーネントを計算する前にはテキストエリアの状態が新しい値で、useDeferredValueから提供される値が古い値で行われるのでした。そのとき、新しい値でレンダリングするテキストエリアだけは新たに計算を行い、古い値でレンダリングを行う重いコンポーネントはメモ化することによって計算を省略して高速にレンダリングが完了します。この工夫がなければNormalと同じように打つ度に重い計算が走ってテキストエリアの更新も止まるので注意が必要です。
このように通常の値を使うコンポーネントとuseDeferredValueから提供される値を使うコンポーネントのレンダリングのタイミングが分かれていることでこの機能は威力を発揮します。例えばSuspenseによってそれらのレンダリングが二分された世界では通常のアカウントのコンポーネントはそのまま動かして、useDeferredValueから提供される値を使うコンポーネントは計算が完了したタイミングで反映するように実装できます。下の例はja.react.devより引用したものです。適当に入力を開始するとLoading状態になって、入力中に計算が間に合う、もしくは入力が完了したタイミングで結果を出力します。
デバウンスやスロットリングが持つ優位性
useDeferredValueが用いられるケース多くはデバウンスやスロットリングを用いることで解決が可能ですが、遅延する時間を固定しなければいけない点がネックとなっています。
このことからReactではデバウンスやスロットリングではなく、useDeferredValueで全て解決することが最適かと思われますがそうではありません。
useDeferredValueは引数の値が変更される度に、変更後の値を用いて一度レンダリングを実施するというところに注意する必要があります。今回紹介した文字数を計算するコンポーネントなどはレンダリングを毎回試みても新しい値が出る度にキャンセルされることもあり不具合はありませんが、外部との通信を行うコンポーネントがレンダリング対象のときは通信が毎回飛ぶことになるので外部への過剰なやり取りを行なってしまいます。
その点、デバウンスやスロットリングはレンダリングではなく、状態自体の更新を抑制するように動くので外部との通信が必要最低限しか行われません。
適材適所で見極めて利用して行くことが大切そうです。