90% 浏览器都支持了,你的懒加载还在手写 IntersectionObserver

全球约 90% 的浏览器已经支持 CSS scroll-driven animations,但绝大多数项目的懒加载和入场动画还在手写 IntersectionObserver——observe、回调里 classList.add、CSS transition,三件套缺一个就炸。其实 view() 加 animation-range 这两行 CSS 就能把整件事干完。

IntersectionObserver 能做到的事,CSS 现在都能做

IntersectionObserver 做的事情本质上就三件:监听元素进入视口、监听元素离开视口、拿到元素在视口中的比例。CSS scroll-driven animations 的 view() timeline + animation-range 组合把这三件事全包了:

/* 入场动画:元素进入视口 0%~40% 时播放 */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}
.reveal {
  animation: fade-up linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

/* 离场动画:元素离开视口时播放 */
@keyframes slide-out {
  to { opacity: 0; transform: translateY(32px); }
}
.exit {
  animation: slide-out linear both;
  animation-timeline: view();
  animation-range: exit 0% cover 20%;
}

animation-range 是关键参数,它决定了 keyframes 的 0% 到 100% 分别映射到滚动进度的哪个区间。entry/cover/exit 是三个基准线:

  • entry 0%:元素刚进入视口那一刻
  • cover 40%:元素覆盖视口 40% 的那一刻
  • exit 0%:元素刚开始离开视口的那一刻

有了这些精确的锚点,进度条、目录高亮、parallax 效果全都可以用纯 CSS 写。

比 rAF scroll listener 强在哪

以前写滚动效果的标准做法是监听 scroll 事件然后用 requestAnimationFrame:

// 旧写法:主线程上跑,每帧都要算
const observer = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) e.target.classList.add('visible');
  });
}, { threshold: 0.4 });
observer.observe(el);

CSS scroll-driven animations 跑在浏览器的合成线程上,不占主线程。即使用户狂滚 60fps,动画也不会卡,因为进度值由浏览器直接计算,不经过 JS 回调。这意味着:

  1. 不需要 will-change: transform 来提示浏览器
  2. 不需要 passive: true 来声明不 preventDefault
  3. 不需要 disconnect() 来清理 observer

浏览器支持情况:约 90%

Chrome 115+(2023 年 7 月)、Edge 115+、Safari 18+(2025 年 9 月)已全部稳定。Firefox 126+ 稳定版也已支持,Firefox Nightly 默认开启。全球约 90% 覆盖,生产环境可以直接用:

/* 渐进增强写法 */
.reveal {
  opacity: 0;
  transform: translateY(24px);
}
@supports (animation-timeline: view()) {
  .reveal {
    animation: fade-up linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }
}

不支持的浏览器会看到 opacity: 0 的初始状态,内容仍然可见,只是没有动画效果。配合 prefers-reduced-motion 媒体查询使用:

@media (prefers-reduced-motion: reduce) {
  .reveal {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

下一步

下次做懒加载或入场动画时,先写 @supports (animation-timeline: view()) 里的 CSS 方案,再在 @supports 外面写静态 fallback。只要设计允许静态展示,你就可以把整个 IntersectionObserver + classList 组合删掉。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 14 阅读