前端技术
#CSS#scroll-snap#滚动性能#轮播

CSS scroll-snap 的 mandatory 与 proximity:为什么强制吸附会引发滚动卡顿

本文以图片轮播场景为例,分析 scroll-snap-type 的 mandatory 与 proximity 值如何影响滚动行为,解释强制吸附导致滚动位置突变和性能问题的机制,并给出减少卡顿的实践建议。

从一次滑动说起

在移动端图片轮播里,用户快速横向滑动后松手,手指离开屏幕的瞬间,页面往往还会继续滚动一段距离。如果这段惯性滚动结束的位置恰好让图片停在视图中间,体验自然顺畅;但更多时候,滚动会停在两张图片之间,露出一半的上一张和一半的下一张。这种“卡在中间”的状态,对轮播这类分页式内容来说并不理想。

CSS Scroll Snap 就是为了解决这个问题而设计的。它允许开发者声明一组“吸附位置”,让滚动操作结束后,滚动容器的滚动偏移自动调整到最近的吸附位置。这样,用户滑动后图片总能完整地停在视口内。

但吸附本身也会带来新的问题。很多开发者把 scroll-snap-type 设为 mandatory,希望滚动“必须”停在吸附位置,结果却发现快速滑动时滚动变得生硬、卡顿,甚至出现位置突然跳变。相比之下,proximity 模式虽然不那么“强制”,却往往更顺滑。本文以图片轮播为贯穿场景,分析这两种严格度值在滚动机制上的差异,以及为什么强制吸附会引发卡顿。

吸附位置是如何产生的

在讨论严格度之前,先要理解吸附位置从哪来。CSS Scroll Snap 的模型分为两层:滚动容器通过 scroll-snap-type 声明自己启用吸附,并指定吸附的轴和严格度;容器内的子元素通过 scroll-snap-align 声明自己在哪个位置产生吸附点。

以横向轮播为例,容器设置 scroll-snap-type: x mandatory,每个图片元素设置 scroll-snap-align: center。当图片的中心与滚动容器的吸附区域中心对齐时,就产生一个吸附位置。这里“吸附区域”默认是滚动容器的可视区域,但可以用 scroll-padding 调整。

吸附位置本质上是一个滚动偏移量。滚动操作结束后,浏览器会从所有有效的吸附位置中选一个作为最终停靠点。规范(CSS Scroll Snap Module Level 1)在“Snapping Mechanics”一节中描述了选择过程:浏览器根据滚动操作的细节(比如惯性速度、方向)、当前滚动位置以及吸附位置的距离,决定最终吸附到哪个位置。

关键点在于,吸附不是滚动过程中实时发生的,而是在滚动操作“完成后”调整滚动偏移。也就是说,用户松手后,滚动先按正常的惯性继续,等它自然停下来,浏览器再计算最近的吸附位置,然后平滑地动画到那里。这个“先停再吸”的过程,是理解卡顿的基础。

mandatory 与 proximity 的严格度差异

scroll-snap-type 的第二个值决定吸附的严格度,可选 noneproximitymandatory

  • none:不启用吸附,滚动行为与普通滚动无异。
  • proximity:当滚动位置接近某个吸附位置时,浏览器会吸附过去;如果距离较远,则不强制吸附。
  • mandatory:滚动操作结束后,滚动位置必须落在某个吸附位置,无论当前离它多远。

规范对 proximity 的描述是“如果滚动操作结束位置接近某个吸附位置,则吸附到该位置”,但“接近”的具体阈值由浏览器实现决定,规范没有规定固定距离。这意味着不同浏览器对 proximity 的敏感度可能不同。

mandatory 则没有这种灵活性。只要滚动操作结束,浏览器就必须把滚动偏移调整到最近的吸附位置。这个“必须”是强制性的,无论用户滚动得多快、离吸附位置多远。

在图片轮播中,mandatory 能保证每次滑动后图片都完整显示,但代价是滚动位置可能被强行拉回。比如用户快速滑动想跳过两张图片,但惯性结束后滚动位置落在第三张和第四张之间,mandatory 会强制吸附到其中一张,而不是让用户停在中间。这种强制调整如果距离较大,就会产生肉眼可见的位置跳变。

强制吸附为什么会导致卡顿

卡顿的直接原因是滚动位置突变。当 mandatory 强制吸附时,浏览器需要在滚动动画结束后,再启动一段新的动画把滚动偏移调整到吸附位置。如果这个调整距离很大,动画时间又短,用户就会看到画面突然跳动,而不是平滑过渡。

更深层的原因与滚动机制有关。现代浏览器中,滚动通常由合成器线程处理,不占用主线程。但吸附调整可能需要主线程参与,尤其是当吸附位置的计算涉及布局信息时。如果滚动容器内有大量图片,或者图片尺寸不一,计算吸附位置可能触发布局读取,导致主线程阻塞。

此外,mandatory 会改变用户对滚动的预期。在普通滚动中,用户可以用任意速度滑动,停在任意位置。但 mandatory 强制吸附后,滚动位置被限制在离散的吸附点之间。当用户快速滑动时,惯性滚动可能“越过”多个吸附位置,最终被强制拉回,这种“越过再拉回”的过程在视觉上表现为卡顿。

相比之下,proximity 只在滚动停止位置接近吸附点时触发吸附,距离远时直接停在原地。这减少了不必要的强制调整,滚动更接近自然行为,因此通常更顺滑。

图片轮播场景的对比

回到图片轮播场景。假设一个横向滚动的容器,包含 10 张等宽的图片,每张宽度等于容器宽度。scroll-snap-align: center 使每张图片的中心对齐容器中心。

当用户快速滑动时,惯性滚动可能让滚动位置停在任意位置。在 mandatory 模式下,浏览器会强制吸附到最近的图片中心。如果用户滑动距离很大,比如想从第一张滑到第五张,但惯性结束后停在第四张和第五张之间,浏览器会吸附到其中一张。这个调整距离可能接近半张图片的宽度,动画过程中画面会明显跳动。

proximity 模式下,如果滚动停止位置离吸附点较远(比如超过半张图片宽度),浏览器可能不吸附,用户就停在中间。虽然这违背了轮播“必须整张显示”的初衷,但滚动过程更自然。

web.dev 的文章指出,mandatory 适合“分页式内容”,比如图片轮播,因为用户期望每次滑动后都停在完整图片上;而 proximity 更适合“具有多个逻辑部分的页面”,比如商品详情页,用户可能想停在任意位置浏览,而不是被强制吸附到某个 section。

减少卡顿的实践建议

优先使用 proximity

对于大多数滚动场景,proximity 是更安全的选择。它既能在用户接近吸附点时提供吸附的便利,又不会强制调整滚动位置,减少卡顿。如果产品需求允许,优先考虑 proximity

合理设置 scroll-padding 和 scroll-margin

scroll-padding 可以调整吸附区域,避免吸附位置与固定头部重叠;scroll-margin 可以调整元素的吸附区域,让吸附位置更合理。这些属性不仅影响吸附,还影响 scrollIntoView 等操作,合理设置能减少不必要的吸附调整。

避免过大的吸附距离

web.dev 提醒,当目标元素间距较大时,避免使用强制对齐,否则会导致吸附位置之间的内容无法访问。在轮播中,如果图片间距很大,mandatory 会让用户无法停在中间查看细节,只能被迫跳到下一张。

使用功能检测和渐进增强

滚动吸附可以作为增强功能,通过 @supports 检测浏览器是否支持,再决定是否启用。对于不支持滚动吸附的浏览器,保持普通滚动即可。

@supports (scroll-snap-align: start) {
  .gallery {
    scroll-snap-type: x proximity;
    overflow-x: scroll;
  }
  .gallery img {
    scroll-snap-align: center;
  }
}

注意程序化滚动与吸附的交互

Element.scrollTo 等 API 发起的滚动也会触发吸附。web.dev 提醒,不要假设程序化滚动一定停在请求的偏移位置,吸附可能会调整最终位置。这在实现“上一张/下一张”按钮时尤其重要,需要处理吸附带来的位置偏移。

对比表格

维度mandatoryproximity
吸附触发条件滚动操作结束后必须吸附到最近的吸附位置仅在滚动停止位置接近吸附位置时吸附
滚动位置自由度低,只能停在吸附位置高,可以停在任意位置
卡顿风险高,强制调整可能导致位置突变低,减少不必要的调整
适用场景分页式内容(如图片轮播)多逻辑部分页面(如商品详情页)
用户控制感弱,用户难以停在中间强,用户可以自由控制
实现复杂度简单,但需处理强制吸附简单,但需测试不同浏览器的阈值

滚动吸附的完整流程

下面用流程图展示一次滚动操作从开始到结束的完整过程,以及吸附在其中扮演的角色。

flowchart TD
    A[用户滑动] --> B[滚动容器产生惯性滚动]
    B --> C{滚动是否自然停止?}
    C -- 否 --> B
    C -- 是 --> D[计算当前滚动位置]
    D --> E{scroll-snap-type 是否为 mandatory?}
    E -- 是 --> F[强制吸附到最近的吸附位置]
    E -- 否 --> G{是否接近吸附位置?}
    G -- 是 --> H[吸附到该位置]
    G -- 否 --> I[停在当前位置]
    F --> J[平滑动画到吸附位置]
    H --> J
    J --> K[滚动结束]
    I --> K

图中关键转折点在于滚动自然停止后,浏览器根据严格度决定是否调整位置。mandatory 直接进入强制吸附,而 proximity 需要判断“接近”与否。这个判断的阈值由浏览器决定,因此不同浏览器下 proximity 的行为可能略有差异。

总结

mandatoryproximity 的选择本质上是“控制力”与“自然度”的权衡。mandatory 提供严格的分页体验,但可能因强制调整导致卡顿;proximity 更顺滑,但吸附不总是发生。在图片轮播中,如果追求绝对的整张显示,可以接受 mandatory 的卡顿风险;如果更看重滚动手感,proximity 是更好的选择。

实际开发中,建议先用 proximity 测试,观察用户是否频繁停在中间位置。如果确实需要强制分页,再切换到 mandatory,同时配合 scroll-padding 和合理的图片间距,尽量减少吸附距离,降低卡顿感。

资料来源

  1. CSS Scroll Snap Module Level 1
  2. Scroll Snap
  3. 利用 CSS Scroll Snap,有效控制滚动  |  Articles  |  web.dev
  4. CSS Scroll Snap Module Level 1