こんにちは、株式会社ファストコーディングのフルスタックエンジニア、独身貴族Fireです。
先日、週末のバイクツーリング中に立ち寄った道の駅で、観光地検索アプリを使っていた時のことです。検索ワードを入力するたびに画面が一瞬固まって、文字入力が追いつかない。イライラして結局使うのをやめてしまいました。帰宅後、ふと「これ、自分たちが作っているアプリでも起きてないだろうか?」と気になって、いくつかのプロジェクトを見直してみたんです。
*バイクの振動で手が震えてたのもあるかもしれませんが、それにしても反応が悪すぎました。。。
特に大規模な商品一覧やデータテーブルでライブ検索機能を実装している場合、ユーザーが文字を入力するたびに数千件のデータをフィルタリングして再レンダリングする必要があります。この処理が重いと、入力欄がカクついてユーザー体験が大きく損なわれます。今回は、React 18以降で導入されているstartTransitionとuseDeferredValueを使って、この問題を解決する方法を解説します。
従来の方法ではUIが固まる理由
React では、状態が更新されると即座に再レンダリングが走ります。これは通常問題ありませんが、レンダリングコストが高い場合、メインスレッドがブロックされて入力が遅延します。
なぜこれが問題になるのでしょうか。JavaScript はシングルスレッドで動作するため、重い処理が実行されている間は他の処理が待たされます。つまり、5000件のデータをフィルタリングしている間は、ユーザーの入力イベントも処理できません。結果として「入力したのに反応しない」という状態が発生するのです。
問題のある実装例
まず、よくある実装パターンを見てみましょう。この例では、入力のたびにすぐさま検索結果を更新しています。
function ProductSearch() {
const [query, setQuery] = useState('');
const [products] = useState(generateProducts(5000));
// 入力のたびに5000件をフィルタリング
const filtered = products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return (
<>
setQuery(e.target.value)} />
);
}
この実装では、onChangeイベントでsetQueryを呼び出すと、すぐにfilteredの再計算が走ります。そしてProductListが即座に再レンダリングされます。この一連の処理が完了するまで、次の入力イベントは処理されません。実際に計測してみると、1文字入力するたびに平均で450msの遅延が発生していました。
優先度制御による解決
React 18からは、更新に優先度をつけることができるようになりました。これにより「ユーザーの入力はすぐに反映するが、重い計算結果の表示は後回しにする」という制御が可能になります。重要なのは、入力欄の値はすぐに更新されるため、ユーザーは「ちゃんと入力できている」と感じられる点です。
startTransition を使った実装
startTransitionは、その中の状態更新を「低優先度」としてマークする関数です。Reactはこれを見て、「この更新は緊急ではないので、他の重要な処理があればそちらを先にやる」と判断します。
import { useState, useTransition } from 'react';
function ProductSearch() {
const [query, setQuery] = useState('');
const [deferredQuery, setDeferredQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [products] = useState(generateProducts(5000));
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 入力欄は即座に更新
startTransition(() => {
setDeferredQuery(value); // 検索は低優先度
});
};
const filtered = products.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<>
{isPending && 検索中...}
);
}
この実装のポイントは、queryとdeferredQueryという2つの状態を持つことです。queryは入力欄の表示に使い、deferredQueryは実際の検索処理に使います。ユーザーが「a」と入力すると、まずsetQuery(‘a’)が即座に実行されて入力欄に「a」が表示されます。次にstartTransition内のsetDeferredQuery(‘a’)が実行されますが、これは低優先度なので、他の処理があればそちらが優先されます。
useDeferredValue を使ったシンプルな実装
useDeferredValueは、値そのものを「遅延バージョン」として取得するフックです。startTransitionよりも宣言的で、状態を2つ管理する必要がありません。
import { useState, useDeferredValue, useMemo } from 'react';
function ProductSearch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const [products] = useState(generateProducts(5000));
const filtered = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [products, deferredQuery]);
const isPending = query !== deferredQuery;
return (
<>
setQuery(e.target.value)} />
{isPending && 検索中...}
);
}
useDeferredValueは内部的にstartTransitionと同様の仕組みを使っており、渡された値の「遅延バージョン」を返します。queryが更新されてもすぐにはdeferredQueryに反映されず、Reactが「今は余裕がある」と判断したタイミングで更新されます。また、useMemoでフィルタリング結果をメモ化することで、deferredQueryが変わらない限り再計算されないようにしています。
こちらにまとめて試せるコードをおいてみました。50,000件のランダムなレコードを扱った場合の体感速度の違いが分かると思います(MacProレベルのパソコンだと、この程度では何も変わらないかもしれませんが。。。)
パフォーマンス計測結果
| 指標 | 従来 | 適用後 | 改善率 |
|---|---|---|---|
| 入力遅延 | 450ms | 45ms | 90% |
| INPスコア | 820ms | 180ms | 78% |
実装時の注意点
処理が非常に速い場合、ローディング表示が一瞬だけ表示されて逆に気になることがあります。一定時間以下の場合は表示しないようにすると、より自然な体験になります。
const [showLoading, setShowLoading] = useState(false);
useEffect(() => {
if (isPending) {
const timer = setTimeout(() => setShowLoading(true), 200);
return () => clearTimeout(timer);
} else {
setShowLoading(false);
}
}, [isPending]);
まとめ
今回紹介したstartTransitionとuseDeferredValueは、React 18で追加された優先度制御の仕組みです。大規模なデータを扱うライブ検索やフィルタリング機能では、入力の反応速度がユーザー体験に直結します。従来の実装では、すべての更新が同じ優先度で処理されるため、重い計算が入力をブロックしてしまいます。
この問題を解決するために押さえておきたいポイントは以下の3つです。
- 入力は即座に反映し、結果表示は遅延させる優先度制御を導入する
- isPendingフラグでローディング状態を表示し、ユーザーに処理中であることを伝える
- useMemoやReact.memoと組み合わせて、不要な再計算を防ぐ
実際のプロジェクトでは、入力遅延が450msから45msに改善され、INPスコアも78%向上しました。次のプロジェクトでぜひ試してみてください。
株式会社ファストコーディングでは、こういったReactのパフォーマンス最適化について豊富な経験があります。INPスコアの改善でお困りの際は、お問い合わせフォームからご相談ください。

