【テクニカル・上級編】 ::view-transition-new 擬似要素による遷移後状態 – CSS実践ガイド

こんにちは。ブラウザの描画パイプラインの裏側でうごめくピクセルどもの挙動に、今日も夜な夜なニヤついているフロントエンド・アーキテクトだ。

近年のモダンブラウザにおける最大の革命の一つが「View Transitions API」であることは論を俟たない。SPAのページ遷移や、DOMの非同期書き換え時に、ブラウザが勝手にクロスフェードやモーフィングをやってくれる夢のような仕組みだ。このAPIの心臓部であり、遷移後の世界を支配する支配者が、今回深掘りする `::view-transition-new` 擬似要素にほかならない。

公式ドキュメントには「新しい状態のビューです。アニメーションさせることができます」としか書いてない。だが、現場のシビアなプロダクション環境でこれを素直に使おうものなら、レイアウトシフトの嵐、メモリの無駄遣いによるガタつき、そして非同期データ取得との競合による「虚無の黒画面」という名の地獄を見る。

今日は、この `::view-transition-new` を手懐け、堅牢かつ極上の滑らかさを誇るWebアプリケーションを構築するためのアーキテクチャと実践知を、余すところなく語り尽くそう。

—

1. 内部挙動の理解:`::view-transition-new` はDOMではない、GPUの「亡霊」だ

まず大前提として知っておくべきなのは、`::view-transition-old` と `::view-transition-new` が生成する擬似要素は、実際のDOMツリーの要素ではないという点だ。ブラウザが遷移開始の瞬間(`document.startViewTransition()` が呼ばれた瞬間)にスナップショットを撮影し、それらを合成レイヤー(Compositing Layer)に放り込んだ「画像(Rasterized Image)の皮を被ったテクスチャ」に過ぎない。

ここで `::view-transition-new(名)` は、遷移後のDOMの見た目をキャプチャしたレイヤーを指す。
このレイヤーがレンダリングパイプライン上でどう扱われるかを知ることは、パフォーマンス最適化において絶対的な正義となる。

/ デフォルトのグループアニメーションを上書きする基本形 /
::view-transition-new(root) {
/ GPUアクセラレーションを強制し、メインスレッドを汚染しない /
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

このコードを書いた瞬間、ブラウザのコンポジターはGPU上でこのテクスチャに対して `transform` や `opacity` のアニメーションを適用する。ここで重要なのは、レイアウト(Reflow)やペイント(Repaint)のコストが完全にゼロになるという点だ。CPUを一切刺激せず、GPUのメモリ帯域だけでアニメーションを完結させる。これぞシニアエンジニアが求める極上のパフォーマンスである。

—

2. メモリ効率とレイヤー爆発の回避

しかし、力には代償が伴う。View Transitions APIは、遷移に関与するすべての要素(デフォルトでは `root`、カスタム名を付けた場合は個別の要素)に対して、それぞれ「旧」と「新」の画像テクスチャをVRAM上に生成する。

もし、SPA内で大量のリストアイテム(例:100個のカード)にそれぞれ `view-transition-name: card-1`, `card-2`… と命名したらどうなるか?
ブラウザは一瞬にして数百枚の高解像度テクスチャをVRAMに展開しようとし、モバイル端末や低スペックなラップトップではメモリ不足(OOM)によるタブクラッシュを引き起こす。これが「レイヤー爆発」という現場の悪夢だ。

対策:カスタムネーミングは「主役」にだけ絞れ

無闇矢鱈と `view-transition-name` を乱発してはならない。アニメーションさせるのは、ユーザーの視線誘導に必要な「主役(ヒーロー要素)」だけで十分だ。

/ ユーザーがクリックした特定のカードだけに動的命名を付与する /
.product-card-active {
view-transition-name: active-product;
}

::view-transition-new(active-product) {
/ テクスチャ自体のサイズが遷移前後で異なる場合のスケール補正 /
animation: none; / デフォルトのアニメーションを殺してカスタム制御する場合 /
}

動的に `view-transition-name` をJavaScriptでアタッチ・デタッチする場合、遷移が終わった瞬間に速やかにスタイルをクリーンアップすること。DOMに残しっぱなしにすると、ブラウザの内部的なペイントツリーのインデックスが肥大化し続け、メモリリークの温床になる。

—

3. 非同期の競合と「虚無の黒画面」バグの防衛策

実務で最も遭遇するクリティカルなバグが、「データの非同期フェッチ完了前に遷移アニメーションが発火し、真っ黒な画面や中途半端なレイアウトがキャプチャされる現象」だ。

`document.startViewTransition()` のコールバック内で `await fetch(…)` などを実行する場合、ブラウザは非同期処理の完了を待ってからスナップショットを撮る。しかし、APIのデータ構造が変わる瞬間と、CSSアニメーションのタイミングがズレると、`::view-transition-new` が描画する「新しい状態」が、まだレンダリングされていない空っぽのDOMを捉えてしまうことがある。

これを防ぐためには、CSS側で「新レイヤーの初期状態」を強制的に隠す、あるいはフェードインのタイミングをデータ描画完了までディレイさせる設計が必要になる。

/ 遷移後の初期フレームでレイアウトの暴れを隠蔽するアーキテクチャ /
::view-transition-new(main-content) {
/ データのレンダリングが確実に終わるまで、新レイヤーの表示をわずかに遅らせるか、
あるいはクロスフェードのブレンドを制御する /
animation:
300ms cubic-bezier(0.4, 0, 0.2, 1) 50ms both,
fadeIn 300ms ease-in-out;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px); / わずかに下から浮き上がる演出でごまかす /
}
to {
opacity: 1;
transform: translateY(0);
}
}

この「50msのディレイ」や「`transform: translateY(8px)`」という微調整は、単なるデザインの好みではない。非同期処理のわずかなレンダリング遅延をユーザーに知覚させないための、極めて高度な心理的・技術的ハックなのだ。

—

4. 複数要素が絡み合う複雑な遷移の制御とセレクタのスコープ

SPAにおいて、ページ全体が切り替わるのではなく、一部のコンポーネント(例えばサイドバーとメインパネル)が同時に異なる方向からアニメーションする場合、`::view-transition-new` のセレクタを適切にグループ化しなければ、CSSの競合(Specificity Wars)に敗北する。

/ ─── ケーススタディ:ダッシュボードの非対称な遷移制御 ─── /

/ サイドバーは左からスライドイン /
::view-transition-new(sidebar) {
animation: slideInFromLeft 0.35s cubic-bezier(0.1, 0.9, 0.2, 1);
}

/ メインコンテンツは下からフェードイン /
::view-transition-new(main-panel) {
animation: slideInFromBottom 0.4s cubic-bezier(0.1, 0.9, 0.2, 1);
}

@keyframes slideInFromLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}

@keyframes slideInFromBottom {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

ここでプロフェッショナルとして注意すべきは、「アニメーションの持続時間(duration)」の不一致だ。複数の `::view-transition-new` でバラバラの時間を指定すると、ブラウザ全体のビュー遷移コンテナ(`::view-transition`)のライフサイクルが、最も長いアニメーションが終わるまで解放されなくなる。これにより、アニメーションが終了しているにもかかわらず、わずかな間インタラクションがブロックされる「死んだ時間」が生まれる。

すべてのカスタム `::view-transition-new` のアニメーション時間は、厳密に統一するか、親である `::view-transition` 全体のタイミング設計と同期させなければならない。

—

5. アクセシビリティ(A11y)という絶対的な免責事項

最後に、ギークとして、そしてプロダクトを愛するエンジニアとして絶対に忘れてはならないのが `prefers-reduced-motion` だ。

View Transitions APIによる派手なモーフィングやスライドは、前庭系疾患を持つユーザーにとって、めまいや吐き気を催す「動く公害」になり得る。`::view-transition-new` を駆使したリッチな表現は、アクセシビリティを犠牲にしてまで実装する価値など微塵もない。

必ず、メディアクエリで一刀両断に無効化するアーキテクチャを組み込んでおこう。

/ ユーザーがモーション削減を求めている場合、全てのView Transitionアニメーションを殺す /
@media (prefers-reduced-motion: reduce) {
::view-transition-old(),
::view-transition-new(),
::view-transition-group() {
animation: none !important;
}
}

この数行を書くだけで、あなたのアプリケーションは単なる「動くおもちゃ」から、世界中のあらゆるユーザーを包摂する「プロフェッショナルなソフトウェア」へと昇華する。

—

総括

`::view-transition-new` は、単なる「アニメーションを当てるためのセレクタ」ではない。それは、DOMのリアリティとGPUのコンポジット空間を架橋し、非同期の混沌を美しい秩序へと変換するための魔法のインターフェースだ。

しかし、その裏にあるメモリの制約、非同期のタイミング問題、そしてアクセシビリティへの配慮を怠れば、途端にアプリを破滅へと導くパンドラの箱にもなり得る。

ブラウザの内部挙動を愛し、ピクセル単位の最適化に魂を燃やす我々フロントエンド・アーキテクトにとって、このAPIを完全に制御下に置くことこそが、真の腕の見せ所だ。さあ、エディタを開き、無駄なレイヤーを削ぎ落とし、極上の滑らかさをコードに宿そう。

コメント

タイトルとURLをコピーしました