用ScrollTrigger打造丝滑动画体验的实战技巧

Tr° 慧娇 交互 阅读 2,485
赞 19 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线前最后做性能测试,一打开 Chrome DevTools 的 Performance 面板,直接傻眼。页面一滚动,帧率掉到 20 多,ScrollTrigger 触发的动画全在“抽搐”,用户稍微划两下手指,整个页面就开始卡顿,甚至浏览器标签页都快被干崩了。

用ScrollTrigger打造丝滑动画体验的实战技巧

我一开始以为是 GSAP 写得太复杂,动画太多了,结果关掉几个非关键动画后还是没起色。看来问题不在动画本身,而在触发机制上——ScrollTrigger 的监听方式太“暴力”了。

找到瘼颈了!

用 Performance 录了一段滚动过程,发现主线程里 scroll 事件回调密集得像下雨,每帧都在执行 ScrollTrigger 的 refreshgetBoundingClientRect 调用,而且有些元素的 offsetTop 还在动态变化,导致频繁重排(reflow),简直是性能杀手。

我还注意到 Lighthouse 报告里 Layout Shift 分数低得可怜,CLS 直接爆红。这说明不只是卡,用户体验也差。必须优化 ScrollTrigger 的触发频率和计算开销。

试了几种方案

第一个想到的是节流(throttle),但直接对 scroll 做节流会导致动画不跟手,尤其是快速滚动时,ScrollTrigger 判断不准位置,动画该触发的时候没触发。

第二个方案是改用 IntersectionObserver 监听元素进入视口,自己手动控制动画播放。这确实能降低 scroll 监听压力,但维护成本太高,原来一行 trigger: ".item" 就搞定的事,现在要写一堆 Observer 逻辑,还容易漏边界情况。

第三个方案是看看 GSAP 官方有没有啥建议。翻了文档和论坛,发现他们推荐用 scrollerProxyfastScrolling,结合 passive event listener 来减少阻塞。这让我意识到:问题可能出在事件监听的配置上,而不是 ScrollTrigger 本身。

核心优化:改监听方式 + 减少重排

最终方案是组合拳:

  • 给 ScrollTrigger 加 scrollerProxy,避免频繁读取 scrollTop
  • 启用 fastScrolling,让 GSAP 内部用更高效的更新策略
  • 确保容器使用 overflow: scroll 并开启硬件加速
  • 避免在滚动过程中修改 DOM 结构或触发布局重算

最关键的是第一点。原本我用的是默认的 window 滚动,但 ScrollTrigger 每次检测都要查 window 的 scroll 位置,这个值一查就可能触发 reflow。改成代理后,GSAP 通过一个缓存值来判断滚动位置,大幅减少了 Layout 计算次数。

// 优化前:直接监听 window 滚动
gsap.to(".box", {
  y: 100,
  scrollTrigger: {
    trigger: ".box",
    start: "top center",
    end: "bottom center",
    scrub: true
  }
});
// 优化后:加 scrollerProxy + fastScrolling
const scroller = document.querySelector(".scroll-container");

// 代理滚动容器
ScrollTrigger.scrollerProxy(scroller, {
  scrollTop(value) {
    return arguments.length
      ? scroller.scrollTop = value
      : scroller.scrollTop;
  },
  getBoundingClientRect() {
    return {
      top: 0,
      left: 0,
      width: window.innerWidth,
      height: window.innerHeight
    };
  }
});

// 启用 fastScrolling
ScrollTrigger.defaults({
  scroller: scroller,
  fastScrolling: true
});

gsap.to(".box", {
  y: 100,
  scrollTrigger: {
    trigger: ".box",
    start: "top center",
    end: "bottom center",
    scrub: true
  }
});

这里注意我踩过好几次坑:一开始忘了设置 ScrollTrigger.defaultsscroller,导致 proxy 没生效。后来才意识到每个 ScrollTrigger 实例都得指定对应的 scroller,否则还是走 window。

CSS 层面也得配合

别以为 JS 优化完就完了。如果滚动容器没开启硬件加速,或者有大面积 repaint,照样卡。我给容器加了以下样式:

.scroll-container {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* iOS 流畅滚动 */
  will-change: scroll-position;     /* 提前告知浏览器 */
  transform: translateZ(0);          /* 强制开启 GPU 加速 */
}

还有个细节:不要在滚动中频繁修改元素宽高或类名,这会触发重排。我把一些依赖滚动位置的 class 切换从 scroll 里移到了 animation complete 回调里。

优化后:流畅多了

改完再跑 Performance,吓一跳——帧率稳定在 55~60,scroll 事件回调从每秒几百次降到几十次,Lighthouse 的 Performance 分数从 42 干到了 89,CLS 也从 0.35 降到 0.05。

最直观的感受是:用户滑动时动画完全跟手,没有延迟或跳跃感。之前滑一半动画突然“蹦”出来的情况消失了。

性能数据对比

以下是优化前后关键指标对比:

  • 平均帧率:从 23 FPS → 58 FPS
  • 滚动事件处理耗时:从 ~16ms/次 → ~2ms/次
  • Lighthouse Performance 评分:42 → 89
  • 首次内容渲染(FCP):未变(本就不慢)
  • 累积布局偏移(CLS):0.35 → 0.05

虽然 FCP 没改善,但交互体验提升巨大。这才是重点。

还有些小细节

顺手做了几件事:

  • 把所有非首屏的 ScrollTrigger 加了 once: trueinvalidateOnRefresh: true,避免无效计算
  • 用了 markers: false 干掉调试标记(生产环境别留着)
  • 对长列表做了虚拟滚动,减少 DOM 节点数量

这些改动单独看影响不大,但合起来又省了约 10% 的 CPU 占用。

最后效果不是完美,但够用

改完后仍有极少数低端安卓机在快速滚动时轻微掉帧,但已经无伤大雅。这个方案不是理论最优,但开发成本低、稳定性高,适合我们这种赶上线 deadline 的项目。

折腾了整整两天,中间还因为 proxy 配置错来回刷新了几十次页面,心态差点崩。但看到最终数据,觉得值了。

以上是我的优化经验,有更优的实现方式欢迎评论区交流

这个技巧的拓展用法还有很多,比如结合 requestIdleCallback 延迟初始化非关键动画,后续可能会继续分享这类实战案例。如果你也在用 ScrollTrigger,不妨检查下有没有开启 fastScrollingscrollerProxy,说不定能救你一命。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
司徒景鑫
这篇文章帮我找到了技术学习的乐趣,让我不再觉得学习是一种负担。
点赞
2026-03-20 17:26