View Transitions API 完全ガイド|SPA・MPA ページ遷移と Astro/React 統合

View Transitions CSS Astro React UX フロントエンド
結論
  • View Transitions API は、DOM 更新やページ遷移の前後をブラウザがスナップショット化し、CSS だけでクロスフェード・共有要素モーフィングを実現する標準機能だ。
  • document.startViewTransition()view-transition-name が核心。
  • 2026 年現在は Chrome・Safari・Firefox が揃い、Astro の <ViewTransitions /> や React 側の手動ラップも実用域にある。
  • ただしアニメーションは クリティカルレンダリングパス を遅らせないよう短く保ち、体感速度は Speculation Rules API の prefetch/prerender とセットで設計しよう。

View Transitions API とは

View Transitions API(以前は Shared Element Transitions と呼ばれていた)は、Web アプリケーションの画面遷移をネイティブアプリのように滑らかに見せるためのブラウザ組み込み API です。従来は React Transition Group、GSAP、Barba.js などのライブラリで DOM を二重管理し、clone ノードを absolute 配置してアニメーションしていました。View Transitions API はその役割の多くをレンダリングエンジン側に移し、開発者は startViewTransition の呼び出しタイミングと CSS 疑似要素のスタイルに集中できます。

API は大きく2系統に分かれます。

  1. Same-document transitions(同一ドocument) — SPA のルート変更、タブ切り替え、フィルタ UI の更新など、1つの document 内で DOM が差し替わるケース
  2. Cross-document transitions(クロスドocument / MPA) — 通常の <a href> によるフルページナビゲーションで、旧ページと新ページのスナップショットを合成するケース(Chrome 126+、Safari 18.2+)

どちらも根っこは同じで、旧ビュー(old)と新ビュー(new)のラスタ画像::view-transition 疑似要素ツリー上でアニメーションします。JavaScript フレームワークに依存しないため、Astro・React・Vue・Svelte いずれも「DOM 更新のタイミング」を制御するだけで統合できます。

2026 年時点のブラウザ対応

機能ChromeSafariFirefox
document.startViewTransition111+18+144+
view-transition-name(CSS)111+18+144+
Cross-document(MPA)126+18.2+144+(フラグ解除後)
@view-transition { navigation: auto }126+18.2+検討中

2026 年 6 月時点では、主要デスクトップ・モバイルブラウザで同一ドocument 遷移は baseline 相当、MPA クロスドocument は Chrome / Safari で本番利用可能です。未対応ブラウザでは if (!document.startViewTransition) でコールバックのみ実行するプログレッシブエンハンスメントが必須です。

従来のページ遷移との違い

方式 特徴とトレードオフ
フルリロード(素の MPA) 実装が単純。白フラッシュやスクロール位置リセットが起きやすい
SPA + クライアントルーター DOM 差し替えのみ。JS バンドル・hydration コストは [CRP](/blog/critical-rendering-path-最適化/) に影響
JS アニメーションライブラリ 柔軟だが clone 管理・z-index 地獄・アクセシビリティ対応が重い
View Transitions API ブラウザがキャプチャと合成を担当。CSS で制御、フォールバック容易
+ Speculation Rules 遷移先 prerender でアニメ中に次ページを準備。[詳細](/blog/speculation-rules-api-prefetch-prerender/)

ユーザー体感では「遷移に 300ms かかった」より「前の画面が自然に変形して次の画面になった」方が高速に感じられることが多いです(Doherty Threshold 付近の応答性)。ただしアニメーション時間を延ばしすぎると、実際の LCP や INP は改善しません。短い transition(200〜400ms)+ 先読みが 2026 年の実務バランスです。

document.startViewTransition の仕組み

同一 document 内の遷移は document.startViewTransition(updateCallback) で開始します。ブラウザは次の順序で処理します。

  1. 旧状態をキャプチャ — 現在の DOM をスナップショット(ペイント画像)として保存
  2. updateCallback を実行 — ここで DOM を変更(SPA ならルートコンポーネント差し替え)
  3. 新状態をキャプチャ — 更新後の DOM をスナップショット
  4. 疑似要素ツリーでアニメーション — デフォルトは root のクロスフェード
  5. finished Promise が resolve — クリーンアップ完了

返り値は ViewTransition オブジェクトで、readyfinishedupdateCallbackDone などの Promise を持ちます。アニメーション開始前に何か調整したい場合は ready を await します。

最小構成の JavaScript 例

function swapContent(nextHtml) {
  const doUpdate = () => {
    document.querySelector('#app').innerHTML = nextHtml;
  };

  if (!document.startViewTransition) {
    doUpdate();
    return;
  }

  document.startViewTransition(doUpdate);
}

document.querySelector('#load-album').addEventListener('click', async () => {
  const response = await fetch('/api/album/42');
  const html = await response.text();
  swapContent(html);
});

この例では fetchトランジションの外で完了させ、DOM 書き換えだけを startViewTransition 内に入れています。コールバック内で await fetch すると、旧画面のキャプチャが長時間固定され UX が悪化するため、ネットワーク I/O は先に終わらせるのが鉄則です。

非同期更新と skipTransition

DOM 更新が Promise を返す場合は、async 関数をコールバックに渡せます。

async function navigateTo(path) {
  if (!document.startViewTransition) {
    await router.replace(path);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await router.replace(path);
    await document.fonts.ready;
  });

  try {
    await transition.finished;
    console.log('Transition complete');
  } catch (err) {
    console.error('Transition skipped or failed', err);
  }
}

ユーザーが途中で別操作をした場合など、進行中のトランジションを打ち切るには transition.skipTransition() を使います。連打対策や Esc キーでのキャンセル実装に使われます。

CSS:view-transition-name と疑似要素

共有要素トランジション(サムネイルが詳細ページのヒーロー画像に拡大する等)では、旧ノードと新ノードに同じ view-transition-name を付けます。ブラウザは名前をキーにペアリングし、位置・サイズを補間(モーフィング)します。

命名とスコープのルール

  • view-transition-namedocument 内で一意である必要がある(同時に2要素に同じ名前は不可)
  • none でトランジション対象から除外
  • カスタム名は ident 形式hero-image--card-42 など。無効な文字列は無視される)
  • view-transition-class(Chrome 125+)で旧/new に異なるクラスを付与し、スタイルを分岐できる

基本的な CSS 例

/* ルート全体のフェード時間 */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* 共有要素:一覧のカード → 詳細のヒーロー */
.card-thumb {
  view-transition-name: product-hero;
  contain: layout;
}

.detail-hero {
  view-transition-name: product-hero;
}

/* 共有要素だけモーフィング、ルートは瞬時に */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}

::view-transition-group(product-hero) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

/* 旧側をフェードアウトさせず形状だけ動かす例 */
::view-transition-old(product-hero) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-new(product-hero) {
  animation: none;
}

疑似要素の階層はおおむね次のとおりです。

疑似要素役割
::view-transition全画面固定オーバーレイのルート
::view-transition-group(name)名前付きグループの transform 原点・サイズ
::view-transition-image-pair(name)old/new 画像ペアのコンテナ
::view-transition-old(name)遷移前スナップショット
::view-transition-new(name)遷移後スナップショット

contain: layout を共有要素に付けると、キャプチャ範囲が安定し、レイアウト揺れによるアーティファクトを減らせます。

MPA:クロスドocument 遷移

従来の Multi-Page Application(MPA) でも、同一オリジンかつ @view-transition を有効にしたページ間では、ナビゲーション自体が View Transition として処理されます。JavaScript で startViewTransition を呼ぶ必要はなく、<a href="/next"> のクリックでブラウザが旧 document と新 document を合成します。

@view-transition ルール

@view-transition {
  navigation: auto;
}

navigation: auto は、ユーザー操作による同一オリジン navigations で cross-document transition を有効にします(Chrome 126+)。navigation: none で明示的に無効化できます。

MPA 用 HTML + CSS の完全例

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>商品一覧 — Shop</title>
  <link rel="stylesheet" href="/css/view-transitions.css">
  <style>
    @view-transition {
      navigation: auto;
    }

    .site-header {
      view-transition-name: site-header;
    }

    .product-card[data-id="7"] .thumb {
      view-transition-name: product-7;
    }
  </style>
</head>
<body>
  <header class="site-header">
    <a href="/">Shop</a>
  </header>
  <main>
    <article class="product-card" data-id="7">
      <img class="thumb" src="/img/product-7.webp" alt="商品7" width="320" height="240">
      <h2><a href="/products/7/">ワイヤレスイヤホン</a></h2>
    </article>
  </main>
</body>
</html>

詳細ページ /products/7/ 側では、ヒーロー画像に同じ view-transition-name: product-7 を付けます。ヘッダーは両ページで共通名 site-header により固定されているように見えるクロスフェードが可能です。

MPA の弱点は依然としてフル document ロードである点です。ここを Speculation Rules APIprefetch / prerender で補い、トランジション開始時点で次 HTML がキャッシュ済みである状態を作るのが 2026 年の推奨構成です。

SPA:ルーターと startViewTransition の統合

Single-Page Application(SPA) では、History API やルーター(React Router、Vue Router 等)が DOM を差し替えます。この更新関数全体を startViewTransition で包みます。

バニラ JS + History API の例

const app = document.querySelector('#app');
const links = document.querySelectorAll('a[data-spa]');

async function renderRoute(pathname) {
  const page = await import(`/pages${pathname === '/' ? '/index' : pathname}.js`);
  const content = await page.render();
  app.replaceChildren(content);
  document.title = page.title;
}

function navigate(pathname, push = true) {
  const update = () => {
    if (push) {
      history.pushState({ path: pathname }, '', pathname);
    }
    return renderRoute(pathname);
  };

  if (document.startViewTransition) {
    document.startViewTransition(update);
  } else {
    update();
  }
}

links.forEach((link) => {
  link.addEventListener('click', (event) => {
    event.preventDefault();
    const pathname = new URL(link.href).pathname;
    navigate(pathname);
  });
});

window.addEventListener('popstate', (event) => {
  const pathname = event.state?.path ?? location.pathname;
  if (document.startViewTransition) {
    document.startViewTransition(() => renderRoute(pathname));
  } else {
    renderRoute(pathname);
  }
});

SPA では初回ロード後の遷移は JavaScript バンドルが既にメモリ上にあるため、ネットワークより CRP 上の JS 実行・再レンダー が支配的になります。View Transitions は「見た目の連続性」を与えますが、データ取得が遅い問題は解決しない点に注意してください。

Astro 統合(2026)

Astroastro:transitions モジュールで View Transitions を第一級サポートしています。<ViewTransitions /> をレイアウトに1行足すだけで、同一サイト内 <a> のクライアントサイドナビゲーションにトランジションが適用されます。

レイアウトの基本構成

---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
import '../styles/global.css';

interface Props {
  title: string;
  description?: string;
}

const { title, description = '' } = Astro.props;
---

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content={description} />
    <title>{title}</title>
    <ViewTransitions />
  </head>
  <body>
    <header transition:persist="site-header">
      <slot name="header" />
    </header>
    <main>
      <slot />
    </main>
  </body>
</html>

ページ単位のアニメーション指定

---
// src/pages/blog/[slug].astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { fade, slide } from 'astro:transitions';

const { entry } = Astro.props;
---

<BaseLayout title={entry.data.title} description={entry.data.description}>
  <article transition:animate={fade({ duration: '0.3s' })}>
    <h1 transition:animate={slide({ duration: '0.25s' })}>{entry.data.title}</h1>
    <img
      src={entry.data.heroImage}
      alt=""
      transition:name={`hero-${entry.slug}`}
      width="1200"
      height="630"
    />
    <div class="prose">
      <entry.Content />
    </div>
  </article>
</BaseLayout>

Astro の transition:persist は、ヘッダーや動画プレイヤーなどナビゲーション後も DOM インスタンスを維持したい要素に使います。React の state を保持したい島には transition:persist="player"client:visible を組み合わせるパターンが一般的です。

一覧 → 詳細の共有要素

一覧ページ src/pages/products/index.astro

---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';

const products = await getCollection('products');
---

<BaseLayout title="商品一覧">
  <ul class="grid">
    {products.map((product) => (
      <li>
        <a href={`/products/${product.id}/`}>
          <img
            src={product.data.thumb}
            alt={product.data.title}
            transition:name={`product-${product.id}`}
            width="240"
            height="180"
          />
          <span>{product.data.title}</span>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>

詳細ページでは同じ transition:name="product-{id}"(例: product-7)をヒーロー画像に指定します。Astro は内部で view-transition-name に相当する属性を付与し、クライアントナビゲーション時に startViewTransition 相当の処理を行います。

Astro + React 島での注意点

React コンポーネントを client:load した場合、トランジション中に hydration タイミングがずれるとちらつくことがあります。対策は次のとおりです。

  • 共有要素は可能な限り Astro テンプレート側の静的 <img> に置く
  • インタラクティブ部分だけ React 島に分離する
  • 初回表示で不要な島は client:visible に下げ、CRP を軽くする

React / Next.js 統合

React 自体に View Transitions 専用 APIはありません。状態更新やルーター遷移を document.startViewTransition で包むパターンが基本です。React 18 の useTransition とは別物なので、命名を混同しないこと。

React 19 + 手動ラップの例

'use client';

import { useState, useCallback, type ReactNode } from 'react';

type ViewTransitionFn = (callback: () => void | Promise<void>) => {
  finished: Promise<void>;
  skipTransition: () => void;
};

declare global {
  interface Document {
    startViewTransition?: (callback: () => void | Promise<void>) => ReturnType<ViewTransitionFn>;
  }
}

function withViewTransition(update: () => void | Promise<void>) {
  if (typeof document !== 'undefined' && document.startViewTransition) {
    return document.startViewTransition(update);
  }
  void Promise.resolve(update());
  return { finished: Promise.resolve(), skipTransition: () => {} };
}

export function TabPanel({ tabs }: { tabs: { id: string; label: string; body: ReactNode }[] }) {
  const [activeId, setActiveId] = useState(tabs[0]?.id ?? '');

  const selectTab = useCallback((id: string) => {
    withViewTransition(() => {
      setActiveId(id);
    });
  }, []);

  const active = tabs.find((t) => t.id === activeId);

  return (
    <div>
      <div role="tablist" aria-label="セクション">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            role="tab"
            type="button"
            aria-selected={tab.id === activeId}
            onClick={() => selectTab(tab.id)}
          >
            {tab.label}
          </button>
        ))}
      </div>
      <div
        role="tabpanel"
        style={{ viewTransitionName: active ? `panel-${active.id}` : 'none' } as React.CSSProperties}
      >
        {active?.body}
      </div>
    </div>
  );
}

Next.js App Router

Next.js 14+ App Router では next/link のクリックを横取りせず、View Transitions 実験的統合はコミュニティパターンが中心です。実務では次のいずれかです。

  1. <ViewTransitions /> 相当を自前で入れない — MPA として @view-transition のみ使う(フルリロード + cross-document)
  2. クライアントコンポーネント内タブ・モーダルだけ startViewTransition を使う
  3. next-view-transitions 等のラッパーライブラリで Link をラップ(内部で startViewTransition + router.push)

App Router で SPA 的に全ページをトランジションさせると、RSC ペイロード取得とアニメーションの競合が起きやすいです。部分適用(ギャラリー・設定画面など同一レイアウト内)から始めるのが安全です。

React Router 7(2026)

React Router の unstable_viewTransition オプション(v6.20+ 系)を BrowserRouter または RouterProvider に渡すと、ナビゲーションを document.startViewTransition で包みます。

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter(
  [
    { path: '/', element: <HomePage /> },
    { path: '/album/:id', element: <AlbumPage /> },
  ],
  {
    future: {
      v7_startTransition: true,
    },
  }
);

export function App() {
  return (
    <RouterProvider
      router={router}
      future={{ unstable_viewTransition: true }}
    />
  );
}

各ページで共有要素を使う場合、CSS Modules や Tailwind の arbitrary property で view-transition-name: var(--hero-name) を設定し、ルートパラメータに応じて CSS 変数を切り替えます。

アクセシビリティ:prefers-reduced-motion

View Transitions は視覚効果が主であるため、前庭障害や動きに敏感なユーザーへの配慮が必須です。

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
  }
}

JavaScript 側では、ユーザー設定を尊重して API をスキップします。

function shouldAnimate() {
  return (
    typeof document.startViewTransition === 'function' &&
    !window.matchMedia('(prefers-reduced-motion: reduce)').matches
  );
}

function updateView(updateFn) {
  if (shouldAnimate()) {
    return document.startViewTransition(updateFn);
  }
  updateFn();
  return { finished: Promise.resolve() };
}

加えて、トランジション中は document.activeElement が失われないよう、終了後にフォーカスを論理的な先頭(見出しまたは main)へ移動させると、スクリーンリーダー利用者に優しいです。transition.finished.then(() => heading.focus()) のようなパターンが使えます。

パフォーマンス設計

View Transitions は GPU 合成を活用しますが、大きな DOM のスナップショットはメモリとペイントコストを消費します。

250
ルート fade 250ms: 250
ルート fade 250ms
350
共有要素 morph 350ms: 350
共有要素 morph 350ms
800
Speculation prerender 節約: 800
Speculation prerender 節約
900
無制限 fade 800ms+: 900
無制限 fade 800ms+

上図は目安のミリ秒です。アニメーション 250〜350ms が UX の甜点、800ms 超は「待たされている」体感に転じやすいです。一方、Speculation Rules で prerender できれば、遷移先の FCP を最大約 800ms 相当短縮できるケースもあり、短い morph + 先読みの組み合わせが最も効きます。

パフォーマンスのベストプラクティス

  • 共有要素は絞る — 同時に10個以上の view-transition-name は合成コスト増
  • 重い filter / blur を old/new に載せない — ペイント負荷が跳ね上がる
  • transition 中の layout thrashing を避ける — コールバック内で大量の offsetHeight 読み取りをしない
  • LCP 要素を old 側に長時間残さない — 新ページの LCP 計測開始を遅らせる可能性
  • INP を監視 — クリックから startViewTransition までの遅延を 50ms 以内に

DevTools の Performance パネルで ViewTransition マーカーが記録されます。アニメーションと Main thread の Long Task が重なっていないか確認してください。

実装手順(StepFlow)

1

[object Object]

2

[object Object]

3

[object Object]

4

[object Object]

5

[object Object]

トラブルシューティング

アニメーションが発生しない

  • document.startViewTransition 未対応ブラウザ — フォールバック分岐を確認
  • DOM 更新がコールバック外 — すべての変更をコールバック内(または同期連鎖)に含める
  • view-transition-name: none が親で上書き — DevTools の Computed タブで確認
  • 同一名前の要素が両方の状態に存在しない — ペアリングされずルートフェードのみになる

ちらつき・二重画像

  • 旧 new 両方に opacity アニメーションを付け fade 二重 — 片側だけにする
  • mix-blend-modeisolation の競合 — シンプルな animation: none から調整
  • フォント未ロード — await document.fonts.ready をコールバック末尾で待つ

スクロール位置

SPA では遷移後に window.scrollTo(0, 0) が必要な場合があります。startViewTransition のコールバック内で scroll を実行すると、キャプチャ後の新状態に反映されます。Astro は scrollBehavior オプションで制御可能です。

z-index とモーダル

モーダル表示中にページ遷移すると、スナップショットにモーダルが含まれ混乱します。遷移前にモーダルを閉じるか、view-transition-name から除外してください。

View Transitions と Speculation Rules の組み合わせ

Speculation Rules API は、<script type="speculationrules">prefetch / prerender 対象 URL を宣言します。View Transitions と併用すると、アニメーション中に次ページの HTML・CRP が進行し、トランジション終了直後にコンテンツが揃いやすくなります。

<script type="speculationrules">
{
  "prefetch": [
    {
      "source": "document",
      "where": {
        "selector_matches": "a[href^='/products/']"
      },
      "eagerness": "moderate"
    }
  ],
  "prerender": [
    {
      "source": "document",
      "where": {
        "selector_matches": "a[href^='/products/'].featured"
      },
      "eagerness": "conservative"
    }
  ]
}
</script>

MPA + @view-transition + Speculation Rules は、SEO フレンドリーなままアプリ風 UX を実現する 2026 年の王道の1つです。SPA 全置き換えより JS 初期ペイロードが小さい場合も多く、クリティカルレンダリングパスの観点でも有利になりやすいです。

セキュリティと同一オリジン

Cross-document View Transitions は same-origin に限定されます。異なるオリジンへのリンクでは通常のフルリロードとなり、スナップショット合成は行われません。これにより、悪意あるサイトへの遷移で旧ページの画像が漏れる心配はありません。Cross-Origin-Opener-Policyno-referrer 設定とは独立ですが、サードパーティ iframe 内では親 document のトランジションとは別世界である点に注意してください。

ライブラリとの使い分け

手段向いているケース
View Transitions API標準優先、MPA/SPA 両方、CSS 中心のチーム
Astro ViewTransitionsAstro サイト、コンテンツ中心、部分 hydration
Framer Motion layoutId複雑な spring・ドラッグ、React 専用、細かい物理モデル
Barba.jsレガシー MPA、jQuery 資産あり

新規プロジェクトでは 標準 API で足りる範囲を先に使い、足りない部分だけライブラリを足す方針が保守コスト的に有利です。

関連記事

この記事は Webパフォーマンス テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。

トピック記事
HTTP/2とHTTP/3の違いHTTP/2とHTTP/3の違い|QUICがWeb表示速度を変える理由
fetchpriority属性でLCP画像を最適化するfetchpriority属性でLCP画像を最適化する|2026年実践ガイド
Web Vitals INP改善ガイドWeb Vitals INP改善ガイド|scheduler.yield・Long Task・Input Delay対策(2026)
content-visibility: auto 完全ガイドcontent-visibility: auto 完全ガイド|遅延レンダリングと長尺ページ最適化 2026
Server-Timing API 完全ガイドServer-Timing API 完全ガイド|Express ミドルウェアと Chrome DevTools 連携
Brotli vs Gzip 圧縮比較Brotli vs Gzip 圧縮比較|nginx静的・動的設定とCDN実践ガイド
CSS/JSを圧縮(Minify)するメリットCSS/JSを圧縮(Minify)するメリット|サイト高速化の基本
Service Worker キャッシュ戦略ガイドService Worker キャッシュ戦略ガイド|PWA の cache-first・network-first・SWR・Workbox 設計
WebP変換で画質が落ちる原因と最適な設定WebP変換で画質が落ちる原因と最適な設定
画像圧縮とWebP変換画像圧縮とWebP変換|PageSpeed改善の基本
HTML Popover API 完全ガイドHTML Popover API 完全ガイド|popover・popovertarget・light dismiss・アクセシビリティ
OAuth2 PKCE SPA実装ガイドOAuth2 PKCE SPA実装ガイド|Authorization Code + S256 + BFFパターン

まとめ

View Transitions API は、document.startViewTransitionview-transition-name を軸に、同一 document 更新から MPA のクロスドocument 遷移までを CSS 疑似要素で統一的に扱える 2026 年の中核 UX 技術です。

実務では次の順で導入するのが効率的です。

  1. プログレッシブエンハンスメント — 未対応ブラウザでは即時 DOM 更新
  2. 短いアニメーション + 共有要素は少数 — 250〜400ms、名前付き要素は1〜3個
  3. フレームワーク統合 — Astro <ViewTransitions />、React Router unstable_viewTransition
  4. 計測と先読みCRP 最適化Speculation Rules をセット
  5. アクセシビリティprefers-reduced-motion とフォーカス復帰

「速く見せる」と「実際に速くする」は別問題です。View Transitions は前者を、Speculation Rules と CRP 最適化は後者を担当します。3つを組み合わせたとき、初めて ネイティブアプリに近い体感Web の配信モデルの強みを両立できます。