IntersectionObserver 是监听元素进入视口并添加动画类的最佳方案,通过 threshold: 0.1 触发、unobserve 防重播、animation-fill-mode: both 保状态,配合 animate-in 类与 keyframes 实现流畅不闪的单次动画。

用 IntersectionObserver 监听进入视口并添加动画类
纯 CSS 无法感知元素是否在视口内,必须靠 JS 触发状态类。最轻量、兼容性够用(Chrome 60+/Firefox 55+)的方案是 IntersectionObserver,它比监听 scroll 事件更高效、不卡顿。
核心逻辑:观察目标元素,当 isIntersecting === true 且尚未播放过动画时,给元素加一个如 animate-in 的类,CSS 里用该类定义 animation。
- 务必设置
threshold: 0.1(10% 进入即触发),避免用户快速滚动时错过回调 - 观察一次后调用
unobserve(),防止重复添加类或多次触发动画 - 动画需用
animation-fill-mode: both,否则动画结束后样式会回退到初始状态
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.classList.contains('animated')) {
entry.target.classList.add('animate-in', 'animated');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.js-animate-on-enter').forEach(el => {
observer.observe(el);
});
animate-in 类要怎么写才不闪、不重播
关键不是“加类”,而是加类后动画能干净地起效。常见翻车点:没设 animation-play-state 初始为 paused,导致页面加载时就动一次;或者没清掉内联 style 导致后续 JS 控制失效。
-
animation声明里不要写infinite或alternate,进一次就播一次 - 用
animation-duration和animation-timing-function显式控制节奏,别依赖浏览器默认 - 如果元素已有内联
style动画相关属性(比如style="animation: none"),会覆盖 class,得先清除
.animate-in {
animation: fadeInUp 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-play-state: running;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
为什么不用 scroll + getBoundingClientRect()
手动监听 scroll 在长列表或高频滚动下极易卡顿,尤其移动端。每次触发都要调用 getBoundingClientRect(),强制同步布局计算(Layout Thrashing)。
立即学习“前端免费学习笔记(深入)”;
-
IntersectionObserver是异步、浏览器原生优化的,不打断渲染流程 - 不需要防抖(
debounce)或节流(throttle),observer 自带性能兜底 - 旧版 iOS Safari(官方 polyfill,但注意 polyfill 会降级为
scroll回退方案
动画播完后想再进视口再播?去掉 unobserve 并加状态重置
默认只播一次是因为调用了 unobserve()。如果需求是「每次进入都播」,就得保留观察,并在元素离开视口时移除动画类(否则 animation 可能因 forwards 卡在终态,再进来也不重播)。
- 移除类时用
classList.remove('animate-in'),别用className = ''清空所有类 - 离开时不要立刻移除,加个
setTimeout延迟一帧(requestAnimationFrame更准),避免刚离开又回来造成闪烁 - 动画本身要用
animation-fill-mode: backwards或both配合重置逻辑,不然 DOM 状态和视觉不一致
真正容易被忽略的是:动画是否依赖父容器的 overflow: hidden 或 transform 层叠上下文——这些会截断 intersection 检测范围,导致 observer 根本收不到回调。调试时先检查目标元素的 offsetParent 和最近的非 visible 父容器样式。










