Lenis 平滑滚动快速上手指南:3 行代码接入,原理、实战与排坑一次讲透
2026/9/19 8:54:16 网站建设 项目流程

Lenis 平滑滚动快速上手指南:3 行代码接入,原理、实战与排坑一次讲透

【免费下载链接】lenisSmooth scroll as it should be项目地址: https://gitcode.com/GitHub_Trending/le/lenis

做视差、WebGL 场景或长滚动叙事页面时,浏览器原生滚动很难用:手指一松滚动立刻停住,没有惯性;把位置喂给动画库,每帧拿到的是离散跳跃,画面发涩。Lenis 就是为这个痛点写的:一个轻量、零运行时依赖的平滑滚动库,直接包装浏览器原生滚动,让position: sticky、锚点链接和无障碍功能照常工作。下面按"接入 → 原理 → 实战 → 排坑 → 选型"的顺序,把它讲透。

3 行代码接入 Lenis:最短路径跑通平滑滚动

安装很简单,npm、yarn、pnpm 都可以:

npm i lenis

如果想读源码(推荐,核心代码量很小),clone 仓库:

git clone https://gitcode.com/GitHub_Trending/le/lenis

接入只需三步——建实例、驱动帧循环、引入官方 CSS:

import Lenis from 'lenis' import 'lenis/dist/lenis.css' // 官方推荐样式,对应 packages/core/lenis.css const lenis = new Lenis({ autoRaf: true }) // autoRaf 让库自己驱动 rAF 循环

如果不想让 Lenis 自管理帧循环(常见于 GSAP/Framer 项目),就关掉autoRaf,自己驱动:

const lenis = new Lenis() function raf(time) { lenis.raf(time) // 每帧推进内部动画,必须手动调用 requestAnimationFrame(raf) } requestAnimationFrame(raf)

packages/core/lenis.css 只有 20 多行,但每条都有用途:html.lenis恢复height: autolenis-stopped时用overflow: clip锁死滚动;带data-lenis-prevent*属性的元素自动overscroll-behavior: contain;平滑滚动期间把 iframe 的pointer-events关掉(iframe 不转发 wheel 事件)。不引这段 CSS,后续不少坑会自己找上门。

实例上有两个实例级事件可用:scroll(每帧滚动时回调,参数就是 Lenis 实例本身)和virtual-scroll(拿到归一化后的{deltaX, deltaY, event})。动画结束还会向 wrapper 派发原生scrollend事件,方便你监听"完全停止"。

原理拆解:虚拟滚动归一化 + 指数阻尼让滚动"滑"起来

Lenis 的数据流是一条单向链路,源码文件头部的注释写得很直白(见 packages/core/src/lenis.ts 前 20 行):监听 wheel/touch →preventDefault拦下原生滚动 → 归一化 delta 累加到targetScroll→ 动画逼近目标 → 每帧写回浏览器原生滚动。拆开看三个关键设计。

第一步:把不同设备的滚动量归一到同一尺度。滚轮事件的deltaMode有像素、行、页三种单位,Mac 触控板、Windows 鼠标、Firefox 的取值习惯还不一样。packages/core/src/virtual-scroll.ts 里做了换算:

// packages/core/src/virtual-scroll.ts if (deltaMode === 1) return LINE_HEIGHT // 行模式:按 100/6 px 一行换算 if (deltaMode === 2) return size // 页模式:按视口尺寸换算

再乘上wheelMultiplier/touchMultiplier(默认都是 1)。也就是说你拿到的永远是"像素",跨设备手感一致。触摸端还有一个细节:touchend时并不直接归零,而是用松手瞬间的速度按|velocity| ** touchInertiaExponent(默认 1.7)放大成一次惯性冲量——这就是"甩一下、滑一段"的来源。

第二步:指数阻尼逼近目标,而不是匀速移动。滚动动画由 packages/core/src/animate.ts 驱动,默认走 lerp 模式(lerp默认 0.1),核心是一个帧率无关的阻尼函数:

// packages/core/src/maths.ts export function damp(x, y, lambda, deltaTime) { return lerp(x, y, 1 - Math.exp(-lambda * deltaTime)) }

注意这个比例1 - exp(-λ·dt)乘的是"剩余距离"——等价于推一个带阻尼的物体:每帧走完剩余路程的固定比例,离目标越远走得越快、越近走得越慢,减速过程是天然指数衰减的,不需要额外写刹车逻辑。而exp(-λ·dt)里的dt让 60Hz 和 120Hz 设备收敛速度相同,不会出现高刷屏"飘"、低刷屏"肉"。你也可以改走时间模式:给duration(秒,默认 1.2)加easing函数(默认Math.min(1, 1.001 - 2 ** (-10 * t)),即 easeOutExpo),两种模式二选一,代码里会自动切换。

第三步:把动画值写回原生滚动。每帧 Lenis 用scrollTo({ top: value, behavior: 'instant' })更新浏览器滚动位置——behavior: 'instant'是为了绕开 CSSscroll-behavior的干扰,保证帧内位置精确。正因如此,滚动条、sticky、锚点跳转、浏览器历史记录这些"原生能力"全部保留,这也是它和"劫持 transform 移动内容"的旧方案本质不同的地方。

动画期间,实例上的velocitydirectionprogressisScrolling'smooth'/'native')持续更新;非动画期间则监听原生scroll事件反向同步(覆盖拖滚动条、键盘等输入),速度 400ms 未变化后置零。根元素上会挂lenis-smoothlenis-stopped这类状态类名,CSS 侧可以直接响应。

实战:把 Lenis 接入 GSAP ScrollTrigger 与章节吸附

最常见的落点是把 Lenis 的时间轴交给 GSAP,让ScrollTrigger与平滑滚动同一时钟:

import { gsap } from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' const lenis = new Lenis() lenis.on('scroll', ScrollTrigger.update) // 每帧通知 ScrollTrigger 重算 gsap.ticker.add((time) => { lenis.raf(time * 1000) // GSAP ticker 单位是秒,转毫秒喂给 Lenis }) gsap.ticker.lagSmoothing(0) // 关掉滞后平滑,避免双重缓冲

要点只有一个:Lenis 自己不再跑autoRaf,帧循环统一由 GSAP 的 ticker 驱动,两边才不会打架。纯滚动驱动的视差就更简单,直接读progress(0~1):

lenis.on('scroll', (e) => { hero.style.transform = `translateY(${e.progress * -60}px)` })

想要"整屏吸附",别去碰 CSS scroll-snap(Lenis 不支持),用官方 snap 插件:

import Snap from 'lenis/snap' const snap = new Snap(lenis) snap.addElements(document.querySelectorAll('.section'), { align: 'center' }) // snap 提供 next() / previous() / goTo(index) 供按钮触发

type支持proximity(默认)、mandatorylock,参数详见 packages/snap/README.md。做新手引导这类"强制看完再放行"的流程,则用scrollTolock

lenis.scrollTo('#feature-1', { offset: 80, // 相当于 scroll-padding-top lock: true, // 到达目标前禁止用户自由滚动 })

框架项目里有现成适配层:React 用<ReactLenis root />+useLenis(callback)(见 packages/react/README.md),Vue 用VueLenis组件 +useLenis,Nuxt 只需在nuxt.config里加modules: ['lenis/nuxt'](见 packages/vue/README.md)。

集成排坑:嵌套滚动、锚点、触摸同步的 4 个高频问题

坑 1:模态框、侧边栏里的嵌套滚动。最稳的做法是给嵌套滚动容器加属性,Lenis 检测到后直接放行原生滚动:

属性效果
data-lenis-prevent拦截该元素上所有平滑滚动事件
data-lenis-prevent-wheel只放行滚轮事件
data-lenis-prevent-touch只放行触摸事件
data-lenis-prevent-vertical只放行垂直方向
data-lenis-prevent-horizontal只放行水平方向

也可以用prevent: (node) => node.id === 'modal'回调按逻辑判断。allowNestedScroll: true虽然能自动识别嵌套容器,但官方明确警告它"每次滚动事件都要检查 DOM 树",大页面有性能代价,优先用上面的属性方案。

坑 2:锚点失效。默认情况下 Lenis 会接管滚动导致锚点跳不过去,开anchors: true即可(也可传ScrollToOptions定制offset/onComplete)。hash 含特殊字符的场景(如#footnote-†)源码里已用decodeURIComponent处理,可用 playground 里的对应用例验证。

坑 3:移动端触摸。默认 Lenis 在触摸端走原生滚动(不干扰系统手势),要"桌面级惯性手感"需开syncTouch: true,配合syncTouchLerp(默认 0.075)和touchInertiaExponent(默认 1.7)调节甩动强度;注意官方标注 iOS 16 以下该功能可能不稳定。一个容易困惑的设计:syncTouch下点按(零位移的 touchstart)会主动reset()停止惯性,这是刻意的 tap-to-stop,不是 bug。infinite: true的无限滚动在触摸设备上同样要求开syncTouch

坑 4:忘了驱动帧循环。这是 README troubleshooting 里排第一的问题——不传autoRaf也不手动调lenis.raf(time),页面就是完全静止的。其余已知限制建议直接记住:Safari 的 rAF 上限 60fps(省电模式 30fps);iframe 内滚动无法平滑(wheel 事件不转发);CSS scroll-snap 不可用,请用 snap 插件;naiveDimensions会牺牲性能,非必要不开。

选型边界与仓库地图:Lenis 适合谁,资源在哪

先说边界。Lenis 的价值集中在"滚动驱动一切"的场景:WebGL 场景同步、GSAP 时间线、视差、横向滚动(orientation: 'horizontal')、无限滚动(infinite,配合modulo取模实现回环)。如果只是一个静态内容站,原生滚动已经够用,引入 Lenis 反而要接受 Safari 帧率上限、iframe 失效这些约束——选型时掂量一下收益。

仓库是 monorepo,各包职责清晰:

路径内容
packages/core/Lenis 本体:lenis.ts主类、animate.ts动画、virtual-scroll.ts输入归一化
packages/react/ReactLenis组件与useLenishook
packages/vue/VueLenis组件、useLenis组合式 API 及 Nuxt 模块
packages/snap/滚动吸附插件
playground/core / horizontal / infinite / snap 等场景的可运行 demo

延伸阅读:整体 API 速查(Settings / Properties / Methods 三张表)见 README.md;设计动机在 MANIFESTO.md,想参与贡献看 CONTRIBUTING.md。把"输入归一化 → 阻尼动画 → 写回原生滚动"这条链路吃透之后,剩下的就是按场景调lerpduration和事件回调的事了。

【免费下载链接】lenisSmooth scroll as it should be项目地址: https://gitcode.com/GitHub_Trending/le/lenis

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询