IntersectionObserver for everything loading=lazy does not cover

Native lazy loading covers images and iframes. Anything else that should happen when an element becomes visible still needs the observer — and it is the right tool.

const io = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;

    load(entry.target);
    io.unobserve(entry.target);
  }
}, { rootMargin: '200px' });

document.querySelectorAll('[data-defer]').forEach(el => io.observe(el));

Unobserving after the first hit is what stops the callback firing for the rest of the session, and omitting it is the most common mistake. rootMargin is what makes it feel instant rather than correct-but-late. The remaining uses are infinite scroll, deferred third-party embeds, and firing an analytics event when a section is actually seen — the last of which is the one that changes what a report means.