プロンプト/メンション補完システムの入力ラグを修正
チャット入力時のオートコンプリートで発生していた入力ラグを、デバウンス導入とレンダリング件数の上限設定という2つのアプローチで解消しました。
背景
キャンプファイアのチャット入力において、プロンプト/メンション補完システムが顕著な入力ラグを引き起こしていました。特にトリガー文字(@など)を入力した直後の最初の数文字でラグが目立ち、UX上の問題となっていました。原因は独立した2つの設計上の問題でした。
1つ目はデバウンスの欠如です。キーストロークのたびに lexxy:change イベントリスナー経由でフィルタリングとDOM描画のパイプライン全体が実行されていました。2つ目はレンダリング件数の無制限です。@ 単体のような広い検索クエリを入力すると、大規模アカウントでは数百件の候補要素がDOMに書き込まれ、レンダリングコストが高騰していました。
技術的な変更
2つの問題に対してそれぞれ独立した修正が加えられ、合わせて新しいパフォーマンステストが追加されています。
デバウンスの導入
src/helpers/timing_helpers.js に同期版の debounce 関数が追加されました。既存の debounceAsync とは別に、通常の関数をラップする実装です。
export function debounce(fn, wait) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => fn(...args), wait)
}
}
src/elements/prompt.js では、#filterOptions を直接イベントリスナーに渡す代わりに、50ms でデバウンスされた #debouncedFilterOptions を生成してリスナーに登録するよう変更されました。
// 変更前
registerEventListener(this.#editorElement, "lexxy:change", this.#filterOptions)
// 変更後
const FILTER_DEBOUNCE_INTERVAL = 50
#debouncedFilterOptions = debounce(() => this.#filterOptions(), FILTER_DEBOUNCE_INTERVAL)
registerEventListener(this.#editorElement, "lexxy:change", this.#debouncedFilterOptions)
なお、トリガー文字を入力した際のポップオーバーの初回表示は即時のままで、デバウンスの対象は「入力中のフィルタリング」のみです。
レンダリング件数の上限設定
LocalFilterSource と RemoteFilterSource の両方に MAX_RENDERED_SUGGESTIONS = 100 が定義され、候補要素の生成を100件でキャップするよう変更されました。LocalFilterSource では forEach ループを for...of に書き換え、上限に達した時点で break するようになっています。
// 変更前
promptItems.forEach((promptItem) => {
const searchableText = promptItem.getAttribute("search")
if (!filter || filterMatches(searchableText, filter)) {
const listItem = this.buildListItemElementFor(promptItem)
this.promptItemByListItem.set(listItem, promptItem)
listItems.push(listItem)
}
})
// 変更後
const MAX_RENDERED_SUGGESTIONS = 100
for (const promptItem of promptItems) {
if (listItems.length >= MAX_RENDERED_SUGGESTIONS) break
const searchableText = promptItem.getAttribute("search")
if (!filter || filterMatches(searchableText, filter)) {
const listItem = this.buildListItemElementFor(promptItem)
this.promptItemByListItem.set(listItem, promptItem)
listItems.push(listItem)
}
}
パフォーマンステストの追加
test/browser/fixtures/mentions-large.html(200件以上のメンション候補を含む大規模フィクスチャ)と test/browser/tests/prompts/prompt_performance.test.js が追加されました。テストは「@ 入力時に100件以下に抑えられること」と「フィルタ入力でさらに絞り込まれること」の2ケースをPlaywrightで検証しています。
設計判断
デバウンス間隔の分離が注目に値します。RemoteFilterSource が既に持っていた DEBOUNCE_INTERVAL = 200ms とは別に、prompt.js 側には FILTER_DEBOUNCE_INTERVAL = 50ms が設定されています。リモートリクエストの抑制(200ms)とDOM更新サイクルの抑制(50ms)を目的に応じて分離することで、応答性とネットワーク効率をそれぞれ独立してチューニングできる設計になっています。
同期版 debounce の独立追加についても、既存の debounceAsync を使い回さず新関数として実装したことに設計の意図が見えます。非同期フィルタリングパイプラインとイベントリスナーの用途の違いを明示し、各関数の責務を明確に保っています。
MAX_RENDERED_SUGGESTIONS の定数化は、ローカルとリモートで同じ値(100)を各ファイル内に定義する形を取っています。共通定数として一元管理するのではなく、ソースの実装ごとに定義を持たせることで、将来的にソース種別ごとに上限値を変更する際の柔軟性を確保しています。
まとめ
本PRは、デバウンスによるイベント発火の間引きとレンダリング件数の上限設定という、相互に独立した2つの施策を組み合わせて入力ラグを解消しています。どちらの変更も既存の補完機能の正確性や動作フローには手を加えず、パフォーマンス劣化の経路のみを外科的に塞いだ設計といえます。