Scroll listeners fire hundreds of times per second and jank the page. IntersectionObserver watches visibility for free, off the main thread.
Reveal-on-scroll (the portfolio effect)
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
io.unobserve(entry.target); // animate once, stop watching
}
});
}, { threshold: 0.15 }); // fire at 15% visible
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));.reveal { opacity: 0; translate: 0 24px; transition: 0.5s ease; }
.reveal.visible { opacity: 1; translate: 0 0; }Infinite scroll
const sentinel = document.querySelector("#load-more-sentinel");
new IntersectionObserver(async ([entry]) => {
if (entry.isIntersecting) await loadNextPage();
}, { rootMargin: "400px" }).observe(sentinel); // start loading 400px earlyThe options
threshold: 0โ1โ how much must be visiblerootMarginโ expand/shrink the trigger zone (preloading!)- Images themselves: just use
loading="lazy"โ the observer is for everything else
Respect prefers-reduced-motion on the animations, always.