View Transitions API 让页面切换变得非常容易:加一行 <ClientRouter />,浏览器就帮你做快照和交叉淡入。但「能跑」和「跑得顺」之间隔着五个坑。
一、group 的宽高动画
这是最隐蔽的一个。::view-transition-group() 的默认动画总是包含 width 和 height,即使两端尺寸完全一样。也就是说,一个看起来只是位移的转场,浏览器在每一帧都在做布局计算。
修法很简单:
/* 根快照尺寸不变时,直接关掉 group 动画 */
::view-transition-group(root) {
animation: none;
}如果确实需要共享元素 morph,可以用 FLIP 思路重写关键帧,把不变的宽高从动画里去掉。
二、命名元素的数量就是快照数量
每给一个元素起 view-transition-name,浏览器就要为它生成旧、新两张快照。一张 1440×900、DPR 2 的全屏快照大约 20MB。
经验值:
| 场景 | 命名元素上限 |
|---|---|
| 桌面列表 | 8-12 个 |
| 移动端 | 4-6 个 |
| 带图片的卡片网格 | 6 个以内 |
超过这个量级,转场开始前的那一帧会明显卡顿——因为截图本身是同步的。
三、重复的 name 会让整次转场失效
同一时刻有两个元素用同一个 view-transition-name,当前规范的处理是跳过整次转场,而不是只跳过重复的那个。
列表页最容易踩:给每个卡片都写 transition:name="hero",结果点了没反应。正确做法是动态命名,只给被点击的那一张:
document.addEventListener('astro:before-preparation', (event) => {
document.querySelectorAll('[data-vt-hero]').forEach((el) => {
el.removeAttribute('data-vt-hero');
});
event.sourceElement?.querySelector('img')?.setAttribute('data-vt-hero', '');
});四、不要在大面积上用 clip-path
clip-path 看起来比 transform 更适合做擦除,但它不保证走合成器。Safari 上给 clip-path 加 will-change 还会闪。
如果要用,遵守三条:
- 点数控制在 4-8 个,不要几十个点的复杂 polygon
- 时长不超过 500ms
- 不加
will-change
更稳的做法是横向位移:translate3d(±100%, 0, 0) 是纯合成器动画,两页各自动不透明,也不可能出现文字叠影。
五、转场期间页面是「死」的
::view-transition 会铺满视口并捕获点击。用户在 300ms 的转场里点任何东西都没反应,这直接影响 INP。
::view-transition {
pointer-events: none;
}另外 Astro 的客户端路由本身会在新导航时跳过上一个未完成的转场,但要理解它的行为:finished 即使被 skip 也会 resolve,所以不能把降级逻辑挂在 .catch() 上。
我的最终方案
三层结构:
- 纸面与报头静止——它们不参与动画,视觉上提供稳定的参照
- 只有正文层横向推移——
view-transition-name: page - 2px 警戒色竖线贴着接缝同步移动——与推移同 duration、同 easing,否则会「漂」
三个命名元素,纯 transform,340ms,cubic-bezier(0.2, 0, 0, 1)。跑起来是稳定的 60fps,也没有快照堆积。
转场的说服力来自「一致」:方向一致、缓动一致、节奏一致。不是来自时长。
最后别忘了 prefers-reduced-motion。转场是装饰,阅读不是。