Edited at

Intersection Observerを触ってみた

More than 1 year has passed since last update.


Intersection Observerとは

指定したDOM要素の交差点(Intersection)を監視するAPIです。


Intersection Observerのメリット

従来、DOM要素の位置計算や監視は、scrollイベントを使用し、scrollTopやrectTopなどで行っていました。

しかし、以下のような問題点があります。


  • Forced Synchronous Layoutが発生する(レイアウトの再計算、詳しくはこちら What forces layout / reflow

  • Scroll Jankの可能性

  • そもそもの実装が面倒

Intersection Observerを使用することで、位置の監視をブラウザが行ってくれるため、パフォーマンスの改善につながります。


サンプル

従来の手法とIntersection Observer APIを使った手法で、画像遅延読み込みのサンプルを実装しました。


従来の手法


none_observer.html


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title></title>
<style>
.image {
width: 400px;
padding: 10px;
background: #efefef;
margin: 0 auto 20px auto;
}
.image img {
display: block;
}
</style>
</head>
<body>
<div class="image" data-image="image01.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image02.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image03.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image04.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image05.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image06.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image07.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image08.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image09.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image10.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image11.jpg">
<img src="" alt="" width="400" height="284">
</div>
<div class="image" data-image="image12.jpg">
<img src="" alt="" width="400" height="284">
</div>
<script>
(function() {

const ROOT_MARGIN = 200;

const images = document.querySelectorAll(".image");

// イベントの間引き
const throttle = (func, wait)=> {
let time = Date.now();
return ()=> {
if ((time + wait - Date.now()) < 0) {
func();
time = Date.now();
}
}
}

class LazyLoad {
constructor(image) {
this.isLoaded = false;

this.windowHeight = window.innerHeight;

this.image = image;
this.rect = this.image.getBoundingClientRect();

this.onScroll = this.onScroll.bind(this);
}
addImage() {
const imagePath = "img/" + this.image.getAttribute("data-image");
this.image.children[0].setAttribute("src", imagePath);
}
checkImageLoad() {
let point = this.rect.top - ROOT_MARGIN;
if(this.windowHeight > point) {
this.addImage();
this.isLoaded = true;
}
}
onScroll() {
if(this.isLoaded) return false;
this.rect = this.image.getBoundingClientRect(); //スクロールの度に画面全体の再レイアウトが発生
this.checkImageLoad();
}
addEventListener() {
window.addEventListener("scroll", throttle(this.onScroll, 100), {passive: true});
}
init() {
this.checkImageLoad();
this.addEventListener();
}
}

Array.from(images).map(image => {
const lazyLoad = new LazyLoad(image);
lazyLoad.init();
});

}());

</script>
</body>
</html>


scrollイベントで監視を行い、対象のDOMがviewportの中に入ったらcallbackを実行しています。scrollの度に位置を取得しているため、画面全体のレイアウト(リフロー)が発生しパフォーマンスに影響を与えます。


Intersection Observerでの手法

html/cssは上記のソースコードと同じため、Javascript部分のみ記載します。


observer.js

(function() {

const ROOT_MARGIN = '200px 0px';

const images = document.querySelectorAll(".image");

const intersectionChanged = (entries) => {
for (let entry of entries) {
let scopeElm = entry.target;
let imagePath = "img/" + scopeElm.getAttribute("data-image");
if(entry.isIntersecting) {
scopeElm.children[0].setAttribute("src", imagePath);
}
}
}

class LazyLoad {
constructor(image) {
this.image = image;
}
init() {
let observer = new IntersectionObserver(intersectionChanged, {
root: null,
rootMargin : ROOT_MARGIN
});
observer.observe(this.image);
}
}

Array.from(images).map(image => {
let lazyLoad = new LazyLoad(image);
lazyLoad.init();
});

}());


従来の手法でいうscroll時の監視を、全てIntersectionObserverクラスが行ってくれます。


解説

Intersection Observerにはいくつかのコールバック引数と、3つのオプションがあります。


コールバック引数


observer_callback.js

const intersectionChanged = (entries) => {

for (let entry of entries) {

// 変更が起こったタイムスタンプ
console.log(entry.time);

// ルートのgetBoundingClientRect()
console.log(entry.rootBounds);

// ターゲットのgetBoundingClientRect()
console.log(entry.boundingClientRect);

// 交差領域のgetBoundingClientRect()
console.log(entry.intersectionRect);

// 交差している領域の割合
console.log(entry.intersectionRatio);

//交差しているかどうか
console.log(entry.isIntersecting);

// ターゲット
console.log(entry.target);

}
}



オプション


observer_option.js

let observer = new IntersectionObserver(intersectionChanged, {

// 交差対象のDOMの指定。初期値はdocument(viewport)。
root: document.getElementbyid('target'),

// entry.intersectionRatioを呼び出すタイミングを指定できる(下記は10%ごとに呼び出す)
threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],

// viewportと交差する何px前に呼び出したい場合や交差後何px後に呼び出したい場合に指定(下記は交差前200pxの部分でコールバック)
rootMargin: "200px"

});
observer.observe(target);



対応ブラウザは相当限られるので使用の際はご注意ください。

(17/5/22現在、Chromeのみ、Node.jsのローカルサーバーで確認)

以下の記事を参考にさせていただきました。

Intersection Observer を用いた要素出現検出の最適化