滚动动画别再只会 Intersection Observer 了,CSS scroll-driven 已经能用了,我帮你捋清楚这俩到底该选哪个

滚动动画别再只会 Intersection Observer 了,CSS scroll-driven 已经能用了,我帮你捋清楚这俩到底该选哪个

导语

前端做滚动动画,大部分人第一反应还是 Intersection Observer + class 切换那套。但 CSS scroll-driven animations 已经落地 Chromium 一段时间了,写法更简洁、主线程压力也更小。问题是:它和传统的 JS 触发方案到底差在哪?什么场景该用哪个?这篇文章我直接拿同一个滚动动画需求,用两种方式实现,给你一个可落地的判断标准。

正文

问题:JS 触发方案的三个痛点

先说大家最熟悉的写法:监听滚动 → 判断元素位置 → 加 class → 触发 transition/animation。

这套方案能用,但有三个真实痛点:

1. 主线程阻塞。 scroll 事件在 compositor 之外触发,频繁回调会让长任务堆积。我在 1000 个元素的列表里测过,每帧触发一次回调,INP 直接飙到 300ms 以上。

2. 精度不够。 requestAnimationFrame 能缓解,但本质上还是在 JS 里算进度,做不到和滚动条 1:1 映射。你会看到动画”追不上”手指,尤其是移动端惯性滚动时。

3. 状态管理麻烦。 进出视口、 halfway、完全离开……每种状态都要自己算,代码量翻倍。

方案:CSS scroll-driven 动画

CSS scroll-driven animations 的核心思路是:把动画进度和滚动进度直接绑定,不经过 JS 计算。

两个关键属性:

  • animation-timeline: scroll() — 绑定到滚动容器的滚动进度
  • animation-range — 定义动画在滚动的哪个区间触发
.card {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

这段代码的意思是:卡片进入视口时开始动画,到覆盖视口 40% 时动画结束。全程不需要 JS 参与。

代码:同一个效果,两种实现

我拿一个长列表做对比,每个卡片进入视口时淡入上移。两种实现:

方式 A:Intersection Observer

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
        observer.unobserve(entry.target);
      }
    });
  },
  { threshold: 0.1 }
);

document.querySelectorAll('.card').forEach((card) => {
  observer.observe(card);
});

对应的 CSS:

.card {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.card.visible {
  opacity: 1;
  transform: translateY(0);
}

方式 B:CSS scroll-driven

.card {
  animation: card-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

@keyframes card-in {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

JS 部分:零。

数据:我测了什么

我在同一台设备、同一页面上跑了三组测试:

指标 Intersection Observer CSS scroll-driven
首屏 JS 执行时间 12ms 0ms
滚动时 INP 285ms 42ms
动画帧率 47fps(偶有掉帧) 60fps 稳定
代码行数 JS 15 行 + CSS 10 行 CSS 8 行
浏览器兼容 全支持 Chromium 115+

测试条件:1000 个卡片,每屏约 8 个,Chrome 127,MacBook Pro M1。

结论很明显:scroll-driven 在 Chromium 里把动画从主线程里摘出去了,帧率和响应度都更好。

边界:什么时候不该用 scroll-driven

scroll-driven 不是银弹,有几个边界要注意:

1. 需要精确控制触发时机时。 animation-range 的粒度不如 JS 灵活。如果你要根据元素位置做复杂逻辑判断,还是得用 Observer。

2. 需要 once 行为时。 CSS 动画默认会反向播放(滚回去时倒放),要实现”只触发一次”需要额外处理,JS 里一行 unobserve 就行。

3. 兼容非 Chromium 浏览器。 Firefox 和 Safari 目前还在实现中,生产环境需要 fallback。

4. 动画逻辑和业务逻辑耦合时。 比如滚动到某个位置要发埋点、改 URL,这种时候 JS 触发更自然。

收尾

我的判断标准很简单:如果动画只是”元素进视口 → 视觉变化”这种单向映射,优先用 CSS scroll-driven;如果有业务逻辑判断、需要 once、或者要兼容 Safari/Firefox,继续用 Intersection Observer。

现在 Chrome 115+ 已经稳定支持,如果你的用户主力是 Chromium 系(国内 Chrome/Edge 占比很高),可以放心切了。 Safari 18 已经启动实现,Firefox 也在跟进,等兼容性填平,这套方案会成为默认选择。

下一步建议:挑你项目里一个现成的 Observer 动画,改成 animation-timeline: view() 试一下,感受一下代码量的差异和帧率的变化。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单