◆ 画面内に要素が入ってる割合が閾値を超えたら関数を呼び出してくれる Observer
◆ 必要な画像のみロードさせてみる

IntersectionObserver は Observer のひとつで 「要素が画面内に入ったら関数を呼び出す」 ということが簡単にできるようになります

new IntersectionObserver(entries => {
console.log(entries)
}, {threshold: [.5]}).observe(document.querySelector("#elem"))

こう書くと #elem が画面に入ってる割合が 50% の閾値を超えた場合に console.log が実行されます
引数は配列で 閾値を超えたものが複数あれば複数のエントリが渡されます
閾値は 0 ~ 1 の間の数値を配列で複数設定可能です
閾値は上回る場合でも下回る場合でも その割合を超えると関数が実行されます
40% → 60% でも 55% → 30% でも実行されます

これを使えば 最初は img タグに src を設定しないでおいて 画面内に入ったときに設定してロードすればムダな通信が減らせます
ロードするファイルが多いのに 縦に長くて一番下まで行くことが少ないようなページだと効果があると思います

デモ

このページで試せます

img タグの data-src に URL を設定しておいて 25% を超えたらロードします
読み込みの遅延の違和感を減らすためにロードされたらフェードイン表示にします
フェードインに対応して 25% 以下になったらフェードアウトさせます

IntersectionObserver では 閾値を超えたタイミングで関数が呼び出されますが 閾値を上回ったのか下回ったのかのフラグはないです
entry.isIntersecting というそれっぽい名前のプロパティもありますが このプロパティは閾値を上回ったか下回ったかではなく 画面内に少しでも入っているかを表しています
全く見えてない状態なら false でちょっとでも見えていたら true です

閾値を上回ったか下回ったかを知りたいなら そのときのパーセント値 (entry.intersectionRatio) から判断することになります
今回は閾値がひとつなので それより上か下かをみるだけで判断できます

また サンプルに使えそうな画像ファイルがなかったので data-src の部分は動的に svg を作成してます

<!doctype html>
<meta charset="utf-8"/>
<style>
section{
width: 1000px;
margin: auto;
}
img{
min-width: 180px;
min-height: 180px;
max-width: 300px;
max-height: 300px;
opacity: 0;
}
</style>

<section id="imgs">
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
<img><img><img><img><img><img><img><img><img><img>
</section>

<script>
// initialize img's data-src (random svg)
function svgdummy(color, backcolor, text){
return `data:image/svg+xml,
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" fill="red">
<circle cx="100" cy="100" r="90" fill="${backcolor}" />
<text x="50%" y="50%" dy=".3em" text-anchor="middle" fill="${color}">${text}</text>
</svg>
`.replace(/\t|\n/g, "")
}
function randomSvg(){
const light = ["pink", "azure", "yellow", "lightgreen", "thistle", "beige", "bisque", "lavender", "whitesmoke", "gray", "lemonchiffon", "palegoldenrod"]
const shade = ["black", "red", "blue", "green", "orange", "purple", "navy", "mediumvioletred", "brown"]
return svgdummy(
shade[~~(Math.random()*shade.length)],
light[~~(Math.random()*light.length)],
Math.random().toString(36).substr(2, 4).toUpperCase(),
)
}
for(const img of document.images){
img.dataset.src = randomSvg()
}

// intersection observer
const thld = .25
const duration = 500
const io = new IntersectionObserver(entries => {
function fadeIn(elem){
elem.animate([
{opacity: 0},
{opacity: 1},
], {
duration,
fill: "both",
})
}
function fadeOut(elem){
elem.animate([
{opacity: 1},
{opacity: 0},
], {
duration,
fill: "both",
})
}
entries.forEach(e => {
const elem = e.target
const is_entering = e.intersectionRatio >= thld
if(is_entering){
if(elem.src === ""){
elem.src = elem.dataset.src
elem.onload = eve => {
fadeIn(elem)
elem.onload = null
}
}
if(!elem.onload){
fadeIn(elem)
}
}else{
if(!elem.onload){
fadeOut(elem)
}
}
})
}, {threshold: [thld]})
for(const img of document.images){
io.observe(img)
}
</script>