JavaScript - Debounceを適用した動的検索
概要
動的検索の重要性
ユーザーが迅速に情報を見つけられるようにするため、入力と同時に関連する結果を表示する動的検索ボックスはユーザー体験を向上させます。これにより直感的で効率的な検索経験を提供し、不要なページロードを減少させます。
デバウンス(Debounce)の役割
ユーザーが入力するたびにサーバーへのリクエストが増えることを避けるため、デバウンスは連続的な入力を一定時間後に一回だけ処理します。これにより、サーバーの負荷を減らし、ユーザーの入力が終わるまで待ってから検索結果を更新することで、よりスムーズなユーザー体験を提供します。
Debounceとは?
Debounceの概念
デバウンスは、短期間に連続して発生するイベント(例えばキーボード入力)を一定時間待ってから処理する技術です。この間に新たなイベントが発生しなければ、最後のイベントのみが処理されます。これにより不要なサーバーの負荷を減らし、よりスムーズで効率的なユーザー体験を実現します。特に、動的な検索機能などで頻繁に利用されます。
実装例:アプリ内広告新規出稿(Wantedly 社内向けAdminツール)
実装背景
出稿しようとする募集の詳細が確認できず、さらに実際に出稿対象とならない募集でも出稿が可能となってしまっております。これにより、正確な広告出稿のために担当者が何度も確認作業を行わなければならないという不便が生じていました。
最初の要件では、出稿対象とならない募集はバリデーションにより出稿できないように設定することでした。しかし、出稿可能な募集の情報も確認できればより良いユーザー体験になると考え、バリデーションに加えて動的検索機能を実装しました。ただし、本日はデバウンスの機能についてのみ詳しくお話しします。
また、対象画面がRubyで作成されたため、HamlのインラインJavaScriptで新しい機能を追加しました。
動的検索の発火
window.onload = () => {
const input = document.getElementById("project_id_input");
const container = document.getElementById("project_info");
const submit = document.getElementById("ad_submit");
const state = document.getElementById("project_state");
const error = document.getElementById("error_msg");
...
input.addEventListener('input', (event) => {
const projectId = event.target.value;
return debounce(projectId, container, submit, state, error)
})
...
}
debounce処理
let timer;
const debounce = (projectId, container, submit, state, error) => {
if (timer) clearTimeout(timer);
if (!projectId) {
container.classList.remove('is-loading');
return setDom();
}
container.classList.add('is-loading');
timer = setTimeout(() => {
fetch('endpointUrl/' + projectId, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((response) => {
setDom(response.id, response.company_id, response.title, getStatus(response));
if (getStatus(response) !== "募集中(閲覧可)") {
state.classList.add('is-moderate');
error.classList.add('is-error');
submit.setAttribute("disabled", true);
} else {
state.classList.remove('is-moderate');
error.classList.remove('is-error');
submit.removeAttribute("disabled");
}
})
.catch(() => {
setDom();
submit.setAttribute("disabled", true);
})
.finally(() => {
container.classList.remove('is-loading');
});
}, 500);
}
debounce 関数の詳細
1. 関数の最上段でタイマーをクリア
const debounce = (projectId, container, submit, state, error) => {
// 以前のタイマーをクリア
if (timer) clearTimeout(timer);
タイマーのクリアを関数の最上部に配置しどのような場合でもタイマーをクリアする方法は一般的にデバウンスパターンで推奨されるアプローチです。
連続したイベントの処理を防ぐ
速く連続してイベントが発生させた場合、以前に設定されたタイマーをクリアし新しいタイマーを開始することで最後のイベントのみが処理されます。不要な関数呼び出しを防ぎ、パフォーマンスを向上させ、サーバーの負荷を減らすことができます。
一貫した動作を保証する
関数が呼び出されるたびに以前のタイマーが確実にキャンセルされ、新しいタイマーが設定されます。予期しないタイマーの実行を防ぎ、関数の動作を予測可能にします。
メンテナンス
コードの読みやすさと理解しやすさを向上させます。さらに、デバウンスロジックの変更や追加機能の実装時にエラーの可能性を減らすことができます。
ただし、特定の状況や要件に応じてタイマーを常に初期化することが適切でない場合もあります。
2. 例外対応
// projectIdがない場合、ローディング表示を削除しDOMを初期状態に戻す。
if (!projectId) {
container.classList.remove('is-loading');
return setDom(); // DOMをデフォルト状態に戻す
}
// ローディングを開始
container.classList.add('is-loading');
これらはDebounceを適用した動的検索機能をさらに改善するための処理です。入力値がない場合(特に入力した値が全て削除された場合)、DOMをデフォルト状態に戻すことで不要な処理を防ぎ、Debounceに設定された時間を待たなくても良くなります。これにより、より迅速かつ効率的なユーザー体験を提供します。
また、ローディング処理を追加することでユーザーが入力した値に基づいて何らかの処理が行われていることを示し、ユーザーが予期した動作が行われていることを認識できるようになります。
3. Debounceのメインロジック
// debounce ロジックを適用
clearTimeout(timer); // 設定されていたタイマーをクリア
timer = setTimeout(() => {
// APIリクエスト
fetch('endpointUrl/' + projectId, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then((response) => {
if (!response.ok) {
throw new Error('ネットワークレスポンスが正常ではありません');
}
return response.json();
})
.then((response) => {
// DOMを更新
setDom(response.id, response.company_id, response.title, getStatus(response));
// 状態に応じたUIを更新。
// 募集の出稿可能状況に対するバリデーションを含む。
if (getStatus(response) !== "募集中(閲覧可)") {
state.classList.add('is-moderate');
error.classList.add('is-error');
submit.setAttribute("disabled", true);
} else {
state.classList.remove('is-moderate');
error.classList.remove('is-error');
submit.removeAttribute("disabled");
}
})
.catch(() => {
setDom(); // エラーが発生した場合、DOMを初期化
submit.setAttribute("disabled", true);
})
.finally(() => {
container.classList.remove('is-loading'); // ローディングを終了
});
}, 500); // debounce 時間は500msに設定
ユーザーが入力を停止してから500ミリ秒後にのみAPIリクエストを送信します。ユーザーが検索語をタイプする間、毎回の入力でリクエストが送られるのを防ぎます。また、デバウンス時間(この実装では500ms)は、アプリケーションのニーズやユーザー体験に応じて調整することが可能です。
デバウンスはリソースを効率的に利用し、不要なトラフィックを減少させ、サーバーへの負担を軽減しながらユーザーにスムーズなインタラクティブな体験を提供する重要な機能です。
Debounceを適応した動的検索の挙動
Before
画面
処理
After
画面
処理
最後に
Timeoutはなぜ500ms?
今回のケースでは数字のみが検索可能であり、記録された数字を見て入力する場合の間隔を測定してみると約300msから700msでした。ユーザーによって入力間隔は異なる可能性があり十分な時間設定は不要な処理を防ぐのに役立ちますが、設定時間が長すぎて入力完了後の待機時間も長くなると望ましくない体験になると感じました。そのため、中間値の500msを採用しました。
DebounceとThrottleの違い
デバウンス (Debounce)
ex) 文字を入力している場合、文字入力が終了してから一定時間が経過した後に処理を行する
- 続けるinputをすぐ実行せず、inputが停止して設定時間が経過した後にのみ実行したい。
- 連続したイベントが発生した場合、最後のイベントの発生後一定時間が経過して他のイベントが発生しないときのみ実行します。
- 主に頻繁なイベント発生を効率的に処理したい場合に使用されます。
スロットル (Throttle)
ex) スクロールイベントの処理やマウスの動き追跡など、連続的かつ高速で発生するイベントを処理する
- イベント処理回数を制限したい。
- 関数が一定時間間隔でのみ呼び出されるように制限します。つまり、設定された時間内に一度だけ関数が呼び出され、その時間内に発生する追加的なイベントは無視されます。
- 主に連続的な入力や動作に対して一定の間隔で反応を維持したい場合に使用されます。
ユーザー体験とサーバー効率の向上
Debounceの導入はわずかな技術的変更であっても、ユーザーのインタラクティブな体験とサーバーの負荷に大きな影響を与えることができます。
ユーザーがより快適に情報を探し出せるようになり同時にサーバーのリソースも合理的に活用できるようになるため、是非ともこの技術をご自身のプロジェクトやアプリケーションに活用してみてください。