TL;DR
问题:Astro 的 ClientRouter(View Transitions API)在 Safari 上会导致动画重复播放和闪烁。
原因:Safari 对 View Transitions API 的支持不完善,这是已知问题。
解决方案:在 Safari 上覆盖 document.startViewTransition API,直接执行回调而不触发过渡动画。
JavaScript
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari && document.startViewTransition) {
document.startViewTransition = (callback) => {
const result = callback();
return {
finished: Promise.resolve(),
ready: Promise.resolve(),
updateCallbackDone: Promise.resolve(result),
skipTransition: () => {}
};
};
}
效果:Safari 保留 SPA 导航(页面秒切换),同时解决闪烁问题。其他浏览器不受影响。
写在前面
本人有轻微的强迫症,我经常对于着一些小细节感到难受,包括但是不仅限于这一次的博客过渡动画,以及生活中的一些琐事。你可能会说“为什么要对这些小事吹毛求疵?”,相信我,我本人也有点烦自己的这种行为。这会让我因为在一些小到不能再小的事情上纠结和难受很久,哪怕是衣服沾到一个小污渍,我也会为此难受好几天。希望我可以帮助你在这篇文章了解WebKit的弊端(或者说是Chromium的优势?),以及以UI/UX设计师角度看待关于「加载速度」。
为何选择Astro?
旧的Wordpress博客因为独服到期不想续费,并且没有找到合适的VPS/托管,想着尝试一下静态博客。因为见过太多这种类型⬇️
“请问你的博客是加了一堆文本加上渐变配色代码块,用衬线字体article 巨大阴影半透明背景加莫名其妙的地方抄来的canvas动效,打开会自动播放网易云音乐且加载一个Live2D*小人挡住播放器控件,还有自定义光标加上点击特效且复制文本的时候会自作主张加上版权通告,切出网页以后title还会变成莫名其妙的文本内容的那个吗”
的Hexo博客,所以Hexo乃至静态博客都没有什么好感,并且我写博客的理念一直是写作优先,对于静态博客部署更麻烦、写作更不便(仅代表个人观点,没有UI编辑器,需要直接写Markdown对于我来说确实没有那么便利。)更像是在互联网早期或者那些喜欢折腾的大佬喜欢的。但是碍于没有现成的机器,所以没办法自托管Ghost(一个现代博客系统)。所以最终决定弄一个素一点主题的静态博客,而Astro自称是性能最佳的内容驱动框架。
问题的开始
这个博客使用 Astro 框架构建,并启用了 ClientRouter(View Transitions API)来实现页面间的平滑过渡动画。在 Chrome 上一切正常——页面切换丝滑,动画流畅。
但当我在 Safari 上测试时,问题出现了:
动画会重复播放两次,并伴随明显的闪烁。
就像是第一次动画还没完成,就被强制打断,然后重新播放了一遍。
第一阶段:以为是 CSS 动画问题
尝试 1:优化 CSS 过渡属性
最初我认为是 CSS 动画性能问题。原本的代码使用了 transition-all:
.animate {
@apply -translate-y-3 opacity-0;
@apply transition-all duration-300 ease-out;
}
transition-all 会监听所有 CSS 属性的变化,可能造成性能开销。于是我改成只监听需要的属性:
.animate {
transition-property: opacity, transform;
transition-duration: 300ms;
transition-timing-function: ease-out;
will-change: opacity, transform;
-webkit-font-smoothing: antialiased;
}
结果:没有效果。闪烁依旧。
尝试 2:各种 GPU 加速技巧
接下来我尝试了各种网上推荐的 Safari 动画优化技巧:
.animate {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
transform: translateZ(0);
perspective: 1000px;
}
结果:依然闪烁。甚至有时候更糟糕了。
尝试 3:参考 astro-sphere 的简单实现
我找到了 astro-sphere 项目,它在 Safari 上运行流畅。对比后发现它的动画实现更简单:
/* astro-sphere 的实现 */
.animate {
opacity: 0;
transform: translateY(50px);
transition: opacity 1s ease, transform 1s ease;
}
没有任何 GPU “优化”。我照搬了这个方案。
结果:依然闪烁。问题不在 CSS。
第二阶段:发现真正的问题
动画被触发了两次
通过调试,我发现 animate() 函数被调用了两次:
document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());
每次 init() 都会调用 animate(),导致动画在播放到一半时被打断重新开始。
我添加了检查防止重复触发:
function animate() {
const animateElements = document.querySelectorAll(".animate");
animateElements.forEach((element, index) => {
if (element.classList.contains("show")) {
return; // 跳过已经动画过的元素
}
setTimeout(() => {
element.classList.add("show");
}, index * 150);
});
}
结果:问题依旧。原来双重触发不是根本原因。
真相大白:View Transitions API 的 Safari 兼容性问题
经过搜索 Astro 的 GitHub Issues,我发现这是一个已知问题。很多人报告过 ClientRouter/ViewTransitions 在 Safari 上的各种问题:
| Issue | 问题描述 |
|---|---|
| #8625 | iOS Safari 17 滚动卡顿,CPU 过载,手机发烫 |
| #8711 | 暗黑模式 + View Transitions = 闪烁 |
| #8803 | 移动设备 UI 闪烁 |
| #9650 | Firefox 和 Safari 动画跳动 |
而且我发现 astro-sphere 之所以流畅,是因为它注释掉了 ViewTransitions:
<!-- <ViewTransitions /> -->
原来问题的根源是 Safari 对 View Transitions API 的支持不完善。
第三阶段:寻找解决方案
方案 A:用 CSS 禁用 Safari 的 View Transition 动画
尝试在 Safari 上禁用 View Transition 的过渡动画,但保留 SPA 导航:
@supports (-webkit-hyphens: none) {
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
结果:还是闪烁。CSS 层面无法解决。
方案 B:在 Safari 上禁用 ClientRouter
检测 Safari 浏览器,强制使用传统页面导航:
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
document.addEventListener("astro:before-preparation", (e) => {
e.preventDefault();
window.location.href = e.to.href;
});
}
配合 instant.page 保持预加载体验:
if (isSafari) {
import('instant.page');
}
结果:闪烁解决了!但新问题出现——页面加载变得很慢。
Chrome 秒切换(SPA 导航,资源只加载一次),Safari 要等好几秒(每次都要重新加载 18 个字体文件、giscus 脚本、Pagefind 脚本)。
方案 C:完全移除 ClientRouter
像 astro-sphere 一样,完全移除 ClientRouter,所有浏览器都使用传统页面导航:
- import { ClientRouter } from "astro:transitions";
- <ClientRouter />
结果:所有浏览器体验一致,没有闪烁。但页面切换没有了 SPA 的流畅感。
方案 D:覆盖 startViewTransition API
核心思路:在 Safari 上覆盖 document.startViewTransition,让它直接执行回调而不触发过渡动画。
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari && document.startViewTransition) {
document.startViewTransition = (callback) => {
const result = callback();
return {
finished: Promise.resolve(),
ready: Promise.resolve(),
updateCallbackDone: Promise.resolve(result),
skipTransition: () => {}
};
};
}
结果:闪烁彻底解决!而且保留了 SPA 导航(页面秒切换)。
但我注意到一个 UX 问题:点击链接后没有任何视觉反馈。
第四阶段:UX 的权衡
感知速度 ≠ 实际速度
覆盖 startViewTransition 的方案技术上是最优的:
- 保留 SPA 导航(页面秒切换)
- Safari 闪烁问题解决
但用户体验上有问题:
- SPA 导航时浏览器不显示进度条
- 点击后”静默等待”,用户不知道系统有没有响应
- 这种”卡住”的感觉比实际等待时间更让人焦虑
而完全移除 ClientRouter 的方案:
- 传统页面刷新,浏览器自动显示进度条
- 用户立刻知道”点击生效了,正在加载”
- 心理上更容易接受等待
结论:即使实际加载时间相同,有进度条的版本让用户感觉更快、更可控。
最终方案:覆盖 API + 自定义加载指示器
为了两全其美,我选择:
- 在 Safari 上覆盖
startViewTransitionAPI(解决闪烁,保留 SPA 性能) - 添加自定义顶部加载进度条(提供视觉反馈)
// Safari 检测:完全禁用 View Transitions API
(function() {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari && document.startViewTransition) {
document.startViewTransition = (callback) => {
const result = callback();
return {
finished: Promise.resolve(),
ready: Promise.resolve(),
updateCallbackDone: Promise.resolve(result),
skipTransition: () => {}
};
};
}
})();
一些心得
1. 问题可能在更底层
CSS 动画优化无法解决问题,因为根本原因在于 Safari 对 View Transitions API 的实现有 bug。
2. 参考其他项目是捷径
当我看到 astro-sphere 直接注释掉了 ViewTransitions,我意识到这可能是一个已知问题,不必纠结于”修复” Safari。
3. 搜索 GitHub Issues
很多问题别人已经遇到过了。Astro 的 GitHub Issues 里有大量关于 Safari 兼容性的讨论。
4. UX 比技术更重要
技术上最优的方案不一定是用户体验最好的方案。有时候”慢但有反馈”比”快但无反馈”更好。
6. 浏览器兼容性永远是痛点
大多数组件库仍因为Chromium的市场占有率高而仅对Chromium优化,不管WebKit的Safari以及Firefox的死活。相信大家也经常遇到一些网站在Firefox和Safari很卡顿,但是在Chromium的浏览器就非常流畅的经历
View Transitions API 是一个很新的标准:
- Chrome 111+(2023年3月)原生支持
- Safari 18+(2024年9月)才开始支持
- Firefox 至今仍不完全支持
即使 Safari 18 “支持” 了这个 API,也不意味着实现是完善的。
相关链接
- Astro View Transitions 文档
- View Transitions API - MDN
- instant.page - 轻量级链接预加载库
- astro-sphere - 一个不使用 View Transitions 的 Astro 主题
- Chrome Paint Holding - Chrome 的页面切换优化
写在最后
这次调试花了我一整天的时间,从最初以为是简单的 CSS 问题,到最终发现是浏览器 API 的兼容性问题。
面对浏览器兼容性问题,与其花大量时间试图”修复”浏览器的 bug,不如选择优雅降级——让有问题的浏览器使用更简单、更稳定的方案。