はじめに
この記事はZOZOテクノロジーズ #2 Advent Calendar 2019 9日目の記事になります。
Chrome76より実装されたloading属性と
loading=lazyが指定された場合のリソース読み込みタイミングについて紹介させていただきます。
loading属性について
<img src="image.png" loading="lazy" alt="" width="200" height="200">
サポートされている値は現在(2019/12/9時点)下記の通りです。
| 値 | 動作 |
|---|---|
| auto | ブラウザのデフォルトの遅延読み込み動作 (default) |
| lazy | viewportから計算された距離に達するまで、リソースの読み込みを延期 |
| eager | ページ上の場所に関係なくリソースをすぐにロード |
因みに、10x10より小さいインラインサイズを指定した要素は遅延されないらしい。
しきい値について
気になるのは loading=lazyを指定した場合のリソース読み込みタイミングですよね。
demoページをDevToolsで確認してみても
実際viewportHeightよりかなり下のimageまで読み込まれているのが確認できます。

Chrome Engineering ManagerのAddy Osmani氏の記事によると
Chrome’s lazy-loading implementation is based not just on how near the current scroll position is, but also the connection speed. The lazy frame and image loading distance-from-viewport thresholds for different connection speeds are hardcoded
つまり遅延読み込みのしきい値は、接続速度に基づいてハードコードされているとのことなので
chromiumの該当コードを確認してみます。
どうやら現在(2019/12/9時点)では下記のようにしきい値が定義されているようです。
| connection speed | image | iframe |
|---|---|---|
| 4G | 3000 | 4000 |
| 3G | 4000 | 5000 |
| 2G | 6000 | 6000 |
| slow 2g | 8000 | 8000 |
| offline | 8000 | 8000 |
| 不明 | 5000 | 6000 |
つまり
つまり、4Gの場合だと
imageがviewportHeightから3000px以内に
入ってきたタイミングで読み込みが開始される仕様になっています。
式で表すと下記のような感じでしょうか。
targetImageElement.getBoundingClientRect().top - window.innerHeight <= 3000
因みに
lazyImageLoadingDistanceはコマンドラインからオーバーライドできるようです。
canary --user-data-dir="$(mktemp -d)" --enable-features=LazyImageLoading --blink-settings=lazyImageLoadingDistanceThresholdPxUnknown=1,lazyImageLoadingDistanceThresholdPxOffline=1,lazyImageLoadingDistanceThresholdPxSlow2G=1,lazyImageLoadingDistanceThresholdPx2G=1,lazyImageLoadingDistanceThresholdPx3G=1,lazyImageLoadingDistanceThresholdPx4G=1 'https://mathiasbynens.be/demo/img-loading-lazy'
任意のしきい値を設定したい場合
結局、現状で任意のしきい値を設定したい場合は
IntersectionObserver等を使用して独自に実装する必要がありそうです。
<img class="lazy" src="placeholderImage.gif" data-src="originalImage.jpg" alt="">
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'))
const lazyImageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach( entry => {
if (entry.isIntersecting) {
const lazyImage = entry.target
lazyImage.src = lazyImage.dataset.src
lazyImage.classList.remove('lazy')
lazyImageObserver.unobserve(lazyImage)
}
})
}, { rootMargin: '100px 0px 100px 0px' }
)
lazyImages.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage)
})
