Enter animations are easy to overdo. The version that feels expensive is the one that animates layout. Keep the observer boring: add a class, let CSS move only transform and opacity.
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.classList.add("is-in");
io.unobserve(entry.target);
}
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
Unobserve after the first intersection. Stagger with a custom property, animation-delay: calc(var(--i, 0) * 80ms), and cap how many items you delay so a long list does not take a second to arrive.