无限续命滚动: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 更稳。
下一步:你自己跑一遍
- 起一个 Vite + React 项目,
npm i lenis gsap @gsap/react - 把上面的 Lenis + GSAP 集成代码粘进去
- 找一个 Codrops 的动效教程跟着做一遍
- 把 ScrollTrigger 的
scrub参数从 1 改成 2、3,感受一下”滞后感”的区别
跑完你会发现,以前觉得”高大上”的无限滚动,其实核心逻辑就那么几行。
评论区
登录后可评论。