このエントリーは aratana (Advent Calendar 2017) 24日目のエントリーです。


はじめまして、BtoB開発グループ フロントエンドエンジニアの菊地1です。

昨日は、新卒の川原くんによる「8ヶ月間gitを触った今気づいた発見や学び」というエントリーでした。つい数ヶ月前にGitでテンパって相談に来たのを思い出しました。失敗を糧にして、Gitマスターになりつつあるようで安心しました!

さて今日は、 クリスマス・イヴ という事で「ウェブサイト上におけるパフォーマンスを意識した要素検出」について、手法などを紹介したいと思います:sparkles::santa_tone1::christmas_tree::sparkles:

背景

今期は、担当案件にてパフォーマンスを意識する機会が多くありました。

色々と行った取り組みの中に、ファーストビュー表示の高速化のため「ビューポート内に存在しないコンテンツを非同期で読み込みする」というものがありました。これは、ページスクロールの過程で対象の要素が画面に表示された際に対応したコンテンツに必要な情報をAjaxで取得してコンテンツを非同期的に構築していくというものです。

コンテンツの非同期的な読込例としては、オートページャーやTwitterのタイムラインなどを想像して頂けると理解しやすいかと思います。その他、画像の遅延読み込みなどでこういった手法がよく使われています。

一部、今回の取り組みも交えて、要素監視について手法を紹介したいと思います。

よくある事例

以下のような要素検知処理はよく見かけるかと思います。

事例1
$(window).on('scroll.target-show', function () {
  var targetTop = $('#target').offset().top;
  var scrollTop = $(window).scrollTop();
  var windowHeight = $(window).height();

  if (scrollTop > (targetTop - windowHeight)) {
    // 処理
    // ...

    // イベント解除
    $(window).off('scroll.target-show');
  }
});

※jQueryオブジェクトのキャッシュなどは割愛します

パフォーマンスを意識した場合、上記のコードはどうでしょうか。上記の例では、要素が検知できた際にイベント解除しているとはいえ、それでもスクロールイベント毎に処理が必要以上に走ってしまっており、パフォーマンスはよくありません。

宗教上の理由でどうしてもスクロールイベントを使って実装せざるを得ない場合は、負荷を下げるために後述のように間引き処理を行いながら使うと良いです。

スクロール処理を間引く

色々と手法はありますが、もし既にプロダクトで lodashを使用しているのであれば、以下のような使用方法で簡単にスクロール処理を間引くことが出来ます。

lodash.throttle
_.throttle(func, [wait=0], [options={}])
事例2
import { throttle } from 'lodash';

$(window).on('scroll.target-show', throttle(function () {
  var targetTop = $('#target').offset().top;
  var scrollTop = $(window).scrollTop();
  var windowHeight = $(window).height();

  if (scrollTop > (targetTop - windowHeight)) {
    // 処理
    // ...

    // イベント解除
    $(window).off('scroll.target-show');
  }
}, 250));

スクロールイベントはスクロール時に大量にイベントを発火させますが、上記のように実装するだけで250ミリ秒に1回だけしか当該処理が呼び出されなくなるため、多少はパフォーマンスの低下を抑制できます。
また、lodashのような外部スクリプトを使わずにsetTimeoutrequestAnimationFrameを使って自前で処理を間引くことも可能です。

ベストプラクティス

スクロールイベントは実行回数が非常に多いです。多用すると以下のような問題点があります。

  • Scroll Jank を引き起こす可能性がある (preventDefault()が呼ばれている場合等)
  • サイズや位置を取得する場合 Forced Synchronous Layout が発生する可能性がある
    参考 - What forces layout / reflow

これまで紹介したスクロールイベントを使った要素検知ではoffset()scrollTop()などを実行していました。これらの処理は、対象DOMの位置を取得するため同期的にLayout計算が発生します。大量のイベントが発火してしまうスクロールイベント内で、これらの処理を行うとスクロールを阻害してしまいますし、その都度計算処理を行わせるためパフォーマンスも良くありません。

Intersection Observer を使う

Intersection ObserverというAPIを使えば、スクロールイベントを使用せず指定要素を監視する事ができます。

ここ最近は利用可能なブラウザも増え、Intersection Observerの利用も多くなってきていると思います。(IE/Safariは少し忘れておいてください…)

caniuse_intersectionobserver

引用: https://caniuse.com/#feat=intersectionobserver

実装例

これまでの実例をIntersection Observerを使って実装すると、ざっくり以下のような形で実装が可能です。

var clientHeight = document.documentElement.clientHeight;

var observer = new IntersectionObserver(function(changes) {
  for (let change of changes) {
    var rect = change.target.getBoundingClientRect();
    var isShow = (0 < rect.top && rect.top < clientHeight) || (0 < rect.bottom && rect.bottom < clientHeight) || (0 > rect.top && rect.bottom > clientHeight);

    if (isShow) {
      // 処理
      // ...

      // イベント解除
      observer.unobserve(change.target);
    }
  }
});

// 対象の要素を監視対象にする(NodeListの場合はループで都度渡す)
var target = document.querySelector('#target');
observer.observe(target);

これで指定要素がビューポートに現れたときに処理を実行することができます。
さらにオプションでthreshold,rootMarginなど監視条件を細かく設定できますが割愛です。

サンプル

Internet Explorer と Safari の対応は?

そろそろ IE と Safari を思い出してください。

現状、これらのブラウザでIntersection Observerはサポートされていません。これらのブラウザでIntersection Observerを利用する場合は、Polyfillが必要となります。

例えば intersection-observer-polyfill を利用した場合は、下記の記述でモダンブラウザと同様にIntersection Observerを利用することができます。

es6
import IntersectionObserver from 'intersection-observer-polyfill';
global
<script src="intersection-observer-polyfill/dist/IntersectionObserver.js"></script>
<script>
    (function () {
        var observer = new IntersectionObserver(function () {});
    })();
</script>

レガシーブラウザでも同じ動きを同じ記述でさせたい場合はPolyfillを使うと便利です。

担当案件の導入事例

結論から述べると、今回担当案件では、Intersection Observerの導入はできませんでした。

理由としては、以下の通りです。

  1. プロダクトの Internet Explorer/Safari 利用シェアが全体の7割を占めている (スマホ含む)
  2. 影響範囲の少ないようにしたい

案件のシェア率を鑑みるとPolyfillへ流す割合の方が大きくなってしまいます。また、Internet Explorer と Safari の場合、確認する環境が Windows/macOS/iOS と複数環境にまたがってしまうため、テスト工数が通常よりも多くなります。今回のスケジュールはタイトであり、出来るだけ影響範囲を少なくしたかったので、Polyfill (Intersection Observer)の導入は今回は断念しました。

どうしたのか

Intersection Observerが使えないため、代替ライブラリを導入することにしました。

「サイズが小さく、依存関係が少なくて、幅広いブラウザに対応している」ことを条件として、色々と試した結果、in-view.jsというライブラリを導入することにしました。コンテンツの非同期読み込みの足がかりとして、まずは in-view.js を利用することにしました。

この in-view.js は内部的にはIntersection Observerは使わず、Mutation Observerを使って要素監視をしているようです。先述で紹介したのと同じようにthrottleで処理を間引きながら、windowload/resize/scrollイベントで100ミリ秒に1回の頻度で監視しているようでした。

下記のようにシンプルに要素監視のイベントを記述する事ができます。

in-view.jsを利用した場合
inView('#target').once('enter', doSomething);

Intersection Observerに比べてパフォーマンス面で少々不安は残りましたが、リリース後も大きな問題などは発生していません。今後、パフォーマンスの定期的な監視とブラウザのシェア状況などを考慮してIntersection Observerの利用を改めて考えていきたいと思っています。

おわり

スクロールイベントは慎重に使っていきましょう。もしこれから新規案件などで要素監視を行うのであれば、Intersection Observerを使うのがベストプラクティスだと思います。

レガシーなプロダクトでも少しずつモダンな実装を取り込み、プロダクトの新陳代謝を良くする事は重要です。パフォーマンス改善はもちろんですが、保守開発をしやすくする開発環境を構築していきましょう:sparkles::santa_tone1::christmas_tree::sparkles:

参考サイト


aratana (Advent Calendar 2017) の大トリは、空前絶後の超絶怒涛のフロントエンドエンジニア、WordPressを愛しWordPressに愛された男、アラタナコーポレートサイトの生みの親、高見さん(@miiitaka)の記事です。

先日、高見さんはコーポレートサイトのリニューアルの実装をお1人で完遂されており、エントリーもそれに合わせた内容(オフィシャルサイトリニューアル - 技術まとめ)となっているようです。私もどういった技術が使われているのか全く知らないため内容が楽しみです!


  1. Uncaught ReferenceError: 菊 is not defined