Safari 直到 macOS Sonoma 14.0 / iOS 17 才原生支持 scroll-behavior: smooth,且仅限 html 元素;iOS 15.3 及以前完全忽略该声明。

因为 Safari 直到 macOS Sonoma 14.0 / iOS 17 才原生支持 scroll-behavior: smooth,且仅限 html 元素;更早版本(尤其是 iOS 15.3 及以前)完全忽略该声明,不报错、不降级、不动画——你写了,它就当没看见。
scroll-behavior: smooth 在 Safari 的真实支持边界
这不是写法问题,是浏览器能力断层。关键事实包括:
-
html { scroll-behavior: smooth; }在 iOS 15.4+ 才开始被解析,但仅对锚点跳转(a[href^="#"])生效;scrollIntoView()仍需显式传参 - 任何带
overflow: auto的自定义容器(如.sidebar或.chat-log)上设置该 CSS,Safari 一律静默丢弃,哪怕你在开发者工具里看到它“存在” - 即使系统未开启
prefers-reduced-motion,旧版 Safari 也会因缺少滚动插值引擎而直接 fallback 到instant -
@supports (scroll-behavior: smooth)检测不可靠——它只查 CSS 属性是否被识别,不反映 JS 行为是否可用
为什么 smoothscroll-polyfill 在 Safari 里反而卡顿
很多项目一上来就 import 'smoothscroll-polyfill',结果在 Safari 中滚动跳帧、目标偏移、连点后动画堆叠。根本原因在于:
- polyfill 重度依赖
MutationObserver,而 Safari 对其触发时机敏感,DOM 更新与 layout 计算不同步时,会反复重算目标位置 - 它没有内置用户手动滚动中断逻辑,
cancelAnimationFrame调用滞后,导致“拉扯感”或滚动过冲 - 不对容器可见性做校验:若目标元素父级是
display: none或overflow: hidden,仍强行驱动,getBoundingClientRect()返回{ top: 0, left: 0 },滚动直接错乱 - 默认使用
ease-in-out缓动,开头太慢、结尾易抖,和原生smooth的cubic-bezier(0.25, 0.46, 0.45, 0.94)行为差异明显
手写 requestAnimationFrame 轻量补丁的关键控制点
30 行内可覆盖 95% 场景,核心不在“实现平滑”,而在“不出错”。必须处理:
- 先校验目标元素是否真正可滚动:
el.offsetParent !== null && getComputedStyle(el).display !== 'none' - 滚动容器必须明确可读写:
const container = options.container || window,且不能是overflow: hidden的父级 - 用
performance.now()记录起始时间,比Date.now()更精准,避免帧率波动导致时长偏差 - 插值必须用增量更新:
container.scrollTop += delta,而非直接赋值scrollTop = target,否则 Safari 容易回弹抖动 - 监听
scroll事件并在用户手动滚动时立即cancelAnimationFrame,这是 Safari 下最易被忽略的体验分水岭
锚点跳转拦截的最小可行封装
不要试图全局 polyfill,只劫持你真正在用的链接。示例逻辑:
document.querySelectorAll('a[href^="#"]').forEach(a => {
a.addEventListener('click', e => {
const hash = a.getAttribute('href').slice(1);
const el = document.getElementById(hash);
if (!el || el.offsetParent === null) return;
e.preventDefault();
smoothScrollTo(el, { offset: 60 }); // 预留导航栏高度
});
});
<p>function smoothScrollTo(el, { container = window, offset = 0, duration = 400 } = {}) {
const start = container === window ? window.scrollY : container.scrollTop;
const rect = el.getBoundingClientRect();
const target = container === window
? rect.top + start - offset
: rect.top + container.scrollTop - offset;</p><p>let startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3); // cubic-bezier(0.25, 0.46, 0.45, 0.94) 近似
const current = start + (target - start) * ease;</p><pre class="brush:php;toolbar:false;">container.scrollTo({ top: current });
if (progress <p>}
requestAnimationFrame(step);
}</p>
真正难的不是让滚动动起来,而是确保它在用户伸手去拖滚动条的瞬间立刻停住、不抢权、不残留——这点 Safari 尤其苛刻,所有“看起来差不多”的 polyfill 往往败在这里。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











