useDeferredValueで重い一覧による入力の遅れを防ぐ方法
入力欄の値をそのまま重い一覧に渡すと、キーを押すたびに一覧の描き直しが終わるまで、入力欄の文字も画面に出ません。一覧に渡す値を useDeferredValue で遅らせ、一覧を memo で包むと、入力欄だけが先に更新され、一覧はあとから追いつくようになります。
筆者は検証環境で、書き方を3通り用意しました。値をそのまま渡す書き方、useDeferredValue だけを使う書き方、useDeferredValue と memo を組み合わせる書き方です。それぞれで、キーを押してから文字が画面に出るまでの時間を測りました。
検証環境は Ubuntu 24.04、Node.js 22.22.0、CPU は4コアです。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、esbuild 0.28.2、jsdom 29.1.1、playwright 1.56.1 です。.tsx を tsc で型チェックしてから esbuild で変換しました。時間の測定は、Playwright で動かした Chromium 141(ヘッドレス)で行い、ブラウザでの測定には本番用のビルドの React を使いました。jsdom で動かしたときは、ビルドを指定していません(開発用のビルドで動きます)。
わざと重くした一覧を用意する
useDeferredValue のリファレンスにある、遅らせて描き直す例にならい、描画に1ミリ秒かかる項目を250個並べた一覧を作りました。一覧を1回描くと、およそ250ミリ秒かかります。
import { memo } from 'react';
// わざと重くした一覧。1項目の描画に1ミリ秒かかる。
function SlowItem({ text }: { text: string }) {
const start = performance.now();
while (performance.now() - start < 1) {
// 何もしないで待つ
}
return <li className="item">Text: {text}</li>;
}
export function SlowList({ text }: { text: string }) {
const items = [];
for (let i = 0; i < 250; i++) {
items.push(<SlowItem key={i} text={text} />);
}
return <ul className="items">{items}</ul>;
}
// props が前回と同じなら描画を飛ばす版
export const MemoSlowList = memo(SlowList);
MemoSlowList は、SlowList を memo で包んだものです。memo のリファレンスのとおり、親が描き直されても、props が前回と同じなら描画を飛ばします。
3つの書き方
1つ目は、入力欄の値 text をそのまま一覧に渡す書き方です。
import { useState } from 'react';
import { SlowList } from './SlowList';
export function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}
2つ目は、useDeferredValue で遅らせた deferredText を渡す書き方です。一覧は memo で包んでいません。
import { useDeferredValue, useState } from 'react';
import { SlowList } from './SlowList';
export function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
3つ目は、2つ目の一覧を memo で包んだ MemoSlowList に替えた書き方です。一覧が入力に追いついていない間は、一覧を薄く表示します。
import { useDeferredValue, useState } from 'react';
import { MemoSlowList } from './SlowList';
export function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const isStale = text !== deferredText;
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<MemoSlowList text={deferredText} />
</div>
</>
);
}
リファレンスの「戻り値」の説明によると、値が変わったとき、React はまず古い値を返して描き直します。そのあと、新しい値でもう一度、裏で描き直します。そのため text と deferredText が違う間は、一覧が古い内容のままだと分かります。薄く表示するのは、リファレンスにある、古い内容だと示す方法と同じ書き方です。
1文字入れた直後の表示を比べる
まず jsdom の上で3つを描画し、入力欄に「a」を入れた直後と、1秒後の表示を比べました。「イベントの処理」は、input イベントを送ってから、React がそのイベントの描画を終えるまでの時間です。
$ node run.mjs ./out/AppPlain.mjs
./out/AppPlain.mjs
最初の表示: 入力欄「」 / 一覧の先頭「Text: 」
「a」を入れた直後(イベントの処理に 286ms): 入力欄「a」 / 一覧の先頭「Text: a」
1秒後: 入力欄「a」 / 一覧の先頭「Text: a」
$ node run.mjs ./out/AppDeferredNoMemo.mjs
./out/AppDeferredNoMemo.mjs
最初の表示: 入力欄「」 / 一覧の先頭「Text: 」
「a」を入れた直後(イベントの処理に 280ms): 入力欄「a」 / 一覧の先頭「Text: 」
1秒後: 入力欄「a」 / 一覧の先頭「Text: a」
$ node run.mjs ./out/AppDeferred.mjs
./out/AppDeferred.mjs
最初の表示: 入力欄「」 / 一覧の先頭「Text: 」
「a」を入れた直後(イベントの処理に 8ms): 入力欄「a」 / 一覧の先頭「Text: 」
1秒後: 入力欄「a」 / 一覧の先頭「Text: a」
値をそのまま渡す1つ目は、入力欄と一覧が同時に「a」になりました。その代わり、イベントの処理に286ミリ秒かかっています。一覧を描き終えるまで、入力欄も待たされたからです。
3つ目は、入力欄だけが先に「a」になり、一覧は「Text: 」のままでした。かかった時間は8ミリ秒です。一覧は、1秒後には「Text: a」に追いついていました。
2つ目も、表示の順番は3つ目と同じでした。しかし、イベントの処理に280ミリ秒かかっています。遅らせた値を使っていても、入力欄を先に更新するための描画で、一覧も古い値のまま描き直されたからです。
ブラウザで、続けて打ったときの時間を測る
次に、Chromium で3つを開き、60ミリ秒おきに「abcdefgh」の8文字を打ちました。前のキーの処理が終わるのを待たずに、決まった間隔で押しています。キーを押した時刻と、その文字が入った状態で次のフレーム(requestAnimationFrame)が来た時刻の差を、文字が画面に出るまでの時間としました。測定のコードは、React の外に置いています。各書き方で3回ずつ測りました。
$ node measure.mjs
Chromium 141.0.7390.37
plain 1回目: 入力欄に出るまで(ms) [1288, 1228, 1168, 1105, 1044, 1753, 1693, 1632] / 入力欄が変わったフレーム 2回・一覧が変わったフレーム 2回 / 最後のキーから一覧が「abcdefgh」になるまで 1632ms
(中略)
deferred-nomemo 1回目: 入力欄に出るまで(ms) [1264, 1204, 1144, 1084, 1023, 1718, 1657, 1596] / 入力欄が変わったフレーム 2回・一覧が変わったフレーム 1回 / 最後のキーから一覧が「abcdefgh」になるまで 1869ms
(中略)
deferred 1回目: 入力欄に出るまで(ms) [4, 10, 2, 6, 12, 4, 8, 14] / 入力欄が変わったフレーム 8回・一覧が変わったフレーム 1回 / 最後のキーから一覧が「abcdefgh」になるまで 284ms
(中略)
中央値(3回×8キーの24個から)
plain: 入力欄に出るまで 中央値 1279ms・最大 1763ms / 一覧が追いつくまで 中央値 1632ms
deferred-nomemo: 入力欄に出るまで 中央値 1263ms・最大 1745ms / 一覧が追いつくまで 中央値 1878ms
deferred: 入力欄に出るまで 中央値 7ms・最大 19ms / 一覧が追いつくまで 中央値 280ms
plain が1つ目、deferred-nomemo が2つ目、deferred が3つ目の書き方です。中央値をグラフにしました。

1つ目と2つ目では、8文字を打つ間に、入力欄の表示が変わったのは2回だけでした。1文字目が画面に出たのは、キーを押してから約1.3秒後で、そのときには5文字目までがまとめて出ています。
3つ目では、入力欄は1文字ごとに更新され、どの文字も20ミリ秒以内に出ました。一方、一覧の表示が変わったのは、打ち終わったあとの1回だけでした。最後のキーから一覧が追いつくまでは約280ミリ秒で、一覧を1回描く時間とほぼ同じです。
useDeferredValue を使っても、一覧そのものの描画は速くなりません。入力欄の更新を先に済ませ、一覧の描き直しを後回しにすることで、入力が待たされなくなります。
memo がないと効かない理由
リファレンスで、画面の一部の描き直しを遅らせる使い方に添えられた注意には、この最適化には一覧を memo で包む必要があると書かれています。入力欄を先に更新する描画では、deferredText はまだ前の値です。memo で包んでいれば、一覧は props が変わっていないので描画を飛ばせます。包んでいないと、一覧はこの描画でも描き直されます。
2つ目の書き方の結果は、この説明どおりでした。入力欄を先に更新する描画でも一覧を描くので、入力欄はそのまま渡す書き方とほぼ同じだけ待たされます。そのうえ、新しい値で一覧をもう一度描くため、一覧が追いつくまでの時間は1つ目よりも長くなりました(中央値 1878ミリ秒、1つ目は 1632ミリ秒)。
memo は、props を1つずつ Object.is で前回の値と比べます(memo のリファレンスにある、props の変化を減らす方法)。同じリファレンスの、memo をどこに使うかの説明によると、描画の中で作ったオブジェクトや関数を渡すと、props は毎回違うことになり、memo は役に立ちません。useDeferredValue の注意事項にも、useDeferredValue に渡す値は文字列や数値のようなプリミティブ値か、描画の外で作ったオブジェクトにするよう書かれています。描画の中で作ったオブジェクトを渡すと、毎回違う値とみなされ、不要な描き直しが起きます。
打っている間、一覧が1回しか変わらなかった理由
3つ目の書き方で、一覧が打ち終わるまで変わらなかったのは、リファレンスの説明によると、裏で行う描き直しが途中で打ち切られるためです。リファレンスの注意事項には、裏で描き直している間に値がまた変わると、React はその描き直しを最初からやり直します。入力が一覧の描画よりも速いと、一覧は入力が止まってから描き直される、とも書かれています。
今回は、60ミリ秒おきに打った文字に対して、一覧を1回描くのに約250ミリ秒かかります。リファレンスの説明どおりなら、打っている間の描き直しは毎回途中で打ち切られ、最後の「abcdefgh」の描画だけが画面に出たことになります。測定でも、一覧が変わったフレームは1回だけでした。
同じ注意事項によると、useDeferredValue そのものには決まった待ち時間がありません。React は、入力欄を先に更新する描画を終えると、すぐに新しい値での描き直しを始めます。
デバウンスやスロットルとの違い
入力が止まるまで待つデバウンスや、一定の間隔でしか更新しないスロットルでも、一覧の描き直しを減らせます。リファレンスにある、デバウンス・スロットルとの違いの説明によると、違いは2つあります。
1つは、待ち時間を決めなくてよいことです。速い端末なら一覧はすぐに追いつき、遅い端末なら、そのぶん遅れて追いつきます。もう1つは、裏の描き直しを途中で打ち切れることです。デバウンスやスロットルでは、一覧の描画が始まると、終わるまで入力は待たされます。デバウンスやスロットルは、描画で入力が止まる時点を後ろにずらすだけです。
ただし、ネットワークへのリクエストのように、描画の外で行う処理を減らしたいときは、デバウンスやスロットルが役に立つと書かれています。注意事項にも、useDeferredValue だけでは余分なネットワークのリクエストは防げないとあります。両方を組み合わせて使うこともできます。
今回測った時間は、検証環境のヘッドレスの Chromium での結果です。端末の速さによって、一覧が追いつくまでの時間は変わります。実際の画面の描画(ペイント)の時刻ではなく、requestAnimationFrame が呼ばれた時刻で測っています。





