1. 始める前に
この Codelab では、Google Chrome でネイティブにサポートされている最新の API を使用して、サンプル ウェブアプリで高速ナビゲーションとシームレスなページ遷移を実現する方法について説明します。
サンプル ウェブアプリは、人気の果物や野菜の栄養価を調べるもので、果物の一覧と詳細ページはシングルページ アプリ(SPA)として、野菜の一覧と詳細ページは従来のマルチページ アプリ(MPA)として作成されています。

具体的には、事前レンダリング、バックフォワード キャッシュ(bfcache)、Private Prefetch Proxy を実装して高速ナビゲーションを実現し、ルート / 共有要素の遷移を実装してシームレスなページ遷移を可能にします。MPA ページでは事前レンダリングと bfcache を、SPA ページでは共有要素の遷移を実装します。
サイトの速度は、ユーザー エクスペリエンスにおいて常に重要な要素となるため、Google では Core Web Vitalsを導入して、その一連の指標を使ってウェブページの読み込みパフォーマンス、インタラクティビティ、視覚安定性を測定し、実際のユーザー エクスペリエンスを評価しています。最新の API を使用すると、実際のウェブサイトで、Core Web Vitals の、特に読み込みのパフォーマンスを向上させることができます。

Mindvalleyのデモ
ユーザーはまた、モバイル ネイティブ アプリで遷移を使って極めて直感的に移動したり状態を変更したりすることに慣れています。しかしながら、そのようなユーザー エクスペリエンスをウェブで再現するのは簡単ではありません。現在のウェブ プラットフォーム API でも同様の効果を得られるかもしれませんが、開発作業は、特に Android アプリや iOS アプリの同等の機能と比べて難しいか複雑になりすぎる可能性があります。シームレスな API は、こうしたアプリとウェブにおけるユーザーとデベロッパー間のエクスペリエンス ギャップを埋めるためのものです。

前提条件
次の知識があること。
- HTML
- CSS
- JavaScript
- Google Chrome デベロッパー ツール
演習内容
実装方法:
- 事前レンダリング
- bfcache
- Private Prefetch Proxy
- ルート/共有要素の遷移
作成するアプリの概要
サンプル ウェブアプリを Next.js で作成し、最新の高速かつシームレスなブラウザ機能でアプリを強化する。
- 事前レンダリングにより高速ナビゲーションと同等の機能を実現
- bfcache を使ってブラウザの [戻る] / [進む] ボタンで即座に読み込み
- Private Prefetch Proxy または Signed Exchange(SXG)を使用したクロスオリジン ナビゲーションにより第一印象を向上
- ルート/共有要素の遷移によりページ間をシームレスに移動
必要なもの
- Chrome バージョン 101 以降
2. 始める
Chrome フラグを有効にする
- about://flags に移動し、
Prerender2とdocumentTransition APIのランタイム フラグを有効にします。 - ブラウザを再起動する。
コードを取得する
- お使いの開発環境で、こちらの GitHub リポジトリのコードを開きます。
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
- サーバーの実行に必要な依存関係をインストールします。
npm install
- ポート 3000 でサーバーを起動します。
npm run dev
- ブラウザで http://localhost:3000 に移動します。
これで、アプリを編集、改善できるようになりました。アプリは変更を加えるたびに再読み込みされ、変更を直接確認できます。
3. 事前レンダリングを統合する
このデモでは、サーバー側での任意の遅延により、サンプルアプリで野菜の詳細ページの読み込み時間が非常に長くなっていて、この待機時間を事前レンダリングを使って解消します。
野菜の一覧ページに事前レンダリング ボタンを追加して、ユーザーがクリックすると事前レンダリングをトリガーできるようにするには:
- Speculation Rules スクリプトタグを動的に挿入するボタン コンポーネントを作成します。
components/prerender-button.js
import { useContext } from 'react'
import ResourceContext from './resource-context'
// You use resource context to manage global states.
// In the PrerenderButton component, you update the prerenderURL parameter when the button is clicked.
export default function PrerenderButton() {
const { dispatch } = useContext(ResourceContext)
const handleClick = (e) => {
e.preventDefault()
e.stopPropagation()
const parent = e.target.closest('a')
if (!parent) {
return
}
const href = parent.getAttribute('href')
dispatch({ type: 'update', prerenderURL: href })
}
return (
<button className='ml-auto bg-gray-200 hover:bg-gray-300 px-4 rounded' onClick={handleClick}>
Prerender
</button>
)
}
list-item.jsファイルにPrerenderButtonコンポーネントをインポートします。
components/list-item.js
// Codelab: Add a PrerenderButton component.
import PrerenderButton from './prerender-button'
...
function ListItemForMPA({ item, href }) {
return (
<a href={href} className='block flex items-center'>
<Icon src={item.image} />
<div className='text-xl'>{item.name}</div>
{/* Codelab: Add PrerenderButton component. */}
<PrerenderButton />
</a>
)
}
- Speculation Rules API を追加するコンポーネントを作成します。
SpeculationRules コンポーネントは、アプリで prerenderURL の状態が更新されると、スクリプトタグをページに動的に挿入します。
components/speculationrules.js
import Script from 'next/script'
import { useContext, useMemo } from 'react'
import ResourceContext from './resource-context'
export default function SpeculationRules() {
const { state } = useContext(ResourceContext)
const { prerenderURL } = state
return useMemo(() => {
return (
<>
{prerenderURL && (
<Script id='speculationrules' type='speculationrules'>
{`
{
"prerender":[
{
"source": "list",
"urls": ["${prerenderURL}"]
}
]
}
`}
</Script>
)}
</>
)
}, [prerenderURL])
}
- コンポーネントをアプリと統合します。
pages/_app.js
// Codelab: Add the SpeculationRules component.
import SpeculationRules from '../components/speculationrules'
function MyApp({ Component, pageProps }) {
useAnalyticsForSPA()
return (
<ResourceContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
{/* Codelab: Add SpeculationRules component */}
<SpeculationRules />
<Script id='analytics-for-mpa' strategy='beforeInteractive' src='/analytics.js' />
</ResourceContextProvider>
)
}
export default MyApp
- [事前レンダリング] をクリックします。
これで、読み込みが大幅に改善されたことを確認できます。実際のユースケースでは、事前レンダリングは、一定の経験則に基づいてユーザーが次にアクセスする可能性が高いページでトリガーされます。

アナリティクス
デフォルトでは、DOMContentLoaded イベントが発生すると、サンプル ウェブアプリの analytics.js ファイルからページビュー イベントが送信されます。しかし、このイベントは事前レンダリングのフェーズで呼び出されるため、そのままではうまくいきません。
document.prerendering と prerenderingchange イベント を導入してこの問題を解決するには:
analytics.jsファイルを書き換えます。
public/analytics.js
const sendEvent = (type = 'pageview') => {
// Codelab: Make analytics prerendering compatible.
// The pageshow event could happen in the prerendered page before activation.
// The prerendered page should be handled by the prerenderingchange event.
if (document.prerendering) {
return
}
console.log(`Send ${type} event for MPA navigation.`)
fetch(`/api/analytics?from=${encodeURIComponent(location.pathname)}&type=${type}`)
}
...
// Codelab: Make analytics prerendering compatible.
// The prerenderingchange event is triggered when the page is activated.
document.addEventListener('prerenderingchange', () => {
console.log('The prerendered page was activated.')
sendEvent()
})
...
事前レンダリングに対応できるよう分析処理に変更を加えたら、ブラウザ コンソールに適切なタイミングでページビュー ログを表示できるようになります。
4. bfcache の阻害要因を排除する
unload イベント ハンドラを削除する
不要な unload イベントを使用することはよくある間違いで、現在は推奨されていません。これによって bfcache が機能しなくなり、また信頼性も低く、たとえば、モバイルや Safari で常に配信されるわけではありません。
そこで、unload イベントの代わりに pagehide イベントを使用します。このイベントは、unload イベントが呼び出されたときと、ページが bfcache に保存されたときに必ず呼び出されます。
unload イベント ハンドラを削除するには:
analytics.jsファイルで、unloadイベント ハンドラのコードをpagehideイベント ハンドラのコードに置き換えます。
public/analytics.js
// Codelab: Remove the unload event handler for bfcache.
// The unload event handler prevents the content from being stored in bfcache. Use the pagehide event instead.
window.addEventListener('pagehide', () => {
sendEvent('leave')
})
cache-control ヘッダーを更新する
配信するページに Cache-control: no-store HTTP ヘッダーを指定すると、ブラウザの bfcache 機能が使用されなくなるため、このヘッダーの使用は控えめにすることをおすすめします。特に、パーソナライズされた情報や重要な情報(ログインの状態など)がページに含まれていなければ、Cache-control: no-store HTTP ヘッダー付きのページを配信する必要はありません。
サンプルアプリの cache-control ヘッダーを更新するには:
getServerSidePropsコードを変更します。
pages/vegetables/index.js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
pages/vegetables/[名前].js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res, query } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
ページが bfcache から復元されているかどうかを判別する
pageshow イベントは、ページが最初に読み込まれたとき(load イベントの直後)と、ページが bfcache から復元されたときに毎回呼び出されます。pageshow イベントには persisted プロパティがあり、ページが bfcache から復元された場合は true、そうでない場合は false に設定されます。この persisted プロパティを使用して、通常のページ読み込みと bfcache からの復元を区別できます。主要な分析サービスでは bfcache が認識されますが、ページが bfcache から復元されているかどうかを手動で確認してイベントを送信することもできます。
ページが bfcache から復元されているかどうかを判別するには:
- 次のコードを
analytics.jsファイルに追加します。
public/analytics.js
// Codelab: Use the pageshow event handler for bfcache.
window.addEventListener('pageshow', (e) => {
// If the persisted flag exists, the page was restored from bfcache.
if (e.persisted) {
console.log('The page was restored from bfcache.')
sendEvent()
}
})
ウェブページをデバッグする
Chrome デベロッパー ツールでは、ページをテストして、bfcache 向けに最適化されているかどうかを確認したり、bfcache が適用されない原因となる問題を特定したりできます。
特定のページをテストするには:
- Chrome で対象のページを表示します。
- Chrome デベロッパー ツールで、[アプリケーション] > [バックフォワード キャッシュ] > [テストを実行] をクリックします。
Chrome デベロッパー ツールでは、一度ページを離れて再度戻ることにより、bfcache からページを復元できるかどうかを判断します。

テストが成功すると、バックフォワード キャッシュからページが復元されたことがパネルに表示されます。

失敗した場合は、ページが復元されなかったこととその理由がパネルに表示されます。デベロッパーが対処可能な理由の場合はそのことも示されます。

5. クロスサイト プリフェッチを有効にする
プリフェッチは、データバイトの取得を早めに開始し、ユーザーが移動する際にはすでにブラウザに取得済みの状態にすることで、ナビゲーションを高速化します。Core Web Vitals を改善し、ナビゲーション前に一部のネットワーク アクティビティを軽減できる簡単な方法です。具体的には、 Largest Contentful Paint(LCP)を直接高速化してリソースを空けることによって、ナビゲーション時の First Input Delay(FID)と Cumulative Layout Shift(CLS)を改善します。
Private Prefetch Proxy を使用すると、クロスサイト プリフェッチを有効にしつつ、ユーザーの個人情報を送信先サーバーに開示しないようにできます。

Private Prefetch Proxy を使用したクロスサイト プリフェッチを有効にする
ウェブサイトの所有者は、よく知られている traffic-advice リソース(ウェブクローラで使用される /robots.txt のようなもの)を使用してプリフェッチを制御できます。これにより、エージェントの実装で対応するアドバイスを使用するよう HTTP サーバーで宣言できるようになります。現在のところ、ウェブサイトの所有者は、ネットワーク接続を禁止または制限するようエージェントにアドバイスすることが可能ですが、今後その他のアドバイスも追加される可能性があります。
traffic-advice リソースをホストするには:
- 次の JSON に似たファイルを追加します。
public/.well-known/traffic-advice
[
{
"user_agent": "prefetch-proxy",
"google_prefetch_proxy_eap": {
"fraction": 1
}
}
]
google_prefetch_proxy_eap フィールドは、早期アクセス プログラム用の特別なフィールドです。fraction フィールドは、Private Prefetch Proxy からリクエストされたプリフェッチの一部を制御するためのフィールドです。
トラフィックに関するアドバイスは、application/trafficadvice+json MIME タイプで返す必要があります。
next.config.jsファイルで、レスポンス ヘッダーを設定します。
next.config.js
const nextConfig = {
// Codelab: Modify content-type for traffic advice file.
async headers() {
return [
{
source: '/.well-known/traffic-advice',
headers: [
{
key: 'Content-Type',
value: 'application/trafficadvice+json',
},
],
},
]
},
}
module.exports = nextConfig
6. Shared Element Transitions API を統合する
ユーザーがウェブ上でページ間を移動し、最初のページが消えて新しいページが開くと、それまで表示されていたコンテンツは突然切り替わります。こうしたつながりのないユーザー エクスペリエンスが続くと、方向感覚が失われ、ユーザーは現在地までの経緯をつなぎ合わせることが必要となり、認知負荷が増します。また、目的のページが読み込まれるまでの待ち時間も長く感じられます。
そこで、スムーズに読み込まれるアニメーションを使用すると、ユーザーはページ間の移動中もそのコンテンツに留まるため認知負荷が減り、ページが読み込まれるまでの間もコンテンツを見て楽しめるので、待ち時間が短く感じられるようになります。こうした理由から、Android、iOS、MacOS、Windows などのほとんどのプラットフォームでは、デベロッパーがシームレスな遷移を実現できる使いやすいプリミティブが用意されています。
Shared Element Transitions API では、ドキュメント間(MPA)とドキュメント内(SPA)のどちらの遷移かにかかわらず、デベロッパーはウェブ上で同じ機能を使用できます。

サンプルアプリの SPA 部分で Shared Element Transitions API を統合するには:
use-page-transition.jsファイルで、遷移を管理するカスタムフックを作成します。
utils/use-page-transition.js
import { useEffect, useContext, useRef, useCallback } from 'react'
import ResourceContext from '../components/resource-context'
// Call this hook on this first page before you start the page transition. For Shared Element Transitions, you need to call the transition.start() method before the next page begins to render, and you need to do the Document Object Model (DOM) modification or setting of new shared elements inside the callback so that this hook returns the promise and defers to the callback resolve.
export const usePageTransitionPrep = () => {
const { dispatch } = useContext(ResourceContext)
return (elm) => {
const sharedElements = elm.querySelectorAll('.shared-element')
// Feature detection
if (!document.createDocumentTransition) {
return null
}
return new Promise((resolve) => {
const transition = document.createDocumentTransition()
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
transition.start(async () => {
resolve()
await new Promise((resolver) => {
dispatch({ type: 'update', transition: { transition, resolver } })
})
})
})
}
}
// Call this hook on the second page. Inside the useEffect hook, you can refer to the actual DOM element and set them as shared elements with the transition.setElement() method. When the resolver function is called, the transition is initiated between the captured images and newly set shared elements.
export const usePageTransition = () => {
const { state, dispatch } = useContext(ResourceContext)
const ref = useRef(null)
const setRef = useCallback((node) => {
ref.current = node
}, [])
useEffect(() => {
if (!state.transition || !ref.current) {
return
}
const { transition, resolver } = state.transition
const sharedElements = ref.current.querySelectorAll('.shared-element')
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
resolver()
return () => {
dispatch({ type: 'update', transition: null })
}
})
return setRef
}
- 一覧ページで
usePageTransitionPrep()カスタムフックを呼び出し、その後、clickイベント内でtransition.start()メソッドをトリガーする非同期関数を呼び出します。
この関数内で、shared-element クラス要素が収集され、共有要素として登録されます。
components/list-item.js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransitionPrep } from '../utils/use-page-transition'
...
function ListItemForSPA({ item, href }) {
// Codelab: Add Shared Element Transitions.
const transitionNextState = usePageTransitionPrep()
const handleClick = async (e) => {
const elm = e.target.closest('a')
await transitionNextState(elm)
}
return (
<Link href={href}>
<a className='block flex items-center' onClick={handleClick}>
<Icon src={item.image} name={item.name} className='shared-element' />
<div className='text-xl'>{item.name}</div>
</a>
</Link>
)
}
- 詳細ページで、
usePageTransition()フックを呼び出してtransition.start()コールバック関数を終了します。
このコールバックでは、詳細ページの共有要素も登録されます。
pages/fruits/[名前].js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransition } from '../../utils/use-page-transition'
const Item = ({ data }) => {
const { name, image, amountPer, nutrition } = data
// Codelab: Add the Shared Element Transitions API.
const ref = usePageTransition()
return (
<div className={'flex flex-col items-center justify-center py-4 px-4 sm:flex-row'} ref={ref}>
<div className='flex flex-col items-center sm:w-2/4'>
<Image
className='object-cover border-gray-100 border-2 rounded-full shared-element'
src={image}
width='240'
height='240'
alt={`picture of ${name}`}
/>
<h1 className='text-4xl font-bold mt-4'>{name}</h1>
</div>
<div className='sm:w-2/4 w-full'>
<Nutrition amountPer={amountPer} nutrition={nutrition} />
</div>
</div>
)
...
}
これで、画像要素が一覧と詳細ページで共有され、ページ遷移でシームレスにつながるようになります。CSS 擬似要素を使用して、アニメーションを魅力的にカスタマイズすることも可能です。

7. 完了
おめでとうございます!スムーズで洗練された直感的なユーザー エクスペリエンスを実現した高速かつシームレスなウェブアプリを作成できました。
詳細
事前レンダリング
bfcache
クロスサイト プリフェッチ
Signed Exchange
ルート/共有要素の遷移
これらの API はまだ開発の初期段階であるため、フィードバックのご協力をお願いいたします。フィードバックは crbug.com にご投稿いただくか、該当する API の GitHub リポジトリで問題としてご報告ください。