Lenis:轻量级平滑滚动库手册——3 个核心机制 + 4 个实战场景 + 配置速查
【免费下载链接】lenisSmooth scroll as it should be项目地址: https://gitcode.com/GitHub_Trending/le/lenis
Lenis 是一款轻量级、零依赖的平滑滚动库,仅数 KB 体积,它包裹浏览器原生滚动而非用 transform 伪造位移,因此 sticky 与锚点链接原样保留。适合需要做视差、WebGL 滚动同步、滚动驱动动画的前端开发者。读完你能完成初始化、调参,并接入 GSAP 与 React/Vue 生态。
快速上手:三步开启平滑滚动
安装npm i lenis,然后在入口文件里引入、初始化、加上推荐 CSS,共三步。
import Lenis from 'lenis' import 'lenis/dist/lenis.css' // 推荐 CSS,负责 stopped 态与嵌套容器的 overscroll 行为 // autoRaf: true 让实例内部自动跑 requestAnimationFrame 循环 // 不开启的话必须每帧手动调 lenis.raf(time) const lenis = new Lenis({ autoRaf: true }) // 位置变化的每一帧都会触发,progress / velocity 等属性直接读实例即可 lenis.on('scroll', (e) => { console.log(e.animatedScroll, e.progress) }) // 参考 README "Setup" 一节;实例定义见 packages/core/src/lenis.ts不想搭构建环境也有一行版:HTML 里引lenis.css和lenis.min.js,然后执行new Lenis({ autoRaf: true, autoToggle: true, anchors: true, allowNestedScroll: true, naiveDimensions: true, stopInertiaOnNavigate: true }),README 的 "No-code usage" 一节列出了这组"全功能"参数组合,能顺带处理模态框、锚点、页面切换时的滚动复位。想看源码可以直接 clone:git clone https://gitcode.com/GitHub_Trending/le/lenis。
核心机制拆解:轻量与顺滑背后的 3 个设计决策
包裹原生滚动,而不是用 transform 伪造
设计意图:多数平滑滚动库用"虚拟画布"方案——把页面塞进容器、用 transform 位移来模拟滚动。Lenis 反其道而行,真正调用浏览器滚动。packages/core/src/lenis.ts文件头的注释把链路写得明明白白:监听wheel事件并preventDefault阻止原生跳动 → 归一化 delta → 累加进targetScroll→ 平滑地把scrollTo动画到目标值;没有动画在跑时,则退化为监听原生scroll事件。
与传统做法的差异一目了然:
| 对比项 | Lenis(原生滚动方案) | 传统 transform 虚拟滚动 |
|---|---|---|
| 实现方式 | 真实scrollTo({ behavior: 'instant' })驱动 window | 对容器做 translate 位移 |
position: sticky/ fixed | 原样生效 | 会失效,需额外补偿 |
| 锚点链接、键盘可达性 | 原样保留 | 需手工重建 |
收益是 README 里 "Runs on native scroll" 那条特性:position: sticky、锚点链接和无障碍交互全部不牺牲。注意setScroll里特意用behavior: 'instant',就是为了绕过页面自己声明的scroll-behaviorCSS,避免双重平滑。
两种手感二选一:damp 插值与固定时长缓动
💡 Lenis 的"手感"由Animate类(packages/core/src/animate.ts)提供,它不是一种算法,而是两种:
// 参考 packages/core/src/animate.ts(advance 方法摘录) advance(deltaTime: number) { if (this.duration && this.easing) { // 模式一:时间驱动。按 duration 走完整条 easing 曲线 this.currentTime += deltaTime const linearProgress = clamp(0, this.currentTime / this.duration, 1) const easedProgress = linearProgress >= 1 ? 1 : this.easing(linearProgress) this.value = this.from + (this.to - this.from) * easedProgress } else if (this.lerp) { // 模式二:指数阻尼。damp 公式 1 - e^(-lambda·dt),帧率无关 this.value = damp(this.value, this.to, this.lerp * 60, deltaTime) } else { // 两者都没给:直接跳到终点 this.value = this.to } }差异点:传统库通常只给一组固定缓动曲线,帧率一波动手感就漂移。damp用1 - Math.exp(-lambda * dt)(packages/core/src/maths.ts)做到帧率无关,60Hz 和 120Hz 屏幕上衰减曲线一致,默认lerp: 0.1一个数就能调"滑多远";给duration + easing则走标准时间轴,默认缓动是Math.min(1, 1.001 - 2 ** (-10 * t))。收益:滚轮的"滑行感"更接近真实惯性,而不是匀速补间。
先归一化输入,再统一状态输出
不同硬件的滚轮 delta 量纲完全不同(像素、行、页面)。packages/core/src/virtual-scroll.ts把deltaMode统一换算:按行滚动的设备乘以LINE_HEIGHT = 100 / 6,按页滚动的乘以视口尺寸,再乘wheelMultiplier/touchMultiplier。触摸端还补了两处细节:touchend时按sign(delta) × |velocity|^1.7(touchInertiaExponent默认 1.7)模拟抬手后的惯性滑行;iOS 上如果手指落在文本选区手柄 40px 半径内(lenis.ts的isTouchOnSelectionHandle),会把事件让给系统去调整选区而不是滚动。
输出侧则把状态写成 CSS 类:lenis、lenis-smooth、lenis-stopped、lenis-locked,随状态自动增删。配套packages/core/lenis.css做了三件小事——html.lenis { height: auto }修正页面高度;给data-lenis-prevent元素加overscroll-behavior: contain防链式回弹;lenis-smooth期间给 iframe 加pointer-events: none,防止 iframe 吞掉 wheel 事件。另外,原生滚动停下 400ms 后velocity归零、isScrolling复位,动画结束后还会派发自定义scrollend事件,方便下游做"滚动停止"逻辑。
实战场景:几乎一定会遇到的 4 个需求
让嵌套容器保持原生滚动
场景:页面里有抽屉、模态框或横向卡片列表,落在它们上面滚动时,不希望外层页面跟着滚。
思路:两档粒度。粗粒度开allowNestedScroll,实例会自动检测可滚动子元素并放行原生滚动;细粒度用data-lenis-prevent属性(另有-wheel、-touch、-vertical、-horizontal变体)或prevent回调,在事件冒泡路径上精确豁免。
// 参考 README "Nested scroll" 一节 const lenis = new Lenis({ allowNestedScroll: true, // 粗粒度:自动识别嵌套可滚动元素 // 细粒度二选一:给元素加>// 参考 README "GSAP ScrollTrigger" 一节 const lenis = new Lenis() // 每帧把 Lenis 的滚动状态喂给 ScrollTrigger lenis.on('scroll', ScrollTrigger.update) // 用 GSAP 的 ticker 驱动 Lenis gsap.ticker.add((time) => { lenis.raf(time * 1000) // ticker 的时间单位是秒,转成毫秒 }) // 关闭 GSAP 的滞后平滑,否则快速滚动会累积延迟 gsap.ticker.lagSmoothing(0)坑:gsap.ticker回调的时间单位是秒,漏乘 1000 会让动画慢十倍;lagSmoothing(0)忘了关,快速滚动时动画与位置脱节,这是 README Troubleshooting 里专门点名的一条。
开启无限滚动模式
场景:品牌页、作品集的网格希望"滚到尽头回到开头"。
思路:一个开关infinite: true。开启后scroll取值器会用modulo(animatedScroll, limit)按回卷值输出,scrollTo的目标也会自动选"距离最近的方向"(超过limit / 2就往回绕)。
// 参考 playground/infinite/test.ts new Lenis({ infinite: true, // 循环滚动,scroll 取值器按 limit 自动取模 autoRaf: true, syncTouch: true, // README 注明:触摸设备开启 infinite 需要它 })坑:触摸设备上必须同时开syncTouch: true,否则惯性触摸会直接打断回卷逻辑。
搭建横向滚动区段
场景:产品画廊、步骤展示需要一段横向滚动的区域。
思路:不用全局实例,单独new Lenis({ wrapper: 容器, orientation: 'horizontal' })挂在具体容器上;scrollTo支持right、end等关键字。
坑:orientation设为horizontal时gestureOrientation默认自动变both(见lenis.ts构造参数默认值),横向纵向的 delta 都会进来;如果你的横向区段嵌在纵向页面里,注意用eventsTarget收窄监听范围,并配合overscroll控制边界处的行为。
配置速查:常用参数与调优点
核心参数(默认值均可在packages/core/src/types.ts的 JSDoc 中核对):
| 参数 | 默认值 | 作用 |
|---|---|---|
autoRaf | false | 实例内部自跑requestAnimationFrame循环;不开则需手动lenis.raf(time) |
lerp | 0.1 | 滚轮平滑的线性插值强度(0~1),定义"滑行距离" |
duration | 未设置(回落 lerp 模式) | 动画时长(秒),与easing配套,一旦提供lerp即被忽略 |
easing | (t) => Math.min(1, 1.001 - 2 ** (-10 * t)) | 时间模式下的缓动曲线 |
smoothWheel | true | 滚轮输入是否走平滑 |
syncTouch | false | 模拟原生触摸滚动并同步位置(iOS<16 可能不稳) |
syncTouchLerp | 0.075 | 触摸惯性阶段的插值系数 |
touchInertiaExponent | 1.7 | 触摸抬手后的惯性强度指数 |
wheelMultiplier/touchMultiplier | 1 | 滚轮 / 触摸输入灵敏度 |
orientation | 'vertical' | 滚动轴向,'horizontal'需配合具体wrapper |
infinite | false | 无限循环滚动 |
overscroll | true | 类 CSSoverscroll-behavior的边界回弹 |
anchors | false | 接管锚点链接点击并平滑滚动 |
allowNestedScroll | false | 自动放行嵌套可滚动元素的滚动 |
naiveDimensions | false | 用简化的尺寸计算(有性能代价) |
autoToggle | false | 按 wrapper 的 overflow 自动 start/stop |
stopInertiaOnNavigate | false | 点击站内链接时清除滚动惯性 |
进阶参数(3 条以内,各附原因):
prevent: (node) => boolean——按节点豁免平滑。比allowNestedScroll省,因为不需要每次事件遍历 DOM。virtualScroll: (data) => boolean——在输入被消费前改写或否决它(返回false即放弃本次输入),适合"按住某个键时不平滑"这类条件交互。naiveDimensions——改用scrollHeight - clientHeight直接算limit,逻辑更简单,README 标注有性能影响,谨慎开启。
性能调优(3 条以内,各附原因):
- 一定引入
lenis.css:data-lenis-prevent元素的overscroll-behavior: contain靠它生效,少了会看到弹性回弹把外层页面带滚。 scroll事件每帧触发,回调里只读随实例带来的scroll/progress/velocity(都是现成属性,零布局读取);确需读布局就自行节流。- 页面尺寸完全固定时可设
autoResize: false并手动调resize():内部ResizeObserver的重算本身带 250ms 防抖(packages/core/src/dimensions.ts),对静态页面是一次可省的开销。
生态集成与常见问题
接入 React 与 Vue 官方适配层
- React(
packages/react/):LenisProvider包裹应用后创建实例;组件内用useLenis(callback, deps, priority)注册滚动回调,回调按 priority 排序执行、卸载时自动移除(packages/react/src/use-lenis.ts)。 - Vue / Nuxt(
packages/vue/):同构的 provider +useLenis;Nuxt 模块在packages/vue/nuxt/module.ts,集成示例见playground/nuxt/plugins/lenis.ts。 - 吸附插件(
packages/snap/):new Snap(lenis, { type })+snap.add(500 或元素)。type支持'proximity'(默认)、'mandatory'、'lock',distanceThreshold默认'50%'、判定防抖 500ms(packages/snap/src/snap.ts),它监听的是 Lenis 的virtual-scroll事件,不与主滚动逻辑打架。
常见问题(现象 → 原因 → 解决)
- 现象:初始化后页面滚动毫无平滑感。原因:
autoRaf默认false,内部动画没有每帧推进。解决:开autoRaf: true,或在自己的循环里每帧lenis.raf(time)(README Troubleshooting 第一条)。 - 现象:
stop()之后仍能被滚。原因:.lenis-stopped { overflow: clip }依赖推荐 CSS,没引入lenis.css时类名没有实际效果。解决:引入lenis/dist/lenis.css。 - 现象:锚点链接点了没反应。原因:Lenis 默认在滚动期间拦截锚点行为。解决:
anchors: true(或传ScrollToOptions);hash 含特殊字符也能正常定位,内部走decodeURIComponent解码(lenis.ts的onClick)。 - 现象:平滑滚动时 iframe 内容点不动。原因:
lenis.css在lenis-smooth期间故意给 iframe 加pointer-events: none,防止它吞掉 wheel 事件导致滚动卡死。解决:需要交互时给 iframe 外层加data-lenis-prevent。 - 现象:Safari 帧率卡在 60fps,省电模式只剩 30fps。原因:WebKit 对
requestAnimationFrame的上限(README Limitations 列出的已知 bug)与系统省电策略。解决:属环境限制而非参数问题,不要为 Safari 单独加"补帧"逻辑。
继续去哪儿
- 核心源码:packages/core/src/lenis.ts、选项类型与默认值:packages/core/src/types.ts
- 插件与适配层:snap 吸附插件、React 适配层、Vue 与 Nuxt 模块
- 可运行的演示:playground 目录(core / horizontal / infinite / snap / touch-debug 各有独立示例)
- 想深入原理先读 MANIFESTO.md,参与开发看 CONTRIBUTING.md
lenis.ts文件头那六行注释加maths.ts里的damp,就是这套滚动系统的全部骨架——改一个参数之前先看这两处,比翻文档更快。
【免费下载链接】lenisSmooth scroll as it should be项目地址: https://gitcode.com/GitHub_Trending/le/lenis
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考