はじめに
スロットリングとは、一定時間内に発生する特定の処理を制限するテクニックの1つです。
例としてユーザーが検索のためにテキストボックスへ入力するシーンを考えます。
テキストボックスへ入力した内容が変更されるたびに検索処理が走るような仕様の場合、大量の処理が押し寄せてくるのでパフォーマンスの低下につながります。
そこで、スロットリングを導入することで検索処理が一定の入力ごとに行われ、パフォーマンスの低下を防げます。
Reactでは、スロットリングは主にユーザーの入力やウィンドウのリサイズなど、高頻度で発生するイベントを制御するために使用されます。
スロットリング無しの処理を見る
まずは、比較対象となるスロットリング無しのコンポーネントを作ります。
useState
を用いて入力したテキストを準備し、テキストが更新されると下部に入力したテキストの文字数が出るようにしました。
今回は長さを求めるだけなのでパフォーマンスに影響はありませんが、外部との通信を同じタイミングで行った場合にトラフィックの増加が想像できます。
また、入力は短期間で行われることが予想されるので実装方法によっては結果の出力順が前後してしまいます。
状態の更新がスロットリングされるhooksを作る
テキストエリアの入力はそのまま行わせたい一方、特定の処理は一定の入力置きに行いたいので、テキストエリアの値を管理する状態と特定の処理を行うように監視する状態を分けることを考えます。
const [text, setText] = useState('');
const displayText = text;
text
がテキストエリアを管理する状態で、displayText
はtext
を渡しただけの値です。
これでは分けた意味がないのでalertText
の更新を1秒に1回のタイミングだけ行うようにします。
このようにする場合displayText
は単純にtext
から生成できないのでuseState
で状態を作るようにします。さらに、タイミングの調整にsetTimeout
を利用するのでuseEffect
を用いて記述します。
const [text, setText] = useState('');
const [displayText, setDisplayText] = useState(text);
useEffect(() => {
const timeoutID = setTimeout(() => {
setDisplayText(value);
}, 1000);
return () => {
clearTimeout(timeoutID);
};
}, [text])
これでtext
が変更されてから1秒後にdisplayText
を更新するようになりました。
useEffect
の処理は初回描画後とtext
が変更されるたびに発火します。そして、中では変更された1秒後にdisplayText
の値をtext
の値に変更させています。
しかし、1秒経つ前にtext
が再度変更されると、再描画が起きるので次のuseEffect
の処理が走る前に前回のクリーンアップ関数が走って1秒後の更新がキャンセルされます。これによって前回の更新から1秒経っていてもテキストエリアへの入力が続いた場合は更新されません。
さらに、最初の更新は即座に行って欲しいところですが、1秒待つ必要があるという問題もあります。
これを修正するために、最後に実行した時間をref
に覚えさせて処理を改めます。
const interval = 1000;
const [text, setText] = useState('');
const [displayText, setDisplayText] = useState(text);
const lastUpdated = useRef<number>();
useEffect(() => {
const now = Date.now();
if (!lastUpdated.current || now >= lastUpdated.current + interval) {
lastUpdated.current = now;
setDisplayText(text);
} else {
const timeoutId = setTimeout(() => {
lastUpdated.current = now;
setDisplayText(text);
}, interval);
return () => {
clearTimeout(timeoutId);
};
}, [text])
まだ一度も実行されていない場合、即ちlastUpdated.current
がundefined
の時と、最後に更新してからすでに1秒以上経っていた場合は更新するようにしました。
これをhooksで切り出すと以下のようになります。interval
は今回だと1000で、遅延させる秒数を入力します。
const useThrottling = <T extends any>(value: T, interval: number) => {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastUpdated = useRef<number>();
useEffect(() => {
const now = Date.now();
if (lastUpdated.current && now >= lastUpdated.current + interval) {
lastUpdated.current = now;
setThrottledValue(value);
} else {
const timeoutId = setTimeout(() => {
lastUpdated.current = now;
setThrottledValue(value);
}, interval);
return () => {
clearTimeout(timeoutId);
};
}
}, [value, interval]);
return throttledValue;
};
スロットリング有りの処理を見る
テキストが変更に合わせて内容が下部に入力した文字の長さが記述されていたところから、1秒置きに変更が反映されるようにします。
このようにすることでテキストエリアの入力のたびに特定の処理が走らないのでパフォーマンスの低下や処理の結果が前後することを防げます。
インターバルの時間は程よい遅延時間を取り繕って設定する必要があるので慎重に検討しましょう(今回の例では時間を取りすぎて利用者のストレスになりそうです)。
おわりに
Reactの状態をスロットリングさせる方法を紹介しました。状態の更新が過多で、余計な計算が発生した場合はこのようにして対応を考えましょう。