GA4でSPAのページビューが計測されない5つの原因 — 公式が見ているのは pushState・popState・replaceState の3つだけ

GA4でSPAのページビューが計測されない5つの原因 — 公式が見ているのは pushState・popState・replaceState の3つだけ

先に結論

「先に結論」の内容を図にまとめたもの

GA4でSPAのページビューが増えないとき、原因は1つではありません。5つあって、それぞれ対処が違います。 管理画面の設定で直るものと、設定をいくらいじっても絶対に直らないものが混ざっているのが、このテーマがややこしい理由です。

まず症状から当たりをつけてください。

表1:症状から原因を切り分ける
症状原因直る場所
PVが初回ロードの1回だけ。URLは /mypage のように変わる原因1:詳細設定がオフGA4管理画面
PVが初回ロードの1回だけ。URLに # が入る原因2:hashルーティングコード側(管理画面では直らない)
1回の画面遷移でPVが2件記録される原因3:GTMとGA4の二重発火どちらか一方をオフ
画面を動かしていないのにPVが増える原因4:replaceStateの呼び出しコード側
PVの数は合うが、参照元が全部「最初の流入元」原因5:page_referrerの未更新コード側

この記事で一番伝えたいのは、次の3行です。

なぜSPAだとページビューが増えないのか

「なぜSPAだとページビューが増えないのか」の内容を図にまとめたもの

リロードが起きない、という一点

普通のサイトは、リンクをクリックするとブラウザがHTMLを取り直します。そのたびにGoogleタグが読み込まれ、page_view が1回送られます。 PVが積み上がるのは、この「HTMLを取り直す」が毎回起きているからです。

SPAはそれをしません。Googleの公式ドキュメントは、SPAを 「HTML ドキュメントを 1 回読み込み、JavaScript API を使用して追加のコンテンツを取得するウェブサイト」 と定義しています。HTMLは1回しか読まれず、以降はJavaScriptがDOMを書き換えるだけ。 タグが実行されるのも最初の1回だけです。だからPVは1のまま止まります。

ここまでは当たり前の話です。問題は「じゃあどうやって2回目以降を送るのか」で、そこにGA4側の決まりごとがあります。

GA4が「ページが変わった」と判断する瞬間

拡張計測機能のヘルプに、page_view の発火条件が書かれています。発火するのは 「ページが読み込まれるたび、またはアクティブなサイトによって閲覧履歴のステータスが変更されるたび」。 そして、その「閲覧履歴のステータスの変更」が何を指すかまで、ヘルプは具体的に書いています。

「この測定オプションでは、pushState、popState、replaceState を検知(イベント リスナー)します。」
— 拡張計測機能イベントについて(Google アナリティクス ヘルプ)

列挙されているのは3つです。ここを表にすると、何が計測されて何が計測されないかがはっきりします。

表2:GA4の自動計測が検知するもの/しないもの
操作検知これが起きる場面
ページの読み込みする初回アクセス、リロード、通常のリンク遷移
history.pushState()するReact Router / Vue Router のHistory APIモードでの画面遷移
history.replaceState()するクエリパラメータの書き換え、履歴を増やさないURL置換
popstate イベントするブラウザの戻る/進むボタン
hashchange イベントヘルプに記載なし#/mypage 形式のルーティング、アンカーリンク
DOMの書き換えだけしないタブUI、モーダル、無限スクロール(URLが変わらないもの)

hashchange が3つのリストに入っていない、というのがこの記事の軸です。 URLに # が入るタイプのSPAは、GA4の自動計測の射程外にあります。

原因1:詳細設定がオフになっている

「原因1:詳細設定がオフになっている」の内容を図にまとめたもの

一番多いのがこれです。「測定機能の強化」のスイッチをオンにしただけでは足りません。 その中の「ページビュー」をクリックして、さらに詳細設定を開く必要があります。

公式の手順をそのまま書きます。

表3:履歴イベントに基づくPV計測を有効にする手順
手順操作
1Google アナリティクスを開く
2「管理」→「データの収集と修正」→「データ ストリーム」→「ウェブ」
3「測定機能の強化」のスイッチをオンにする
4「ページビュー」をクリックし、「詳細設定を表示」を開く
5「ページ読み込み」と「ブラウザの閲覧履歴イベントに基づくページの変更」を両方オンにする
6保存

公式が「両方を有効にしてください」とわざわざ書いているのは、ここが分かれているからです。 手順3で止めてしまうと、履歴イベントのほうは有効になりません。

なお page_view そのものについて、ヘルプには 「このイベントは自動的に収集されます。収集をオフにすることはできません」 と書かれています。オフにできるのは履歴イベントに基づく送信のほうだけで、 初回ロードのPVは必ず送られます。この区別は原因3で効いてきます。

原因2:ルーティングがhashで、pushStateを呼んでいない

「原因2:ルーティングがhashで、pushStateを呼んでいない」の内容を図にまとめたもの

なぜhashだと発火しないのか

URLの # より後ろはフラグメントと呼ばれる部分で、ブラウザはこれをサーバーに送りません。 # 以降だけが変わったとき、ブラウザが出すのは hashchange イベントです。 History APIのメソッド(pushState / replaceState)は呼ばれません。

そしてGA4が待ち構えているのは pushState・popState・replaceState の3つ。 呼ばれないメソッドを待っているので、何も起きません。

これは設定ミスではありません。設定を正しくしても直りません。 ここを混同すると、管理画面のスイッチを何度もオンオフして数日を溶かすことになります。 アドレスバーを見るのが先です。

自分のサイトがどちらか調べる

一番速いのは、画面を遷移させてアドレスバーを見ることです。

コードで確認するなら、ルーターの初期化部分を見ます。

表4:フレームワーク別・hashモードになっている書き方
環境hashモード(計測されない)History APIモード(自動計測が効く)
Vue RoutercreateWebHashHistory()createWebHistory()(公式が推奨と明記)
React Router(Data mode)createHashRouter()createBrowserRouter()
React Router(Declarative mode)<HashRouter><BrowserRouter>
AngularprovideRouter(routes, withHashLocation())provideRouter(routes)
Next.js / Nuxt(標準では用意されていない)既定でHistory API
自前のJavaScriptlocation.hash = '...'history.pushState(...)

左の列に当たるものを使っていたら、原因2が確定します。 フレームワークの新旧は関係ありません。hashでページを切り替える仕組みなら、古いものでも新しいものでも同じように計測されません。

直し方は2つしかない

1. ルーターをHistory APIモードに変える。

これが本筋です。Vue Routerの公式ドキュメントも createWebHistory() を推奨モードとして書いています。 ただし、変えるだけでは終わりません。サーバー側で、どのURLに来ても index.html を返す設定が必要です。 これをやらないと、直リンクやリロードで404になります。 Vue Routerの公式も、静的アセットに一致しないURLではアプリの index.html を配信する必要があると書いています。

2. 手動で page_view を送る。

hashモードのまま運用するなら、これしかありません。hashchange を自分で拾って、gtag で送ります。 コードは後述します。

表5:どちらを選ぶか
条件選択理由
新規開発History APIモード自動計測に乗れる。実装コストがゼロになる
URLを検索エンジンに拾わせたいHistory APIモード# 以降はサーバーに送られない。別ページとして扱わせる設計には向かない
サーバー設定を触れない(rewriteできない静的ホスティング)hashモード+手動計測History APIモードにすると直リンクが404になる
既存の大規模SPAで、URLを変えると外部リンクが切れるhashモード+手動計測URL変更の影響範囲が計測の手間より大きい
GTMで既に運用しているどちらでも手動計測GTMの履歴変更トリガーで統一するほうが管理が楽(原因3)

原因3:GTMとGA4の自動計測を両方オンにしている

「原因3:GTMとGA4の自動計測を両方オンにしている」の内容を図にまとめたもの

「計測されない」を直した次に踏むのがこれです。公式のデベロッパードキュメントに、はっきりした警告が書かれています。

Googleタグマネージャーでタグを設定している場合は、GA4側で自動の履歴トラッキングを有効にしないこと。 ページビューの二重計測につながる可能性がある、と公式が明記しています。
— シングルページ アプリケーションを測定する(Google for Developers)

組み合わせを並べると、正解は2通りしかないことが分かります。

表6:設定の組み合わせと結果
GA4「履歴イベントに基づくページの変更」GTM側の設定1遷移あたりのPV判定
オン履歴変更トリガーなし1件正常(GA4だけで実装)
オフ履歴変更トリガーでGA4イベントを送信1件正常(GTMで実装)
オン履歴変更トリガーでGA4イベントを送信2件二重計測
オフ履歴変更トリガーなし0件(初回のみ)計測されない(原因1)

公式は「代わりにTag ManagerでSPAを測定するチュートリアルを見てください」と誘導しています。 つまりGTM運用ならGA4側はオフが正解です。どちらで実装するかを先に1つ決めて、もう片方は触らない。 両方オンは必ずズレます。

ここでさっきの区別が効いてきます。page_view の自動収集そのものはオフにできません。 オフにできるのは履歴イベントに基づく送信のほうだけです。 だから初回ロードのPVはGA4側から必ず1件出ます。 GTMで「ページビュー」トリガーのタグも作っていると、初回ロードだけが二重になります。 GTMで実装するときに作るべきなのは、履歴変更トリガーのタグだけです。

原因4:replaceStateでPVが増えすぎている

「原因4:replaceStateでPVが増えすぎている」の内容を図にまとめたもの

表2のとおり、GA4は replaceState も検知します。 replaceState は履歴を増やさずにURLを書き換えるメソッドなので、画面が変わらない場面でも呼ばれます。 そしてそれが1件のPVになります。

表7:replaceStateが呼ばれがちな場面
場面URLの変化画面は変わるかPVとして妥当か
絞り込みフィルタの状態をURLに保存?sort=price が付く並び順だけ妥当とは言いにくい
無限スクロールで現在位置をURLに反映?page=2 が付く継ぎ足されるだけ妥当ではない
フォームのステップをURLに保存?step=2 が付く変わる妥当
ログイン後にトークンをURLから削除?token=... が消える変わらない妥当ではない
タブUIの選択状態をURLに保存?tab=reviews が付く一部だけ設計次第
スクロール位置の復元用にURLを更新#section-3 などが付く変わらない妥当ではない

「妥当ではない」行が全部PVになります。1セッションあたりのPVが実感よりやたら多いときは、ここを見てください。 絞り込みを3回かけたユーザーが、4PVとして記録されます。

対処は1つです。自動計測をオフにして、ルーターのナビゲーション完了イベントだけで手動送信に切り替える。 URLの書き換えとPVの送信を切り離します。 自動計測はURLの変化しか見ていないので、「このURL変更はPVではない」という区別を自動計測側で表現する方法はありません。

原因5:page_referrerが前のURLになっていない

「原因5:page_referrerが前のURLになっていない」の内容を図にまとめたもの

PVの数は合っているのに、レポートの参照元が全部「最初の流入元」になっているパターンです。 数字は出ているので気づきにくく、経路の分析を始めた段階で発覚します。

公式ドキュメントは、SPA計測の要点をこう書いています。 「ユーザーが操作する各画面のページビューをカウントし、ページ referrer を正しく取得して、ユーザー ジャーニーを正しく追跡できるようにすること」。 PVを送ることと、参照元を正しくすることが並べて書かれています。

自動計測ならGA4が面倒を見てくれます。これが問題になるのは手動実装したときで、 page_location だけ更新して page_referrer を忘れるのが典型です。

表8:手動実装で更新すべきパラメータ
パラメータ入れる値忘れると起きること
page_location遷移後のURL全PVが初回のURLで記録される。画面別の数字が出ない
page_referrer遷移前のURL画面間の遷移が追えない。経路データ探索が成立しない
page_title遷移後の document.title全画面が同じタイトルで記録される
send_page_viewfalse(configに指定)configの時点で1件送られ、直後の手動送信と合わせて二重になる
updatetrueGoogleタグが再初期化され、前に設定したパラメータが落ちる

手動でpage_viewを送る

「手動でpage_viewを送る」の内容を図にまとめたもの

公式のコード

公式ドキュメントに載っているコードです。

// 現在のURLを変数に保存しておく(公式サンプルはhashを落としている)
const oldUrl = window.location.href.split('#')[0];

// URLを変更する
history.pushState({}, undefined, newUrl);

// ページの変化を計測する
function OnPageChange(oldUrl, newUrl) {
  gtag('config', 'G-XXXXXX', {
    'send_page_view': false,
    'page_referrer': oldUrl,
    'page_location': newUrl,
    'update': true,
  });
  gtag('event', 'page_view');
}

読むべき点は3つです。

公式サンプルが split('#')[0] でhashを落としていることに注意してください。 これはHistory APIモードを前提にしたコードです。hashモードでそのまま使うと、全画面が同じURLで記録されます。

hashモードで手動計測する場合

原因2をコード側で埋めるなら、公式サンプルを3箇所読み替えます。

表9:公式サンプルとhashモードでの差分
項目公式サンプル(History APIモード)hashモードで使う場合
URLの取得window.location.href.split('#')[0]window.location.href(hashを残す)
送信のきっかけpushState を呼んだ直後hashchange イベント、またはルーターの遷移完了フック
page_location の値/mypage/#/mypage
GA4側の自動計測オフ(手動と併用すると二重)オン・オフどちらでも結果は同じ(そもそも発火しない)

読み替えたコードです。

let prevUrl = window.location.href;

window.addEventListener('hashchange', function () {
  const newUrl = window.location.href;

  gtag('config', 'G-XXXXXXXXXX', {
    'send_page_view': false,
    'page_referrer': prevUrl,
    'page_location': newUrl,
    'page_title': document.title,
    'update': true,
  });
  gtag('event', 'page_view');

  prevUrl = newUrl;
});

これは公式サンプルをhashモードに読み替えたもので、公式にこの形で載っているコードではありません。 hashchange は document.title の更新より先に走ることがあるので、 ルーターを使っているならナビゲーション完了フックから呼ぶほうが確実です。 どちらにしても、DebugViewで必ず確認してください。

DebugViewで確認する

「DebugViewで確認する」の内容を図にまとめたもの

公式が示している検証手順は、すべてのタグでデバッグモードを有効にして、DebugViewでイベントを見るというものです。 そして「前の page_view イベントと比較して、ページリファラーとページロケーションが正しく更新されているか確認する」と書かれています。 1件だけ見ても分かりません。2件並べて差分を見ます。

表10:DebugViewで見るべきもの
確認項目期待する状態ズレていたら
1遷移で page_view が何件出るか1件0件 → 原因1か原因2/2件 → 原因3
page_location遷移後のURLになっている初回URLのまま → 手動実装のパラメータ漏れ
page_referrer遷移前のURLになっている流入元のまま → 原因5
page_title遷移後のタイトル前の画面のタイトル → 送信タイミングが早い
user_engagement遷移の直前に送られている出ない → 仮想PVが効いていない(次章)
画面を動かさずURLだけ変えたときpage_view が出ない出る → 原因4

最後の行は、意識して試さないと見つかりません。絞り込みを1回かけて、PVが増えないことを確認してください。 増えたら原因4です。

仮想PVを入れると他の指標も直る

「仮想PVを入れると他の指標も直る」の内容を図にまとめたもの

ここは見落とされやすいところです。仮想PVはPVの話だけで終わりません。公式はこう書いています。 「画面の変更に対して仮想ページビューが記録されない場合、Google アナリティクスは SPA を単一ページとして扱うため、指標が歪むことになります」。

「PVが1件しかない」は症状ではなく、レポート全体が1ページぶんのデータとして処理されているという状態です。

表11:仮想PVの有無で変わる指標
指標・イベント仮想PVなし仮想PVあり
user_engagement全エンゲージメント時間が初回ロードに紐づく画面を離れるたびに、その画面の滞在時間として送られる
画面別の滞在時間分析できない画面ごとに出る
クリック・スクロール等のイベント全部、初回のURLに紐づくいま見ている画面の page_location に紐づく
経路データ探索1ページしかないので成立しない画面間の遷移が追える
離脱した画面常に同じURL実際に離脱した画面が分かる

公式は順序まで決めています。仮想PVが正しく入っていれば、user_engagement は 新しい仮想ページの page_view が処理される直前に、前の画面の滞在時間とともに送られます。 DebugViewで user_engagement → page_view の順に並んでいれば、効いています。

公式はあわせて、SPAで別途やるべきことを2つ挙げています。計測とは別の話ですが、放置すると数字が歪みます。

表12:SPAで別途必要になる対応
項目推奨される対応放置すると
スクロール位置遷移ごとに window.scrollTo(0, 0) を呼ぶ位置が保持され、新しい画面の先頭が見えない。スクロール計測も狂う
ページ内検索(Ctrl+F)DOMの該当部分を完全・同期的に更新する遅延レンダリングや隠されたコンテンツがブラウザの検索で拾われない

AIにやらせるなら

「AIにやらせるなら」の内容を図にまとめたもの

このテーマは、AIに丸投げすると古いコードが返ってくる確率が高い分野です。 ユニバーサルアナリティクス時代の書き方が大量に出回っていて、それが学習データに入っています。 _gaq.push(['_trackPageview']) や ga('send', 'pageview') が出てきたら、GA4では動きません。

だから、頼むことを選びます。

表13:AIへの依頼の切り分け
頼むこと向くか理由
ルーターの初期化コードを貼って「hashモードか判定して」向く判定基準が表4で決まっている。コードを読むだけの作業
「このルーターの遷移完了フックに計測を足して」向くフレームワークのAPIは学習済み。差分も小さい
DebugViewのログを貼って「page_referrerが更新されない原因の候補を出して」向く症状から原因を絞るのは得意
「replaceStateを呼んでいる箇所を全部洗い出して」向くコードベースの検索。原因4の調査がこれで済む
「GA4のSPA計測のコードを書いて」向かないUA時代のコードが混ざる。公式サンプルを渡したほうが速い
「GA4の管理画面のどこを押せばいいか教えて」向かないUIの名称が変わる。表3か公式ヘルプを見る
「計測できているか確認して」向かないDebugViewは自分で開く。ここは代わりが立たない

依頼するときのコツは、公式ドキュメントのURLを貼って「このページの仕様に従って」と条件を付けることです。 それでも出てきたコードに _gaq か ga( が入っていたら、読まずに捨てていいです。

よくある質問

拡張計測機能をオンにしたのにPVが増えません

スイッチだけでは足りません。「ページビュー」の詳細設定を開いて、 「ブラウザの閲覧履歴イベントに基づくページの変更」が有効か確認してください(原因1)。 それでも増えないなら、アドレスバーに # が入っていないか見てください(原因2)。

URLに「#」が入っています。管理画面の設定で直りますか

直りません。公式ヘルプが検知対象に挙げているのは pushState・popState・replaceState の3つで、 hashchange は入っていません。ルーターをHistory APIモードに変えるか、 手動で送るかの二択です。

GTMで実装済みです。GA4側の設定はどうすれば

オフにします。公式が、両方有効にすると二重計測になる可能性があると明記しています(原因3)。 GTM側で作るのは履歴変更トリガーのタグだけにしてください。「ページビュー」トリガーのタグも作ると、初回ロードが二重になります。

PVが実態の2倍あります

原因3(GTMとGA4の両方オン)か、 原因4(replaceStateでURLだけ書き換わる場面がPVになっている)です。 DebugViewで、1回の画面遷移に page_view が何件出るかを数えてください。 2件なら原因3、1件なのにPVが多いなら原因4です。

page_viewの自動収集をオフにできますか

できません。公式ヘルプに「このイベントは自動的に収集されます。収集をオフにすることはできません」と書かれています。 オフにできるのは、履歴イベントに基づく送信のほうだけです。

古いフレームワークでも同じですか

hashでページを切り替える仕組みなら、原因2に当たります。 問題はルーティング方式がhashかどうかだけで、フレームワークの新旧は関係ありません。 2010年代前半のモバイル向けフレームワークも、最近のルーターのhashモードも、GA4から見れば同じ扱いです。

手動で送るとき、page_titleも必要ですか

入れたほうがいいです。入れないと全画面が同じタイトルで記録されます。 ただし document.title の更新タイミングには注意してください。 hashchange のリスナーは、タイトル更新より先に走ることがあります。

SPAでないサイトでも、この設定は影響しますか

影響することがあります。replaceState を検知するので、 クエリパラメータをJSで書き換えているページがあれば、そこでPVが増えます(原因4)。 SPAでなくても、絞り込みUIや無限スクロールを入れているサイトは確認しておく価値があります。

まとめ

ブラウザ側にデータを持たせる話は localStorage の使い方と、2026年に気をつけること、 古い書き方が今も検索結果に残り続けているという同じ型の話は PHPでOGPを取得する と CakePHPでループ中にsaveする にも書きました。 サイトを置く場所から考えるなら レンタルサーバーおすすめ8選 です。

この記事の仕様は2026年10月25日時点のGoogle公式ドキュメントとヘルプの記載を確認したものです。 GA4の管理画面の名称や拡張計測機能の既定値は変更されます。 実装する前に、必ず公式ドキュメントで最新の内容を確認してください。 また、計測が効いているかどうかは推測せず、DebugViewで実際のイベントを見て判断してください。