无限续命滚动:GSAP + Lenis 把”卡顿感”从网页里彻底删掉了

你刷那些 Awwwards 站的时候,有没有觉得页面滚起来特别”跟手”——不是原生那种一卡一卡的,而是像抹了油一样顺滑?这种体验不是玄学,背后是 GSAP + Lenis 这套组合在撑着。

最近 Codrops 发了一个用 React Three Fiber 做的 3D 无限滚动管效果,滚一下整个场景里的图像管、网格、玻璃罩全都在动,而且互相之间还有物理关联。看完源码我发现,这套方案的工程思路其实可以拆出来用到普通网页里。

为什么你的页面滚动就是”不顺”

原生滚动最大的问题是:每次滚动事件都是一跳一跳的。浏览器通知你”用户滚了一下”,然后页面直接跳到新位置。没有任何缓冲,没有任何惯性。

很多人会用 scroll-behavior: smooth,但这个 CSS 属性只能处理锚点跳转,不能给你的整个页面加惯性。Wheel 事件每触发一次,页面就跳一下,高端网站要的不是这个。

Lenis 就是来解决这个问题的。它把原生滚动的离散事件拦截下来,换成 RAF 驱动的连续插值——滚动的速度会被存起来,然后在每一帧里慢慢衰减,让页面”滑出去”而不是”跳过去”。

import Lenis from "lenis"

const lenis = new Lenis({
  wrapper: window,
  content: document.documentElement,
  smoothWheel: true,     // 鼠标滚轮也平滑
  lerp: 0.1,             // 惯性强度,越小越滑
  duration: 1.2,         // 惯性持续时间(秒)
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)) // ease out exp
})

// 接入 RAF 驱动循环
function raf(time) {
  lenis.raf(time)
  requestAnimationFrame(raf)
}
requestAnimationFrame(raf)

GSAP 接 Lenis:两件事一起干

光有 Lenis 只能让滚动顺滑,但网页里的动画元素还需要跟着滚动位置走。GSAP 的 ScrollTrigger 是干这个的,但它和 Lenis 的时间轴是独立的——直接一起用会有冲突,动画会抖。

解决方案是把 ScrollTrigger 绑到 Lenis 的 RAF 循环里,让两者共用同一个时间源:

const lenis = new Lenis()

// 关键:把 ScrollTrigger 的更新塞进 Lenis 的事件流
lenis.on("scroll", ScrollTrigger.update)

// GSAP ticker 改成用 Lenis 驱动
gsap.ticker.add((time) => {
  lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)

这样 GSAP 的所有 ScrollTrigger 动画都会以 Lenis 的滚动时间轴为准,不再有原生滚动事件带来的抖动。

实战:Codrops 这个 3D 管效果是怎么搭的

Codrops 发的这个 React Three Fiber 3D 无限滚动管,工程结构拆开看其实就三层。

第一层:统一运动信号

所有交互(滚动、移动鼠标、悬停)都写入同一个 refs 对象,不走 React 状态。每帧 useFrame 里只操作这些 refs:

const tubeScrollTarget = useRef(0)
const tubeSpinVelocity = useRef(0)
const rotationSpeedScaleTargetRef = useRef(1)

useFrame((_state, dt) => {
  // 滚动位置插值(lerp)
  scrollCurrent.current += (scrollTargetRef.current - scrollCurrent.current) * 0.12

  // 惯性衰减
  spinVelocityRef.current *= Math.pow(0.92, dt * 60)

  // 更新时间轴
  angle.current += (baseSpeedRef.current + spinVelocityRef.current) * scaledDt
})

这样滚动、鼠标、悬停都能同时影响动画,互不干扰。

第二层:GLSL 顶点着色器里的网格变形

背景那个随鼠标动的网格,不是用 Three.js 的变换矩阵做的,而是直接改顶点着色器里的 Y 坐标:

// 伪代码示意
float wave = sin(position.x * 10.0 + time) * mouseInfluence;
position.y += wave * amplitude;

平面几何细分了 512×512 段,所以变形看起来很平滑。这个思路在做”背景随鼠标呼吸”效果时特别有用,比监听 mousemove 改 CSS 性能好得多。

第三层:Lenis 接管无限循环

管内的图像是循环的,当一张图滚出视野,新的一张马上从对面接上。实现方式是维护一个”逻辑位置”,把这个位置对图像总数取余,然后用 Three.js 的 BufferAttribute 直接更新顶点缓冲,不需要重新创建几何体。

不用 React Three Fiber?普通网页也能用这套思路

如果你不做 3D,只想在普通网站里实现顺滑的无限滚动,核心代码其实很少:

<script type="module">
  import Lenis from "https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.34/dist/lenis.min.js"

  const lenis = new Lenis()

  // 给 GSAP ScrollTrigger 用
  lenis.on("scroll", ({ scroll, progress }) => {
    document.querySelector(".progress").style.transform = `scaleX(${progress})`
  })

  function raf(time) {
    lenis.raf(time)
    requestAnimationFrame(raf)
  }
  requestAnimationFrame(raf)
</script>

然后在 GSAP 的 ScrollTrigger 里正常写动画,两边就自动同步了:

gsap.registerPlugin(ScrollTrigger)

gsap.to(".hero-text", {
  y: -200,
  opacity: 0,
  scrollTrigger: {
    trigger: ".hero",
    start: "top top",
    end: "bottom top",
    scrub: 1
  }
})

2026 年了,CSS Scroll-Driven 能替代这套方案吗?

有人会说:Chrome 115 以后 CSS 出了 animation-timeline: scroll(),原生就能做滚动驱动动画了,还需要 GSAP + Lenis 吗?

能替代一部分,但替代不了全部。

CSS Scroll-Driven 擅长的是”绑定在某个元素上的简单动画”,比如进度条、背景色随滚动变化。但以下几个场景它搞不定:

场景 CSS Scroll-Driven GSAP + Lenis
带惯性的平滑滚动
滚动速度影响动画速度
多个动画互相联动(滚动+鼠标+悬停)
3D 场景里的复杂物理
无限循环内容(管/轮播)

所以结论是:简单场景用 CSS 足矣,酷炫高端网站还是这套 GSAP + Lenis 更稳。

下一步:你自己跑一遍

  1. 起一个 Vite + React 项目,npm i lenis gsap @gsap/react
  2. 把上面的 Lenis + GSAP 集成代码粘进去
  3. 找一个 Codrops 的动效教程跟着做一遍
  4. 把 ScrollTrigger 的 scrub 参数从 1 改成 2、3,感受一下”滞后感”的区别

跑完你会发现,以前觉得”高大上”的无限滚动,其实核心逻辑就那么几行。

评论区

0 条评论

登录后可评论。