合者离之始
Record:002

View Transitions 的五个性能陷阱

页面转场看起来很酷,但浏览器默认的 group 动画会在每一帧重新布局。记录几个必须知道的坑。

Posted2026-08-02Words1661Reading4 min

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() 上。

我的最终方案

三层结构:

  1. 纸面与报头静止——它们不参与动画,视觉上提供稳定的参照
  2. 只有正文层横向推移——view-transition-name: page
  3. 2px 警戒色竖线贴着接缝同步移动——与推移同 duration、同 easing,否则会「漂」

三个命名元素,纯 transform,340ms,cubic-bezier(0.2, 0, 0, 1)。跑起来是稳定的 60fps,也没有快照堆积。

转场的说服力来自「一致」:方向一致、缓动一致、节奏一致。不是来自时长。

最后别忘了 prefers-reduced-motion。转场是装饰,阅读不是。