📝

View TransitionでわかるNext.jsとAstroの設計思想

に公開

PLAID Designer's Advent Calendar 11日目の記事です。こんにちは。株式会社プレイドのDirection & Development Unit所属のデザインエンジニア、ケンジです。

はじめに

ウェブフロントエンド開発のエコシステムは成熟期を迎えました。「シングルページアプリケーション(SPA)」と「マルチページアプリケーション(MPA)」という長年の二項対立。この議論は、View Transition APIの標準化と普及により、その境界線を溶解させつつあります。

この新しいウェブ標準APIは、MPAであってもSPAのようなシームレスなページ遷移ができます。SPAにおいてはアニメーションライブラリに依存せず、宣言的で滑らかな遷移アニメーションができます。

しかし、このAPIはブラウザに組み込まれた機能であるため、各フレームワークは、「自分たちのコアアーキテクチャにどのように取り込むか」という課題に直面しました。

この記事ではNext.js(App Router)とAstroに焦点を当て、各フレームワークがView Transition APIを「どのように実装し、統合しているか」を解説します。View Transition APIという共通のレンズを通すことで、React Server Components(RSC)による「状態駆動型」と、Islands Architectureによる「DOM駆動型」、それぞれの根本的なアーキテクチャ思想の差異を浮き彫りにすることを目的とします。

View Transition APIの動作原理

各フレームワークの実装に踏み込む前に、基盤となるブラウザのメカニズムを整理しましょう。これは単なるCSSアニメーションの拡張ではなく、ブラウザのレンダリングパイプラインへの深い介入です。

https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API

View Transition APIの核となるのは、document.startViewTransition() です。このメソッドが呼び出されると、ブラウザはレンダリングループを一時停止し、厳密なステップを踏みます。

  1. 現在の画面を画像としてキャプチャします(Old Snapshot)。
  2. レンダリングを凍結し、ユーザーにはキャプチャしたスナップショットを表示し続けます。
  3. コールバック関数を実行し、DOMの更新を完了させます。
  4. 更新後の新しい画面をキャプチャします(New Snapshot)。
  5. 新旧のスナップショット間で、CSSに基づいたクロスフェードや位置移動のアニメーションを実行します。

このデモサイトも触ってみてください。より自然なページ遷移が体験できるはずです。

AstroのDOM駆動アプローチ

AstroはIslands Architectureを標榜し、デフォルトでJavaScriptを排除するHTMLファーストのフレームワークです。Astroで作られたサイトは典型的なMPAであり、本来ページ遷移は「ドキュメントの破棄と再読み込み」を意味します。

しかし、View Transition APIをサポートするために導入された <ClientRouter />(旧 <ViewTransitions />)は、このMPAの前提を覆します。

index.astro
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="ja">
  <head>
    <title>title</title>
    <ClientRouter /> // ClientRouterコンポーネントをimportして使用します。
  </head>
  <body>
    <h1>Astro Demo</h1>
    <a href="/page2">
      次のページへ
    </a>
  </body>
</html>

なぜAstroは「MPAのふり」をするのか

AstroにおけるView Transitionの実装は、いわば「MPAのふりをしたSPA」です。Astroは本来、サーバーファーストのMPAフレームワークです。<ClientRouter />を導入した瞬間、ブラウザ上での実行モデルはJavaScriptコンテキストを持つSPAになります。なぜAstroは、MPAというアイデンティティを持ちながら、わざわざSPAのようなクライアントサイドルーターを導入したのでしょうか。そこには、View Transition APIの仕様における2つのモードが関係しています。

  1. Same-Document View Transitions (通称Level 1)
    単一のドキュメント内でDOMを書き換えるモード。SPAでの利用が想定されています。JSでAPIを呼び出し、コールバック内でDOMを更新します。ブラウザはページ遷移と認識せず、JSの状態は維持されます。

  2. Cross-Document View Transitions (通称Level 2)
    異なるドキュメント間を遷移するモード。MPAでの利用が想定されています。CSSのみで有効化可能ですが、遷移中の細かい制御や複雑な状態の引継ぎには制約が伴います。

本来MPAであるAstroは「Cross-Document (Level 2)」と相性が良いはずです。しかし、Level 2はまだ仕様策定やブラウザ実装の途上にあります。そこでAstroは、現時点でリッチな体験を提供するため、「MPAのふりをしたSPA」として振る舞う実用的な道を選びました。

<ClientRouter /> がリンク操作を捕捉し、次ページのHTMLを取得と書き換えする。これにより擬似的にSame-Document環境を作り出し、MPAの簡潔さを保ちつつSPA並みのリッチな制御を可能にしています。

transition:persist による物理的なDOMスワップ

この擬似的なSPA化において、Astroが提供する機能であるtransition:persistです。

通常、bodyをHTML文字列で入れ替えれば、<video> の再生位置や <canvas> の描画内容はリセットされます。これを防ぐため、AstroはDOMノードの物理的なスワップ(移植)を行います。

スワップ直前、対象のDOMノードを現在のツリーから切断し、メモリ上に退避させます。この時点でノードはドキュメントから外れますが、JSインスタンスとして内部状態を保持し続けます。新しいHTML挿入後、対応する部分を退避させた「生きたDOMノード」で置き換えます。

これはReactの差分検知とは根本的に異なります。Reactが「コンポーネントの状態を維持して再描画」するのに対し、Astroは「物理的なDOMノードそのものを移動」させます。フレームワーク固有のライフサイクルに依存せず、ブラウザネイティブの状態を維持できる点は、AstroのDOM駆動思想を象徴しています。

Next.jsにおける状態駆動アプローチ

一方、Next.jsにおけるView Transitionは、アプローチが全く異なります。ブラウザのネイティブAPIを直接使わず、Reactの同時実行レンダリング(Concurrent Rendering)へ、同期的なDOM更新を「埋め込む」よう統御しています。

その構造は、大きく3つのフェーズで動作します。

1. RSC Payloadによる論理更新

ナビゲーションがトリガーされると、Next.jsは新しいHTMLページではなく、RSC Payload(シリアライズされたReact Server Componentsのデータ)を取得します。

  1. サーバーでレンダリングされた差分データ(Payload)がクライアントに流れてきます。
  2. クライアント上のReactランタイムがこれをデシリアライズし、React Element(UIの定義情報)を生成します。
  3. 生成されたReact Elementをもとに、メモリ上のFiber Tree(アプリケーションの状態を保持する内部ツリー)をマージします。

ここでは物理的なDOMの総入れ替えではなく、React内部のFiber Tree上での論理的なマージが行われます。これにより、Fiber Nodeに保持されているState(動画の再生状態やフォームの入力値など)が維持されます。

2. useTransitionによる表示タイミングの制御

View Transition APIは「遷移完了後の完成された画面」へのアニメーションを前提とします。しかし、Reactのレンダリング(特にStreamingやSuspenseを伴う場合)は非同期かつ段階的です。このギャップを埋めるのが useTransition です。

  1. ナビゲーション開始時、Reactは isPending 状態になり、バックグラウンド(メモリ上のFiber Tree)で次の画面のレンダリングを開始します。
  2. データ取得が完了し、次の画面の描画準備が整うまで、現在のDOM(画面)を一切変更せず維持します。
  3. 準備完了のシグナルが出た瞬間に、次のフェーズへ移行します。

ブラウザが勝手に遷移を始めるのを防ぎ、Reactが「いつ遷移を開始するか」の全権限を掌握します。これにより「ローディングスピナーが表示される画面への遷移」といった不要な中間状態を防いでいます。

3. flushSyncによる強制的DOM更新

ここがNext.js (React) の実装において難解であり、かつ凄みを感じる点です。ブラウザの document.startViewTransition(callback) は、callback 内でDOMが同期的に更新されることをリクエストします。しかし、Reactのコミットフェーズ(Fiber TreeからDOMへの反映)は、Concurrent Featuresにおいては中断か非同期になることがあります。

そこでReactは以下のような挙動をとります。

  1. Reactは準備が整った新しいFiber TreeをDOMに反映する際、並列処理の「優先度付き更新」を一時停止します。
  2. startViewTransition のコールバック関数内で、flushSync 相当の処理を実行し、強制的に同期的なDOMコミットを行います。
  3. これにより、ブラウザは「Old Snapshot」と「New Snapshot」の間にあるDOMの変化を、一瞬の空白もなく連続的にキャプチャできます。

非同期で動くReactのFiberアーキテクチャと、同期を求めるブラウザAPIの世界を接続するためのブリッジ処理がランタイム深部に組み込まれているのです。

つまり、View Transition APIと足並みを揃えるために、Reactは計算済みの全てのDOM変更(コミットフェーズ)を単一のタスクとして実行せざるを得ないことを意味します。巨大なFiber Treeの変更を伴う場合、このDOM反映処理が完了するまでメインスレッドは占有されます。リッチなアプリケーションでは、この一瞬のフリーズがユーザー体験に影響する可能性すらありますが、そこまでしてもReactは「状態の一貫性」を優先しているのです。

まとめ

AstroとNext.jsは、同じView Transition APIを利用しながらも、そのアプローチは対照的です。

特徴 Astro (DOM駆動) Next.js (状態駆動)
遷移の仕組み HTMLをFetchしてDOMを総入れ替え RSC PayloadをFetchしてFiber Treeをマージ
状態の維持 transition:persist による物理的なDOM退避 React State / Context による論理的な維持
JSへの依存 小(必要な部分のみIslandsとして動作) 大(巨大なランタイムがDOMを管理)
設計思想 Polyfillぽさ。
将来的に標準API (Level 2) に準拠し、ルーターを捨てられる設計。
統合ぽさ。
標準APIを素材として取り込み、独自のUXを提供するためのランタイムへ統合。

Astroのアプローチは、将来的なウェブ標準を見据えたPolyfillに近いと言えます。DOMの置換と要素の永続化による実装は、ウェブ標準に寄り添う設計です。ブラウザがCross-Document View Transitions (Level 2) をサポートした際には、<ClientRouter /> を削除しても動作することを目指しています。「今は泥臭いことをしてでも、標準へ橋渡しをする」という気概を感じます。

対してNext.jsは、View TransitionをReactエコシステムの一部として取り込んでいます。SuspenseやServer Actions、そしてConcurrent Renderingと協調させることで、コンポーネント単位での状態管理とリッチな体験を追求しています。これはウェブ標準をあくまで「素材」として使い、ブラウザの限界を超えた独自のアプリケーションランタイムを構築しようとします。

どちらが優れているという話ではありません。ただ、View Transitionという1つの機能を通して見ると、両者の設計思想のコントラストが鮮明になります。「薄く保ち標準に準拠するAstro」と、「厚く作り込み、体験を支配するNext.js」。この対比が浮き彫りになるのです。


あと、せっかくなので、いただいたサムネイルを貼ります。Zennではサムネイルを設定できないみたいなので。
thumbnail

PLAID Designer’s Advent Calendar 2025 12日目は kazukiさんのUIは死なない —AI時代のUIデザインです。

GitHubで編集を提案

Discussion