本記事はVildan Softic、Julian Motzが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
スクロールイベントのリスニングでは、パフォーマンスの低下が懸念されます。ユーザーが1回スクロールするごとにブラウザーがコールバックを実行し、1秒間に多くのイベントが発生しかねません。コールバックで何回も再描画を実行すると、ユーザーに不快な印象を与えることになります。再描画は負荷が大きく、スクロールイベントのように画面の広い面積を再描画する場合は特に顕著です。
この課題は次の例に示されています。
パフォーマンスの低下以外に、画面の焼き付きも起こしやすくなります。この例は、人が出す「命令」をコンピューターが逐一そのとおり実行したらどうなるか、を示しています。背景色がスムーズに変わらず、スクリーンがちらついていますね。気の毒なプログラマーの中には「完璧にお手上げ」と感じている人もいます。なにか良い方法はないでしょうか?
イベントの調整
解決策の1つは、イベントを延期し、何回かのイベントを一度にまとめて扱うことです。この点で役立つ、よく使われる2つの関数があります。throttleとdebounceです。
throttleは決められた時間間隔でイベントが確実に一定量で推移し、一方、debounceは瞬時に頻発するイベントを単一のイベントにグループ化します。1つの考え方として、throttleは時間ベース型、debounceはイベントドリブン型と言えます。ひとたびグループ化してしまうと、debounceの実行は保証されなくなりますが、throttleは(時間ごとに)確実に実行されます。詳しくは、debounceとthrottleを比較した詳細な記事『Debouncing and Throttling Explained Through Examples』を参照してください。
debounce
debounceは、Ajaxでのキーの押下など、ほかの問題も解決します。フォーム入力で、キーをたたくごとにリクエストが送信されると困ったことになります。エレガントなソリューションの1つは、立て続けに起こるキー操作を、Ajaxリクエストをトリガーする1つのイベントにグループ化することです。1つのイベントにグループ化するとタイピングの自然な流れにぴったり合い、サーバーリソースの節約にもなります。キーの押下では、イベントの間隔は重要ではありません。なぜなら、ユーザーは一定の間隔でキーを押すわけではないからです。
throttle
debounceでは対応しきれない部分の代替手段として、スクロールイベントにthrottleを使えます。スクロールが決まった時間間隔で発生するのがthrottleのメリットです。いったんユーザーがスクロールを始めたら、ちょうどよいタイミングで確実に実行されるのが望ましいことです。
この手法は、ユーザーがページ内の特定の場所にいるかどうかをチェックするのに役立ちます。ページのサイズが大きいと、コンテンツをスクロールしていくのに何秒もかかります。これを使って間引けば、任意の決まった時間間隔で1度だけイベントを発生できます。イベントを間引くことでスクロール体験をスムーズにし、しかも実行が確実です。
以下は素のJavaScriptで書かれた、イベントを間引く単純なコードです。
function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
この実装では変数timeを設定し、関数が最初に呼び出された時点をトラッキングしています。返される関数が呼ばれるたびにwaitの時間間隔が経過したかどうかをチェックし、経過していた場合コールバックを発生させてtimeをリセットします。
ライブラリーの使用
イベントを間引く方法には多くの危険があり、コードの自作はすすめられません。自前での実装よりも、サードパーティーのライブラリーを使った実装を推奨します。
lodash
lodashは、JavaScriptにおけるイベント制御のデファクトスタンダードです。このライブラリーはオープンソースなので、気軽にコードを探せます。幸いライブラリーはモジュール形式で、必要なものだけを取得できます。
lodashのthrottle関数を使えば、スクロールイベントの制御は次のようにシンプルになります。
window.addEventListener('scroll', _.throttle(callback, 1000));
瞬時になだれ込むスクロールイベントを、1000ミリ秒(1秒)に1回に制限しています。
このAPIで、次のようにleadingとtrailingのオプションも作成できます。
_.throttle(callback, 1, { trailing: true, leading: true });
このオプションによって、コールバックが立ち上がりと立ち下がりのどちらで実行されるかを決定します。
1つ押さえておきたいことは、「leading」と「trailing」をfalseに設定した場合、コールバックは発生しないということです。「leading」をtrueに設定すると、コールバックの実行が直ちに始まり、次いで間引き処理が実行されます。「leading」と「trailing」をどちらもtrueに設定すると、時間間隔を置いて確実に実行されます。
スクロールイベントを間引くデモをCodePenで確認してください。
このソースコードの興味深い点は、throttle()がdebounce()のラッパーであるということです。「throttle」がパラメーターの異なるセットを渡すだけで、希望の反応に変えられます。「throttle」でmaxWaitを設定すれば、設定した時間が経過すると確実に実行されるようになります。実装の残りの部分も同様になります。
今度イベントの制御に挑戦するときには、lodashのメリットを体感できるといいですね!
最後に
throttleかdebounceかを決めるカギを握るのは、解決すべき課題の性質です。この2つの手法は、別々のユースケースに向いています。ユーザー目線に立って課題を把握し選ぶことをおすすめます。
とんでもなく複雑なものをシンプルなAPIに抽象化できることが、lodashのメリットです。プロジェクトで_.throttle()を使うのに、必ずしもライブラリー全体を追加しなくても良いというのは朗報です。必要な関数だけでカスタムビルドを作成できるツール、lodash-cliがあります。イベントの間引き処理は、ライブラリー全体のほんのわずかな部分に過ぎません。
(原文:Quick Tip: How to Throttle Scroll Events)
[翻訳:新岡祐佳子]
[編集:Livit]