TL

History APIとSPAルーティングの内部(pushState/popstate)

戻る・進むが効くのに画面が白くならない仕組みが腑に落ちる。pushState の履歴操作、popstate の発火条件、スクロール復元、Navigation API までを内部から解きます。

応用History APINavigation APISPAルーティングブラウザ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.pushState/replaceState は履歴エントリを操作し URL を書き換えるが、ナビゲーションも popstate も発火させない。だから SPA ルーターは pushState の直後に自前で描画を呼ぶ必要がある。
  • 2.popstate は「履歴位置の移動」(戻る・進む・history.go)でのみ発火し、pushState では発火しない。hashchange とスクロール復元(history.scrollRestoration)の挙動も併せて押さえる。
  • 3.Navigation API は navigate イベントで全遷移を一元的に横取りし、intercept() で描画を宣言できる。pushState 中心の旧来ルーターが抱えた発火の非対称性とスクロール管理を構造的に解消する。

なぜ履歴 API の挙動を正確に押さえる必要があるのか

SPA の「遷移してもページが白くならない」体験は、ブラウザの History API が支えています。React Router・Vue Router をはじめ、あらゆるクライアントルーターはこの API の上に実装されています。ところが History API は発火の非対称性という独特の落とし穴を持ち、ここを誤解すると「戻るボタンで画面が更新されない」「スクロール位置が飛ぶ」「履歴が二重に積まれる」といった不具合を生みます。本記事は pushState / popstate を中心に、SPA ルーターが依存する原理を内部レベルで解きほぐし、最後に刷新版の Navigation API までを扱います。

セッション履歴とエントリ:何が積まれているか

ブラウザのタブはセッション履歴(session history)を持ち、これは履歴エントリ(history entry)の配列と、現在位置を指すインデックスで構成されます。戻る・進むはこのインデックスを増減させる操作にすぎません。各エントリは少なくとも次を保持します。

  • URL:そのエントリのアドレス。
  • state オブジェクトpushState の第1引数で結びつける任意のシリアライズ可能な値。history.state で読み出せる。
  • スクロール位置:そのエントリを離れた時点のスクロール座標(後述の復元に使う)。

history.length は履歴エントリの総数を返します(最大値はブラウザ依存で、おおむね数十〜数百で頭打ち)。重要なのは、JavaScript からはインデックスや他エントリの URL を直接読めない点です。セキュリティ上、見えるのは現在エントリの history.statelocation だけで、history.go(n) のように相対移動の操作だけが許されています。

state オブジェクトの容量と用途

pushState(state, ...) の state は構造化複製(structured clone)でシリアライズされて履歴に保存され、ページ再読み込みやブラウザ再起動後も history.state から復元できます。ブラウザは安全のため上限(多くは数 MB、Firefox は 16 MiB 程度)を設けており、超過すると例外になります。巨大なデータではなく「このエントリを再描画するのに必要な最小限のキー(記事 ID・スクロール対象など)」を入れるのが定石です。

pushState / replaceState:履歴を操作する2つの命令

SPA ルーターの心臓部がこの2命令です。どちらもページ遷移(ナビゲーション)を一切起こさず、履歴エントリと URL だけを操作します。

命令履歴への作用現在位置のインデックス
history.pushState(state, '', url)新しいエントリを末尾に追加+1 され新エントリを指す
history.replaceState(state, '', url)現在エントリを上書き変わらない

pushState は「戻れる遷移」を作るとき(リンククリックでの画面切り替え)、replaceState は「戻り先を増やしたくない更新」(フィルタ条件の URL 反映、リダイレクト的な置換)に使います。第2引数の title は歴史的経緯でほぼ無視されるため空文字を渡すのが慣例です。URL は同一オリジンでなければ例外(SecurityError)になります。

ここで決定的に重要な性質が2つあります。

  • ナビゲーションは起きないpushState してもサーバーへの要求も load も走らない。アドレスバーの URL が変わるだけ。
  • popstate は発火しない:これが最大の落とし穴。pushState は履歴を積むが、popstate鳴らさない

つまり SPA ルーターは「pushState で URL を変える」と「画面を描き替える」を自分で順に呼ぶ必要があります。pushState が描画を肩代わりしてくれることはありません。

// SPA ルーターのリンク遷移の最小形
function navigate(url, state) {
  history.pushState(state, '', url); // 履歴に積む。popstate は鳴らない
  render(location.pathname);          // 描画は自前で呼ぶ(ここが肝)
}

// アンカーを横取りして全リンクをルーター経由にする
document.addEventListener('click', (e) => {
  const a = e.target.closest('a');
  if (!a || a.origin !== location.origin) return; // 外部リンクは素通し
  e.preventDefault();                              // 既定のフル遷移を止める
  navigate(a.href, { from: location.pathname });
});

popstate の発火条件:ここを取り違えると戻るが壊れる

popstate は「セッション履歴の現在位置が移動したとき」に発火します。具体的には次のケースです。

  • ユーザーが戻る/進むボタンを押した。
  • history.back() / history.forward() / history.go(n) を呼んだ。
  • 同一ドキュメント内のフラグメント(#section)への移動で履歴位置が動いた。

逆に、pushState / replaceState では発火しません。また、別ドキュメントへ実際にナビゲートする戻る・進む(SPA 外への離脱)では、ページがアンロードされるため popstate ではなく通常のロードになります。popstate はあくまで「同一ドキュメント内で履歴インデックスが動いた」合図だと捉えるのが正確です。

popstate ハンドラでは event.state(=移動先エントリに紐づけた state、history.state と同値)を読み、それに応じて描画を復元します。

window.addEventListener('popstate', (event) => {
  // 戻る・進むで現在エントリが変わった。event.state は移動先エントリの state
  render(location.pathname, event.state);
});
ページ読み込み直後の popstate に注意

歴史的に一部ブラウザ(古い Chrome 等)は初回ロード時に popstate を1回発火させ、event.statenull でした。現行の主要ブラウザはこの初回発火を行いませんが、初期描画は popstate ではなくロード時に一度明示的に render() を呼ぶ設計にしておくと、ブラウザ差や直リンク流入に強くなります。「popstate に初期描画を兼ねさせない」が安全側の原則です。

hashchange と pushState:2系統のルーティング

URL のフラグメント(# 以降)だけを変える遷移は、pushState とは別系統で hashchange イベントを発火させます。歴史的に History API が普及する前の SPA はこのハッシュルーティング(/#/users のような URL)に依存していました。

方式URL の形発火イベントサーバー設定
History ルーティング/users/42popstate(戻る・進む時)全パスを index に向ける必要あり
ハッシュルーティング/#/users/42hashchange不要(# 以降はサーバーに送られない)

History ルーティングはきれいな URL を作れる代わりに、/users/42 への直リンクやリロードでサーバーが 404 を返さないよう、サーバー側で「未知のパスはすべて index.html を返す(SPA フォールバック)」設定が必須です。フラグメントはサーバーに送信されないため、ハッシュルーティングはこの設定が不要ですが、URL が冗長になり SEO 上も不利です。現代は History ルーティング+フォールバックが標準です。

スクロール復元:飛ぶ/飛ばないを制御する

ブラウザは既定で、戻る・進みのときに離脱時のスクロール位置を自動復元します。これは MPA では望ましい挙動ですが、SPA では「描画が非同期で間に合わず、復元のタイミングがズレて変な位置に飛ぶ」問題を起こします。この自動復元は history.scrollRestoration で制御できます。

// 自動復元を切り、ルーター側でスクロールを完全管理する
if ('scrollRestoration' in history) {
  history.scrollRestoration = 'manual'; // 既定は 'auto'
}

// 描画完了後に自前で復元(state に保存しておいた座標へ)
window.addEventListener('popstate', (event) => {
  render(location.pathname, event.state).then(() => {
    const y = (event.state && event.state.scrollY) || 0;
    window.scrollTo(0, y);
  });
});

'manual' にすると自動復元が止まり、復元の責任がアプリ側に移ります。SPA ルーターの多くは、遷移時に現在のスクロール位置を replaceState で現エントリの state に書き戻し、戻ったときに popstate ハンドラで scrollTo する、という流れを組みます。新規遷移(pushState)ではトップへスクロール、復帰(popstate)では保存位置へ、と方針を分けるのが定石です。

非同期描画とスクロール復元のレース

データ取得を伴う SPA では、popstate 受信時点でまだ DOM が旧状態です。ここで即 scrollTo しても、目標要素の高さが確定しておらず位置がずれます。必ず描画・レイアウト確定の後に復元してください。scrollRestoration = 'auto' のまま自前描画を行うと、ブラウザの自動復元とアプリの復元が二重に走り、ガクつきの原因になります。自前管理するなら必ず 'manual' に切り替えるのが鉄則です。

Navigation API:発火の非対称性を構造的に解消する

History API の本質的な弱点は、「URL を変える操作(pushState)」と「描画を起こす合図(popstate)」が非対称で、開発者が両者を手で繋ぎ、さらにスクロールや遷移の中断を個別に面倒見る必要がある点でした。これを根本から作り直したのが Navigation APIwindow.navigation)です。Chromium 系で先行実装され、仕様は WHATWG HTML に統合されています。

中核は navigate イベントです。リンククリック、navigation.navigate(url)、戻る・進む、フォーム送信まで、あらゆる同一ドキュメント内遷移を1つのイベントで一元的に捕捉できます。さらに event.intercept({ handler }) を呼ぶと「この遷移は SPA が自前で処理する」と宣言でき、ハンドラの非同期完了までを遷移の一部としてブラウザが管理します。

navigation.addEventListener('navigate', (event) => {
  // 横取りすべきでない遷移は素通し(別オリジン・ダウンロード等)
  if (!event.canIntercept || event.hashChange || event.downloadRequest) return;

  event.intercept({
    async handler() {
      // 描画完了まで Navigation API が「遷移中」として扱う
      await render(new URL(event.destination.url).pathname);
    },
  });
});

event.destination は遷移先の URL・state・履歴インデックスを保持し、event.signal(AbortSignal)で遷移キャンセル時の取得中断まで標準化されています。navigation.entries() で履歴エントリ配列を読めるため、History API では不可能だった現在位置以外のエントリ参照もできます。

観点History APINavigation API
遷移の捕捉click 横取り+popstate を別々にnavigate イベントで一元化
pushState 相当の描画自前で render を呼ぶintercept(handler) で宣言
遷移の中断自前で AbortControllerevent.signal が標準提供
スクロール復元scrollRestoration を手動管理intercept の scroll オプション
履歴エントリの一覧不可(現在位置のみ)navigation.entries() で取得可

Navigation API は2026年初頭に主要4ブラウザ(Chrome・Edge・Firefox・Safari)が出揃い、Baseline Newly Available(新たに利用可能)に到達しました。ただし古いバージョンのブラウザがまだ残るため、当面はNavigation API を優先しつつ未対応環境では History API にフォールバックする二段構えが現実解です。主要なルーターも段階的に Navigation API への対応を進めています。

試験・面接の頻出ポイント

押さえるべきは次の4点です。❶ pushState / replaceState は履歴を操作するがナビゲーションも popstate も起こさない——描画は自前で呼ぶ。❷ popstate戻る・進む・history.go でのみ発火し、pushState では鳴らない。❸ スクロールの自動復元は history.scrollRestoration = 'manual' で切り、非同期描画ではレイアウト確定後に復元する。❹ Navigation API は navigate イベントと intercept() で遷移を一元管理し、History API の発火非対称性を解消する。pushState と popstate の非対称性を言語化できるかが理解の分岐点です。

まとめ

まとめ

SPA ルーティングはブラウザのセッション履歴(エントリ配列+現在位置インデックス)の上に成り立ちます。pushState は新エントリを追加、replaceState は現エントリを上書きしますが、どちらもナビゲーションも popstate も発火させないため、ルーターは URL 書き換えと描画を自前で繋ぎます。popstate戻る・進む・history.go のような履歴位置の移動でのみ鳴り、この発火の非対称性こそが History API 設計の核心であり最大の落とし穴です。スクロールは history.scrollRestoration = 'manual' で自動復元を切り、非同期描画ではレイアウト確定後に自前復元します。これらの面倒を構造的に解消するのが Navigation API で、navigate イベントと intercept() により遷移を一元管理し、中断・スクロール・履歴参照まで標準化します。描画タイミングを支える イベントループ や、エントリに紐づく state の永続化を担う構造化複製の周辺は Web ストレージ、どこで HTML を生成するかの整理は SPA / SSR / SSG と併せて押さえると、SPA の全体像が立体的になります。

Web/フロントエンド Article

History APIとSPAルーティングの内部(pushState/popstate)を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

History API

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

popstate は「履歴位置の移動」(戻る・進む・history.go)でのみ発火し、pushState では発火しない。hashchange とスクロール復元(history.scrollRestoration)の挙動も併せて押さえる。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「History API / Navigation API」に近いか確認する。
  • 強みである「pushState/replaceState は履歴エントリを操作し URL を書き換えるが、ナビゲーションも popstate も発火させない。だから SPA ルーターは pushState の直後に自前で描画を呼ぶ必要がある。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

History APINavigation APISPAルーティングブラウザHistory APINavigation APISPA
参考: 公式情報