滚动触发动画应使用 IntersectionObserver 而非 window.onscroll,因其轻量、不阻塞主线程、支持懒加载与反向触发;需合理配置 threshold 和 rootMargin 控制触发时机,添加动画类后及时 unobserve 防止重复播放,并配合 CSS 初始态与硬件加速属性实现流畅效果。
滚动触发动画的核心不是“等滚动完成”,而是实时监听元素进入视口的瞬间,用 IntersectionObserver 替代手动计算 scrollTop 和 getBo —— 它更轻量、不阻塞主线程,且天然支持懒加载和反向触发。
undingClientRect()
window.onscroll 手动判断?手动监听 scroll 事件容易掉帧、抖动,尤其在移动端;每次触发都要调用 element.getBoundingClientRect(),频繁重排重绘;边界条件难处理(比如元素初始就在视口内、页面缩放、iframe 嵌套)。
用 IntersectionObserver 是标准解法,浏览器原生优化,兼容性已覆盖 Chrome 64+、Firefox 55+、Safari 12.1+、Edge 79+。
scroll 回调里反复调用 getBoundingClientRect()
setTimeout 节流——IntersectionObserver 自带防抖scroll + getBoundingClientRect,但要加 throttle 且只监听关键元素IntersectionObserver 的关键配置项触发时机由 threshold 和 rootMargin 共同决定,不是“一进就动”。比如想让动画在元素顶部到达视口底部时开始,就要调整这两个值。
threshold 是一个数组,表示目标元素可见面积占比(0–1),常见取值:[0](只要露一点)、[0.1](10% 可见)、[0.5, 1.0](两个触发点)。
rootMargin 类似 CSS 的 margin,可写成 "0px 0px -100px 0px",负值表示提前触发(上边距 -100px = 元素还有 100px 进入视口时就通知)。
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
},
{
threshold: [0.1],
rootMargin: '0px 0px -50px 0px' // 提前 50px 触发
}
);
默认情况下,元素滚出视口再滚回,isIntersecting 会再次为 true,导致动画重复播放。需要手动控制状态或使用 observer.unobserve()。
unobserve,确保只执行一次entry.intersectionRatio === 0 判断完全离开,再移除类animation-play-state 控制暂停——它不重置动画状态,可能卡在中间帧const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-fade-up');
observer.unobserve(entry.target); // 关键:只触发一次
}
});
});
JS 控制类名,CSS 负责表现。但很多动画失效是因为忽略了 transform 和 opacity 的硬件加速前提,或没设初始态。
opacity: 0; transform: translateY(20px);),否则加类瞬间无过渡transform 和 opacity,避免触发布局(height、left 等)will-change: transform 提前提示浏览器优化(仅对高频动画元素)prefers-reduced-motion,可用 @media (prefers-reduced-motion: reduce) 关闭动画真正难的不是让动画动起来,而是让它们在正确的时间、以正确的节奏、不干扰用户滚动体验地动起来——IntersectionObserver 的 rootMargin 和 threshold 组合,比想象中更敏感,微调 10px 或 0.05 就可能让动效“卡半拍”或“抢跑”。