#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を使った手法で、画像遅延読み込みのサンプルを実装しました。
##従来の手法
<!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部分のみ記載します。
(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つのオプションがあります。
####コールバック引数
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);
}
}
####オプション
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 を用いた要素出現検出の最適化