配了三年IntersectionObserver,今天才发现它早就可以被浏览器自己替换了——animation-trigger把这件事彻底变了

写滚动入场动画,你大概也是这么干的:写一个 IntersectionObserver,监听元素进入视口,然后调 element.animate() 播放动画,最后 unobserve 防止重复触发。这套写法跑了快十年,没人觉得有问题。

Chrome 146 把这件事彻底变了。

问题的根子在哪

IntersectionObserver 本身不占主线程,但它触发的是一段 JavaScript,这段 JS 里调 element.animate() 是 Web Animations API,每次执行都要从浏览器渲染管道里绕一圈。对于滚动这种高频事件,”触发了然后做什么”才是真正开销所在。

更关键的是:IntersectionObserver 的语义是”观测”,它只告诉你”进了还是出了”,动画播放逻辑完全是 JS 自己兜着的。同一个动画效果,两拨人可能写出完全不同的 observer 代码,维护成本高,还容易出 bug。

animation-trigger 是什么

Chrome 146 新引进的 CSS 属性,叫 Scroll-Triggered Animations(滚动触发动画),和已有的 Scroll-Driven Animations 是两码事。

Scroll-Driven: 动画进度和滚动位置完全绑定,滚到哪动画就到哪,滚回去就倒回来,没有”播放完”这个概念。

Scroll-Triggered: 滚动到某个阈值时触发一次,然后这个动画按自己的时长播完,不管后面滚到哪。触发一次,播一次,跟 IntersectionObserver 加 element.animate() 的语义完全一样,但是纯 CSS。

语法长这样:

/* 先定义触发器,命名随便起 */
.trigger-target {
  timeline-trigger-name: --reveal;
  timeline-trigger-source: view();
  timeline-trigger: --reveal scroll() contain / cover;
}

/* 然后在要动画的元素上引用它 */
.reveal-card {
  animation: fade-up 0.6s ease-out both;
  animation-trigger: --reveal play-forwards;
}

@keyframes fade-up {
  from { opacity: 0; translate: 0 32px; }
  to { opacity: 1; translate: 0 0; }
}

11 行 JS 变成了 5 行 CSS。

一套语法,多种行为

animation-trigger 接受的动作关键词很丰富,不只是 play-forwards

  • play-forwards:进入触发,播完停住(最常用,等同 IntersectionObserver 场景)
  • play-backwards:离开触发,反向播完(适合”滑出去”动画)
  • play-forwards play-backwards:进播、离反,适合需要双向动效的场景
  • pauseresetreplay:手动控制动画状态

timeline-trigger 的范围写法也灵活:contain 是元素完全进入视口才触发,cover 是元素开始覆盖视口就触发,entry / exit 分别对应进和出的时刻。混搭可以做出非常精细的控制,比如”元素完全进入视口时播,离开时反向收”。

和 scroll-driven 怎么选

判断标准很简单:你要的是”滚动到哪里,动画就到哪里”(进度同步),还是”滚动到这里,动画开始播,播完就停”(状态触发)?

前者用 animation-timeline: scroll()animation-timeline: view(),对应 Scroll-Driven;后者用 timeline-trigger + animation-trigger,对应 Scroll-Triggered。

两者可以共存,不互斥。实际项目中,同一个列表可能同时用到了:视差背景用 Scroll-Driven(和滚动同步),卡片入场用 Scroll-Triggered(进页面播一次就够)。

@supports 渐进增强

目前只有 Chrome 146+ 支持,Safari 和 Firefox 暂无。生产使用要加降级:

.reveal-card {
  opacity: 1;
  transform: translateY(0);
}

@supports (animation-trigger: play-forwards) {
  .reveal-card {
    opacity: 0;
    transform: translateY(32px);
    animation: fade-up 0.6s ease-out both;
    animation-trigger: --reveal play-forwards;
    timeline-trigger-name: --reveal;
    timeline-trigger-source: view();
  }
}

不支持的浏览器直接显示最终状态,支持的浏览器播进入动画。功能不退化,只是没动效。

accessibility 注意

所有 CSS 动画都受 prefers-reduced-motion 控制,在媒体查询里加上就行:

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

下一步

如果你现在正在给项目里的卡片入场动画配 IntersectionObserver,可以停下来试试这个思路:先确认你的项目有没有 targeting Chrome 146+ 的条件(用 browserslist 或者 Canary 测试),有的话直接换 CSS,写完测试一下 Safari 和 Firefox 下的降级是否正常——如果不需要动效,只是”显示内容”,降级本身就该是最终状态。

旧代码不需要删,等浏览器支持跟上去了,IntersectionObserver 那套 observer 逻辑可以整体下掉。

核心证据来源:CSS-Tricks – animation-trigger (2026-08-26)modern-css.com – Scroll-Triggered Animationsdaily.dev – A First Look at Scroll-Triggered Animations

评论区

0 条评论

登录后可评论。

阿速·性能优化 12 阅读