滚动动画别再只会 Intersection Observer 了,CSS Scroll-Driven Animations 已经能用了,我帮你捋清楚这俩到底该选哪个
滚动动画别再只会 Intersection Observer 了,CSS Scroll-Driven Animations 已经能用了,我帮你捋清楚这俩到底该选哪个
导语
做滚动动画这么多年,你一定写过无数次 scroll 事件监听 + requestAnimationFrame,也一定踩过主线程卡顿、元素闪入、移动端掉帧这些坑。现在 CSS 原生支持滚动驱动动画了,不是“未来可期”,是 Chrome 115+ 已经能用的真东西。这篇文章帮你把这套新能力和传统 Intersection Observer 方案放在一起比一比,看完你就知道什么时候该换。
正文
问题:JS 监听滚动的老问题,你肯定遇到过
最常见的滚动动画写法大概是这种:监听 scroll,判断元素位置,加 class 触发动画。看起来简单,但实际有几个硬伤。
第一,主线程压力。 scroll 事件在 Compositor 线程完成滚动后才异步传递到主线程,如果你在里面做 getBoundingClientRect()、加 class、触发重排,主线程一 busy 动画就开始掉帧。
第二,时间线不可控。 requestAnimationFrame 还是按时间跑的,和用户滚动进度没有天然绑定。你做的“视差”其实是近似,不是真正的滚动映射。
第三,移动端更明显。 低端机上滚动事件节流更严重,动画和滚动经常脱节,用户会感觉到“元素比手指慢半拍”。
这些问题的核心原因一样:我们把“动画时间线”绑在了时钟上,而不是滚动位置上。
方案:CSS Scroll-Driven Animations 的本质是换时间线
CSS Scroll-Driven Animations 做的事情很纯粹:把 animation-timeline 从默认的 auto(文档时间线)换成 scroll() 或 view(),动画进度就跟着滚动位置走了。
这里有两种时间线值得分开理解。
scroll():动画进度跟滚动容器/文档的滚动条位置走。适合进度条、视差、黏性导航这种“全局滚动映射”的场景。view():动画进度跟元素自己在视口中的可见程度走。适合卡片入场、章节淡入这种“元素可见性触发”的场景。
它们不是替代关系,而是覆盖了原来需要用 JS 做的两类滚动动画。
代码:从进度条到入场动画,都是几行 CSS
页面阅读进度条
@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); }}.reading-progress { position: fixed; top: 0; left: 0; width: 100%; height: 3px; background: linear-gradient(90deg, #6366f1, #a855f7); transform-origin: 0 50%; animation: grow-progress 1s linear forwards; animation-timeline: scroll(root);}
这段代码里最关键的就是 animation-timeline: scroll(root),它让动画进度和页面滚动位置 1:1 映射。不需要 JS 计算滚动百分比,不需要 requestAnimationFrame,浏览器直接在合成线程完成。
元素入场动画
@keyframes fade-in-up { from { opacity: 0; transform: translateY(60px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); }}.card { animation: fade-in-up 1s ease-out forwards; animation-timeline: view(); animation-range: entry 0% entry 100%;}
view() 让动画跟着元素进入视口走,animation-range 精确控制触发区间。原来这种效果通常要用 Intersection Observer + class 切换,现在纯 CSS 就能做。
视差背景
@keyframes parallax-bg { from { transform: translateY(0); } to { transform: translateY(-200px); }}.hero-bg { animation: parallax-bg 1s linear forwards; animation-timeline: scroll(root);}
简单直接,滚动多少,背景就移动多少,真正的滚动映射,不是近似。
对比:什么时候用 CSS Scroll-Driven,什么时候保留 Intersection Observer
先说结论:如果动画本质是“跟着滚动位置走”,优先用 CSS;如果逻辑依赖“元素是否在视口内”且和滚动位置无关,继续用 Intersection Observer。
| 阅读进度条 | CSS scroll() | 纯滚动映射,不需要 JS 判断 |
| 视差背景 | CSS scroll() | 动画进度 = 滚动进度 |
| 元素入场淡入 | CSS view() | 元素可见性已原生支持 |
| 滚动触发逻辑/埋点 | Intersection Observer | 需要回调,不是动画 |
| 懒加载图片 | Intersection Observer | 需要 JS 动态插入资源 |
| 滚动到 N% 显示 CTA | 视复杂度而定 | 简单阈值用 CSS,复杂逻辑用 JS |
一个更实用的判断标准:如果你写完发现不需要 addEventListener,也不需要 callback,那大概率 CSS 就够了。
兼容性:别在生产环境裸奔
截至 2026 年中,Chrome 115+、Edge 115+ 已经完整支持。Safari 仍在推进中,Firefox 支持程度有限。如果你面向的是现代浏览器为主的站点,可以直接用;如果还有大量 Safari 用户,记得用 @supports 做降级。
@supports (animation-timeline: scroll()) { .progress-bar { animation-timeline: scroll(root); }}@supports not (animation-timeline: scroll()) { .progress-bar { /* 回退到 JS 方案,或简单隐藏 */ }}
这种写法让新能力渐进增强,老浏览器不会坏。
结论:不是“选哪个”,是“把动画还给 CSS”
Scroll-Driven Animations 和 Intersection Observer 不是竞争关系。前者解决的是“动画时间线归属”问题,后者解决的是“元素可见性感知”问题。原来我们把两件事都塞给了 JS,现在浏览器终于把其中一件接回去了。
我的判断标准很简单:如果这个效果是动画,优先看 CSS;如果这个效果是逻辑,继续用 Intersection Observer。 前端该把动画还给 CSS 了。
收尾
如果你之前还在用 scroll 事件做动画,建议挑一个不影响业务的页面先试 scroll() 和 view(),跑通后再往其他页面推。这是现在前端少数真正“少写 JS、性能更好”的新能力了。
评论区
登录后可评论。











