やあ。今日もReactのコードと睨めっこ、お疲れ様。
Reactを触り始めて数年、コンポーネントの分割やHooksの使い分けにも慣れてきた頃だろう。でも、いざデータ量が増えたり、複雑なフィルタリング処理を走らせたりすると、「UIが微妙にカクつく」という壁にぶつからないか?
ボタンを押した瞬間、メインスレッドが計算に占有されてしまい、ローディングスピナーすら止まって見える。あれ、本当にストレスだよな。
今日は、そんな「重い処理によるUIのフリーズ」を解消し、Reactのレンダリングパイプラインを意のままに操るための切り札、`useTransition`について話をしよう。
—
なぜ「重い処理」はUIを止めるのか
ブラウザのメインスレッドは、基本的に「シングルタスク」だ。JavaScriptの実行、レイアウト計算、描画(ペイント)は、同じ一本の道を走っている。
Reactが状態更新を受け取ると、コンポーネントを再レンダリングし、仮想DOMを比較して、最終的にDOMを更新する。この一連のプロセスが長引くと、ブラウザは次のフレームを描画するタイミングを逃す。これが「カクつき」の正体だ。
これまでのReactなら、`debounce`や`throttle`で入力を間引くのが定石だった。だが、`useTransition`はアプローチが根本的に違う。「React自身に、どの更新が緊急で、どの更新が後回しでいいのかを教える」という手法だ。
useTransition:緊急度を制御する魔法
`useTransition`を使うと、状態更新を「トランジション(遷移)」としてマークできる。Reactはこれを「重要度が低い」と判断し、ブラウザの隙間時間を見計らってレンダリングを実行してくれる。
実践:フィルタリングの最適化
例えば、数千件のデータリストを検索ボックスでフィルタリングするシーンを想像してくれ。入力するたびに即座に全件検索を回すと、タイピングすら重くなるだろう。
import React, { useState, useTransition } from ‘react’;
// 重い処理をシミュレートする関数
const filterData = (data: string[], query: string) => {
// あえて重い計算を挟む
const start = performance.now();
while (performance.now() – start < 20) {}
return data.filter((item) => item.includes(query));
};
export const SearchList = ({ items }: { items: string[] }) => {
const [query, setQuery] = useState(”);
const [filteredItems, setFilteredItems] = useState(items);
// isPending: トランジションが進行中かどうかを示すフラグ
// startTransition: この中で状態更新を行うと「非緊急」とマークされる
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent
const value = e.target.value;
// 1. 入力値の更新は「緊急」として即時反映(タイピングの追従性確保)
setQuery(value);
// 2. フィルタリングという重い処理は「非緊急」としてマーク
startTransition(() => {
setFilteredItems(filterData(items, value));
});
};
return (
{/ 進行中なら薄く表示するなどのUX改善も容易 /}
))}
);
};
ここがプロの視点:なぜDebounceより優れているのか
`useTransition`の真価は、「中断可能(Interruptible)」であることだ。
もしユーザーが素早くタイピングし、さらにその途中で別の操作を行ったとしても、Reactは現在実行中の「非緊急のレンダリング」を中断し、より優先度の高いユーザー操作(入力の反映など)を優先できる。
`debounce`は「一定時間待つ」という強制的な遅延を加えるが、`useTransition`は「Reactのレンダリングエンジンに委ねる」という賢い戦略をとる。ブラウザが暇な瞬間を見つけて処理をねじ込むため、結果として最も応答性が高い体験を提供できるんだ。
—
実務で運用する際の注意点
ただし、万能薬ではない。これにはいくつか「現場の泥臭い教訓」がある。
1. 状態更新が同期的に行えること: `startTransition`の中に非同期処理(`async/await`など)を直接書くことはできない。あくまで「状態更新のスケジューリング」であることを忘れないでほしい。
2. `isPending`を活用したフィードバック: 非緊急扱いとはいえ、ユーザーには「今裏で処理中であること」をUIで伝えるべきだ。ローディングスピナーを出すか、透明度を下げるか、あるいはボタンを無効化するか。無言のまま待たせるのが一番のUXの敵だ。
3. 過信しない: もし処理が本当に重すぎて、何をどう最適化してもメインスレッドを止め続けるなら、それはReact側の問題ではなく「データ構造」や「Web Workerへのオフロード」を検討すべきフェーズだ。
最後に:Reactは「魔法」ではない
`useTransition`は強力だが、あくまでツールだ。
一番の最適化は、「そもそも重い計算を避けること」にある。不要な再レンダリングを`React.memo`や`useMemo`で防ぎ、それでもどうしようもない計算量がある時に、この`useTransition`というカードを切るのが、我々フロントエンド・アーキテクトの正しい戦い方だ。
コードを書き終えたら、一度ChromeのDevToolsで「CPUを6x slowdown」にして、`useTransition`がある場合とない場合を比較してみてくれ。その差を体感した時、君のエンジニアとしての引き出しが一つ増えるはずだ。
また何か詰まったら、いつでも聞きに来るといい。現場からは以上だ。

コメント