TL;DR
- パフォーマンスの重要性をおさらい
- Scroll Jank とは
- 60fpsとリフレッシュレート
- スクロールイベントを最適化する
パフォーマンスの重要性をおさらい
検索エンジンと広告収益事例
GoogleとMicrosoftの検索エンジンチームによって2009年に発表された調査
レスポンスの遅延が招く収益への影響:
- 0.5秒遅延 → -1.2%
- 1.0秒遅延 → -2.8%
- 2.0秒遅延 → -4.3%
スクロールイベントは負荷が高い
フロントエンドの実装をしたことのある方は、スクロールイベントを使った実装をしたことも多いはず。
スクロールイベントは発生頻度が高いため、負荷が高くなりがちです。スクロールイベントのリスナーは適切に実装しないとScroll Jankの発生・UXの低下を招いてしまいます。
パフォーマンスを出来るだけ下げないような実装事例を紹介していきます。
Scroll Jank とは
ページのスクロールをした際に、スクロールが詰まったような遅延を
「Scroll Jank」と呼びます。
Scroll Jankが起きるパティーン
下記の2点をピックアップします。
- スクロールイベント内で
preventDefault()
が実行されている可能性がある - スクロールイベント内で要素のサイズや位置を取得する処理がある(Forced Synchronous Layout が発生する可能性がある)
スクロールイベント内でpreventDefault()
が実行されている可能性がある
可能性がある…?🤔
例えば、スクロールイベント内でpreventDefault()
が実行されたら、スクロール処理は中止されます。しかしながら、ブラウザ側はイベント内でpreventDefault()
が実行されるかどうかを事前に判定できません。(そのイベントが実行が終了するまで判定ができない)
そのため、イベント内の処理が終了するのを待つ(=遅延が発生する)ことになります。
いわゆるScroll Jank 発生原因のほとんどはコレです。
スクロールイベント内で要素のサイズや位置を取得する処理がある(Forced Synchronous Layout が発生する可能性がある)
Forced Synchronous Layoutはレイアウト計算処理で発生します。(「レイアウト処理」が実行されないと期待する値が分からない系の処理)
ただ、近年、こういった処理を実施しなければならない処理は、Intersection Observer や position: sticky
を使うことでスマートかつ良いパフォーマンスで実装できるため、減りつつある(はず)。
60fpsとリフレッシュレート
一般的な端末は画面を1秒間に60回リフレッシュする
→ 間隔は、1000ミリ秒 / 60 = およそ16.66ミリ秒
この間隔からズレるとフレームレートが低下して、画面上の描画がブレる(ジャンクが発生)
https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=ja
スクロールイベントを最適化する
スクロールイベント内でpreventDefault()
がされてない事をブラウザに教える
Event Listener Options passive
この指定で「処理がpreventDefault()
を実行していない」という事が明示できるようになった
→ イベント内の処理が終了を待たず判定が可能になった
指定方法
addEventListener
の第三引数に{passive: true}
を指定するだけ
document.addEventListener('scroll', function() {
// SUGOI SHORI
}, {passive: true});
互換性の問題
Internet Explorer「こんにちは」
https://caniuse.com/#feat=passive-event-listener
非対応ブラウザで同処理を実行すると、元々のuseCaptureがtrueになってしまう
一応、このpassiveが使えるかどうかの検出は可能
/* "passive" が使えるかどうかを検出 */
var passiveSupported = false;
try {
window.addEventListener("test", null, Object.defineProperty({}, "passive", { get: function() { passiveSupported = true; } }));
} catch(err) {}
/* リスナーを登録 */
var elem = document.getElementById('elem');
elem.addEventListener('touchmove', function listener() {
/* do something */
}, passiveSupported ? { passive: true } : false);
未対応ブラウザにOpera miniがあるが、IEのみ回避すれば良いのであれば、単純に下記のように書くことができる。
var elem = document.getElementById('elem');
elem.addEventListener('touchmove', function listener() {
/* do something */
}, !document.documentMode ? { passive: true } : false);
※document.documentMode
でIEかどうかを判定
jQueryでpassive
を使う方法は…?
ない
スクロールイベントの処理を間引く
setTimeout
間引き処理として、setTimeoutが広く利用されてきた。
次回の処理をスケジューリングし処理を頻繁に実行させないようにする事ができる。
- ブラウザ側の準備に関わらず必ず実行される
- タブが非アクティブ時でも実行される
var timer = null;
var FPS = 1000 / 60;
function func() {
clearTimeout(timer);
timer = setTimeout(function() {
// YABAI SHORI
}, FPS);
}
document.addEventListener('scroll', func, {passive: true});
※delayに渡す値を 1000 / 60 (16.66ms) にしている(60fps相当)
throttle
lodash や Underscore.js に throttle 関数がある
先述のsetTimeout
での実装と目的は同じ
import { throttle } from 'lodash';
const FPS = 1000 / 60;
document.addEventListener('scroll', throttle(func, FPS), {passive: true});
requestAnimationFrame
処理を待つように時間指定するのではなく、次のフレームのレンダリングが準備が整った時に呼び出される。
ほかの処理に割り込まれてフレームのレンダリングが遅延することなく適切なタイミングで呼び出される。
- ブラウザの画面リフレッシュと同じタイミングで呼び出される
- 画面が非アクティブ時には実行されない
- なにげにIE11でも使える
var ticking = false;
function func() {
if (!ticking) {
requestAnimationFrame(function() {
ticking = false;
// GREAT SHORI
});
ticking = true;
}
}
document.addEventListener('scroll', func, {passive: true});
デモ
See the Pen requestAnimationFrame vs throttle vs setTimeout by kikuchi hiroyuki (@kikuchi-hiroyuki) on CodePen.
setTimeout
だけ異常にもたつく