一致的跨文档过渡

问题

跨文档视图过渡会在同源导航期间为两个页面之间的元素添加动画效果。浏览器会捕获旧页面的快照,进行导航,然后从快照动画过渡到新页面。如果新页面尚未完成关键资源(样式表、布局脚本或关键 DOM 元素)的加载,过渡动画会呈现不完整或无样式的状态。这会导致视觉故障,例如元素变形到错误的位置、内容在动画中途重排,或在过渡完成后后备字体闪烁到 Web 字体。

解决方案

在新网页的 <head> 中对关键 <link> 和 <script> 元素使用 blocking="render",并使用 <link rel="expect"> 阻止渲染,直到特定 DOM 元素被解析。这样可以确保浏览器在新页面的视觉状态稳定之前不会开始视图过渡动画。浏览器会继续在后台解析 HTML,只有绘制操作会被推迟。

实施策略

  1. 强制性要求:在两个页面上使用 @view-transition CSS at 规则,选择启用跨文档视图过渡。
  2. 强制性要求:确保关键样式表位于 <head> 中。默认情况下,<head> 中的样式表会阻塞渲染。动态注入的样式表需要显式 blocking="render"。
  3. 务必在必须在过渡动画播放之前执行的 <script> 元素(例如,应用主题或影响布局的脚本)上使用 blocking="render"。
  4. 务必使用 <link rel="expect" href="#element-id" blocking="render"> 阻止渲染,直到首屏内容解析完毕。这适用于所有过渡类型:全页淡入淡出(避免动画过渡到空白页)、变形动画(确保命名元素存在于 DOM 中)以及依赖脚本的布局(确保样式化内容被解析)。
  5. 请勿在非关键内容上阻止渲染。仅阻止影响初始视口的资源和元素。阻塞过多内容会延迟过渡并降低感知性能。

实现指南

第 1 步:选择启用跨文档视图过渡

强制性:源网页和目标网页都必须包含 @view-transition at 规则。否则,不会发生跨文档过渡。

/*
  MANDATORY: Include this rule in every page that participates
  in cross-document view transitions.
  `navigation: auto` enables transitions for standard navigations
  (link clicks, form submissions, back/forward).
*/
@view-transition {
  navigation: auto;
}

/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

第 2 步:在关键脚本执行之前阻止渲染

如果 <head> 中的非阻塞脚本必须在过渡动画播放之前运行(例如,为了应用主题类或影响布局),请使用 blocking="render" 标记该脚本。否则,async、defer 或 type="module" 脚本可能会在过渡开始后执行。

<head>
  <!--
    DO: Mark layout-critical scripts with blocking="render".
  -->
  <script type=module blocking="render">
    // Example: apply a stored theme before the page renders,
    // so the transition snapshot reflects the correct theme.
    document.documentElement.dataset.theme =
      localStorage.getItem('theme') || 'light';
  </script>
</head>

第 3 步:在解析关键 DOM 元素之前阻止渲染

<head> 中的样式表和 blocking="render" 脚本只能保证 <head> 已完全处理。它们不会等待任何 <body> 内容被解析。如果没有额外的阻塞,浏览器可能会在 DOM 中存在首屏元素之前截取新页面快照,从而导致过渡动画呈现空白或部分渲染的页面。

<link rel="expect"> 通过阻止渲染,直到特定元素(由其 id 标识)被解析,从而解决了这个问题。href 值必须是与目标元素的 id 属性匹配的片段标识符(例如 #hero)。解析该元素的结束标记后,系统会释放渲染阻塞。请注意,<link rel="expect"> 仅保证目标 DOM 元素存在于文档中,而不会等待外部子资源(例如 <img> 像素)下载或解码。

务必在以下所有场景中使用 <link rel="expect">:

应用场景 1:全页淡入淡出

即使没有任何单个元素具有 view-transition-name,默认的 root 过渡也会淡入淡出整个页面。如果新网页的快照是在首屏内容解析之前拍摄的,则淡入淡出效果会从旧网页动画过渡到空白或不完整的网页。在标记可见首屏内容结尾的元素上阻止渲染。

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Block rendering until the main content area is parsed,
    even for a simple cross-fade. Without this, the browser may
    snapshot the page before visible content exists in the DOM,
    causing the cross-fade to reveal a blank or partial page.
  -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <header>...</header>
  <main id="main-content">
    <h1>Page Title</h1>
    <p>Above-the-fold content the user should see immediately.</p>
  </main>
  <!-- Content below the fold does NOT need to be blocked on -->
  <section>...</section>
</body>

应用场景 2:在特定元素之间进行变形动画

当两个网页上的元素共享一个 view-transition-name 时,浏览器会在导航过程中平滑地变形。如果目标元素在过渡开始时未被解析,浏览器将无法找到它,变形效果会降级为单独的退出和进入动画。在解析具有 view-transition-name 的元素之前,阻止渲染。

由于 <link rel="expect"> 仅会暂停渲染,直到目标 DOM 元素被解析,而不会等待图片数据下载或解码,因此未缓存的 <img> 可能会在新页面快照中被捕获为空矩形。为防止在图片变形期间出现空白快照或布局偏移,请预加载或预解码关键过渡图片(例如,使用 <link rel="preload" as="image"> 或预渲染目标网页),并提供明确的尺寸后备(width 和 height 属性或 CSS aspect-ratio,以及占位符背景颜色)。

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Preload critical transition images so their pixels are
    available to decode before the new-page snapshot is captured.
    <link rel="expect"> only waits for the DOM element to be parsed,
    NOT for image pixels to download or decode.
  -->
  <link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">

  <!--
    DO: Block rendering until the element participating in the
    morph animation has been parsed. Without this, the browser
    may start the transition before #hero exists, causing the
    morph to degrade to a fade-out/fade-in.
  -->
  <link rel="expect" href="#hero" blocking="render">

  <!--
    When multiple blocking="render" resources are present,
    rendering is blocked until ALL of them are satisfied.
    Here, the browser waits for both the script to execute
    AND the #hero element to be parsed — whichever comes last.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
  <header>...</header>
  <section id="hero">
    <h1 style="view-transition-name: page-title">Product Name</h1>
    <!--
      DO: Provide explicit dimensions (width/height or aspect-ratio)
      as a sizing fallback so the snapshot captures the correct layout
      box even if image decoding is still in progress.
    -->
    <img
      style="view-transition-name: hero-image"
      src="/img/product.webp"
      width="800"
      height="600"
      alt="Product"
    >
  </section>
</body>

第 4 步:使用媒体查询实现自适应渲染阻塞

不同的视口尺寸可能会在首屏上显示不同数量的内容。在 <link rel="expect"> 上使用 media 属性,仅针对在给定视口宽度下可见的内容阻止渲染。

<head>
  <!--
    DO: Use media queries to conditionally block rendering.
    On wide screens, both the hero and the sidebar are visible,
    so block until both are parsed. On narrow screens, only the
    hero is visible initially.
  -->
  <link
    rel="expect"
    href="#hero"
    blocking="render"
    media="screen and (width <= 768px)"
  >
  <link
    rel="expect"
    href="#sidebar"
    blocking="render"
    media="screen and (width > 768px)"
  >
</head>

第 5 步:使用 pagereveal 实现上下文相关过渡(可选)

核心渲染阻塞策略不需要 pagereveal 事件。只有在必须根据用户导航来源动态分配 view-transition-name 值时,才需要使用此方法,例如将特定列表项变形为详情页面标题。

如果 view-transition-name 值是在 CSS 中静态分配的,或者您仅使用默认的全页淡入淡出效果,请完全跳过此步骤。

<head>
  <!--
    MANDATORY: The pagereveal listener must be registered before
    the page renders. Inline small setup scripts with blocking="render"
    to avoid an extra network round-trip on cold loads, or use an
    external async script with blocking="render" so the listener is
    registered early without blocking HTML parsing.
    If the listener is registered too late (e.g., in a deferred
    script), the event may have already fired.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation?.from;
  if (!from) return;

  const fromUrl = new URL(from.url);

  // DO: Assign view-transition-name based on navigation context.
  // This enables a morph animation from the product card on the
  // list page to the heading on the detail page.
  if (fromUrl.pathname === '/products/') {
    const heading = document.querySelector('main h1');
    if (!heading) return;

    heading.style.viewTransitionName = 'product-title';

    try {
      await event.viewTransition.ready;
    } catch (error) {
      // Skipped view transitions reject `ready` with InvalidStateError.
      if (error.name !== 'InvalidStateError') {
        throw error;
      }
    } finally {
      // MANDATORY: Remove the temporary name after the transition
      // finishes. Stale names interfere with subsequent navigations
      // and prevent the page from entering the bfcache.
      await event.viewTransition.finished;
      heading.style.viewTransitionName = '';
    }
  }
});

最佳做法

  • 务必尽可能通过 CSS 分配 view-transition-name。在名称取决于导航上下文的情况下,保留 JavaScript 赋值(通过 pagereveal)。
  • 务必确保会阻塞渲染的脚本体积小、速度快。浏览器具有内置超时(大约 4 秒),超时后会完全跳过过渡(如果已启动过渡,则会拒绝 viewTransition.ready 并显示 InvalidStateError)。
  • 请勿使用 <link rel="expect"> 屏蔽初始视口中不可见的网页深层元素。这会延迟过渡,而不会带来视觉上的好处。
  • 请勿使用 blocking="render" 标记分析、广告联盟、代码管理器或其他第三方加载器,即使它们会将内容注入初始视口。它们的网络延迟会计入过渡超时,并且其输出不属于网页的稳定状态。
  • 请勿将同一 view-transition-name 分配给同一网页上的多个元素。重复的名称会导致整个转换过程被跳过。
  • 辅助技术时间影响:使用 blocking="render" 会延迟视觉更新和初始绘制。虽然这可以防止视力正常的用户看到视觉故障,但可能会导致屏幕阅读器和其他依赖于渲染的无障碍树的辅助技术出现处理延迟或延迟初始化。权衡视觉连续性优势与非视觉用户的初始读取延迟,并确保渲染阻塞脚本数量最少且经过极致优化。

后备策略

跨文档视图过渡非常适合渐进增强。在不支持这些规则的浏览器中,@view-transition 规则会被忽略,并且会像没有该功能一样进行标准的同源导航。支持的浏览器可实现流畅过渡;所有其他浏览器则采用标准导航。浏览器支持有限并不是避免采用的理由。

所有支持跨文档视图过渡的浏览器也都支持 blocking="render" 和 <link rel="expect">,因此无需为本指南中介绍的渲染阻塞功能单独设置回退。

其他注意事项

  1. 对性能的影响:每个阻塞渲染的资源都会延迟视图过渡动画的开始。尽量减少会阻止呈现的脚本数量,并且仅对首屏元素使用 <link rel="expect">。使用 Speculation Rules API 预渲染目标网页,彻底消除加载延迟。
  2. 超时行为:如果总渲染阻塞时间超过大约 4 秒,浏览器会跳过过渡(并且任何活跃的 viewTransition.ready promise 都会被 InvalidStateError 拒绝)。请确保关键资源在此时间范围内顺利加载。
  3. bfcache 兼容性:在过渡后未清理的临时 view-transition-name 分配可能会阻止网页进入 bfcache。始终在 finished 回调中移除动态分配的名称。