GA4でSPAのページビューが計測されない5つの原因 — 公式が見ているのは pushState・popState・replaceState の3つだけ
先に結論
GA4でSPAのページビューが増えないとき、原因は1つではありません。5つあって、それぞれ対処が違います。 管理画面の設定で直るものと、設定をいくらいじっても絶対に直らないものが混ざっているのが、このテーマがややこしい理由です。
まず症状から当たりをつけてください。
| 症状 | 原因 | 直る場所 |
|---|---|---|
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行です。
- GA4の自動計測が検知するのは pushState・popState・replaceState の3つだけ。
hashchangeは公式の検知対象に入っていません。 - だから
#/mypage形式のルーティングは、管理画面をどういじっても計測されません。ルーターを変えるか、手で送るかの二択です。 - GTMで実装するなら、GA4側の自動履歴計測はオフにします。公式が、両方有効にすると二重計測になると明記しています。
なぜ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つです。ここを表にすると、何が計測されて何が計測されないかがはっきりします。
| 操作 | 検知 | これが起きる場面 |
|---|---|---|
| ページの読み込み | する | 初回アクセス、リロード、通常のリンク遷移 |
history.pushState() | する | React Router / Vue Router のHistory APIモードでの画面遷移 |
history.replaceState() | する | クエリパラメータの書き換え、履歴を増やさないURL置換 |
popstate イベント | する | ブラウザの戻る/進むボタン |
hashchange イベント | ヘルプに記載なし | #/mypage 形式のルーティング、アンカーリンク |
| DOMの書き換えだけ | しない | タブUI、モーダル、無限スクロール(URLが変わらないもの) |
hashchange が3つのリストに入っていない、というのがこの記事の軸です。
URLに # が入るタイプのSPAは、GA4の自動計測の射程外にあります。
原因1:詳細設定がオフになっている
一番多いのがこれです。「測定機能の強化」のスイッチをオンにしただけでは足りません。 その中の「ページビュー」をクリックして、さらに詳細設定を開く必要があります。
公式の手順をそのまま書きます。
| 手順 | 操作 |
|---|---|
| 1 | Google アナリティクスを開く |
| 2 | 「管理」→「データの収集と修正」→「データ ストリーム」→「ウェブ」 |
| 3 | 「測定機能の強化」のスイッチをオンにする |
| 4 | 「ページビュー」をクリックし、「詳細設定を表示」を開く |
| 5 | 「ページ読み込み」と「ブラウザの閲覧履歴イベントに基づくページの変更」を両方オンにする |
| 6 | 保存 |
公式が「両方を有効にしてください」とわざわざ書いているのは、ここが分かれているからです。 手順3で止めてしまうと、履歴イベントのほうは有効になりません。
なお page_view そのものについて、ヘルプには
「このイベントは自動的に収集されます。収集をオフにすることはできません」
と書かれています。オフにできるのは履歴イベントに基づく送信のほうだけで、
初回ロードのPVは必ず送られます。この区別は原因3で効いてきます。
原因2:ルーティングがhashで、pushStateを呼んでいない
なぜhashだと発火しないのか
URLの # より後ろはフラグメントと呼ばれる部分で、ブラウザはこれをサーバーに送りません。
# 以降だけが変わったとき、ブラウザが出すのは hashchange イベントです。
History APIのメソッド(pushState / replaceState)は呼ばれません。
そしてGA4が待ち構えているのは pushState・popState・replaceState の3つ。 呼ばれないメソッドを待っているので、何も起きません。
これは設定ミスではありません。設定を正しくしても直りません。 ここを混同すると、管理画面のスイッチを何度もオンオフして数日を溶かすことになります。 アドレスバーを見るのが先です。
自分のサイトがどちらか調べる
一番速いのは、画面を遷移させてアドレスバーを見ることです。
https://example.com/mypageと変わる → History APIモード。原因は1か3か5。https://example.com/#/mypageと変わる → hashモード。原因2。
コードで確認するなら、ルーターの初期化部分を見ます。
| 環境 | hashモード(計測されない) | History APIモード(自動計測が効く) |
|---|---|---|
| Vue Router | createWebHashHistory() | createWebHistory()(公式が推奨と明記) |
| React Router(Data mode) | createHashRouter() | createBrowserRouter() |
| React Router(Declarative mode) | <HashRouter> | <BrowserRouter> |
| Angular | provideRouter(routes, withHashLocation()) | provideRouter(routes) |
| Next.js / Nuxt | (標準では用意されていない) | 既定でHistory API |
| 自前のJavaScript | location.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 で送ります。
コードは後述します。
| 条件 | 選択 | 理由 |
|---|---|---|
| 新規開発 | History APIモード | 自動計測に乗れる。実装コストがゼロになる |
| URLを検索エンジンに拾わせたい | History APIモード | # 以降はサーバーに送られない。別ページとして扱わせる設計には向かない |
| サーバー設定を触れない(rewriteできない静的ホスティング) | hashモード+手動計測 | History APIモードにすると直リンクが404になる |
| 既存の大規模SPAで、URLを変えると外部リンクが切れる | hashモード+手動計測 | URL変更の影響範囲が計測の手間より大きい |
| GTMで既に運用している | どちらでも手動計測 | GTMの履歴変更トリガーで統一するほうが管理が楽(原因3) |
原因3:GTMとGA4の自動計測を両方オンにしている
「計測されない」を直した次に踏むのがこれです。公式のデベロッパードキュメントに、はっきりした警告が書かれています。
Googleタグマネージャーでタグを設定している場合は、GA4側で自動の履歴トラッキングを有効にしないこと。
ページビューの二重計測につながる可能性がある、と公式が明記しています。
— シングルページ アプリケーションを測定する(Google for Developers)
組み合わせを並べると、正解は2通りしかないことが分かります。
| 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が増えすぎている
表2のとおり、GA4は replaceState も検知します。
replaceState は履歴を増やさずにURLを書き換えるメソッドなので、画面が変わらない場面でも呼ばれます。
そしてそれが1件のPVになります。
| 場面 | 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になっていない
PVの数は合っているのに、レポートの参照元が全部「最初の流入元」になっているパターンです。 数字は出ているので気づきにくく、経路の分析を始めた段階で発覚します。
公式ドキュメントは、SPA計測の要点をこう書いています。 「ユーザーが操作する各画面のページビューをカウントし、ページ referrer を正しく取得して、ユーザー ジャーニーを正しく追跡できるようにすること」。 PVを送ることと、参照元を正しくすることが並べて書かれています。
自動計測ならGA4が面倒を見てくれます。これが問題になるのは手動実装したときで、
page_location だけ更新して page_referrer を忘れるのが典型です。
| パラメータ | 入れる値 | 忘れると起きること |
|---|---|---|
page_location | 遷移後のURL | 全PVが初回のURLで記録される。画面別の数字が出ない |
page_referrer | 遷移前のURL | 画面間の遷移が追えない。経路データ探索が成立しない |
page_title | 遷移後の document.title | 全画面が同じタイトルで記録される |
send_page_view | false(configに指定) | configの時点で1件送られ、直後の手動送信と合わせて二重になる |
update | true | Googleタグが再初期化され、前に設定したパラメータが落ちる |
手動で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つです。
gtag('config', ...)で先にパラメータを差し替える。update: trueがないと設定が初期化され、他のパラメータが落ちます。send_page_view: falseを入れないと、configの時点で1件送られます。そのあとgtag('event','page_view')でもう1件。これだけで二重計測になります。- 最後に
page_viewを自分で送る。configはパラメータの更新で、送信そのものではありません。
公式サンプルが split('#')[0] でhashを落としていることに注意してください。
これはHistory APIモードを前提にしたコードです。hashモードでそのまま使うと、全画面が同じURLで記録されます。
hashモードで手動計測する場合
原因2をコード側で埋めるなら、公式サンプルを3箇所読み替えます。
| 項目 | 公式サンプル(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でイベントを見るというものです。
そして「前の page_view イベントと比較して、ページリファラーとページロケーションが正しく更新されているか確認する」と書かれています。
1件だけ見ても分かりません。2件並べて差分を見ます。
| 確認項目 | 期待する状態 | ズレていたら |
|---|---|---|
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の話だけで終わりません。公式はこう書いています。 「画面の変更に対して仮想ページビューが記録されない場合、Google アナリティクスは SPA を単一ページとして扱うため、指標が歪むことになります」。
「PVが1件しかない」は症状ではなく、レポート全体が1ページぶんのデータとして処理されているという状態です。
| 指標・イベント | 仮想PVなし | 仮想PVあり |
|---|---|---|
user_engagement | 全エンゲージメント時間が初回ロードに紐づく | 画面を離れるたびに、その画面の滞在時間として送られる |
| 画面別の滞在時間 | 分析できない | 画面ごとに出る |
| クリック・スクロール等のイベント | 全部、初回のURLに紐づく | いま見ている画面の page_location に紐づく |
| 経路データ探索 | 1ページしかないので成立しない | 画面間の遷移が追える |
| 離脱した画面 | 常に同じURL | 実際に離脱した画面が分かる |
公式は順序まで決めています。仮想PVが正しく入っていれば、user_engagement は
新しい仮想ページの page_view が処理される直前に、前の画面の滞在時間とともに送られます。
DebugViewで user_engagement → page_view の順に並んでいれば、効いています。
公式はあわせて、SPAで別途やるべきことを2つ挙げています。計測とは別の話ですが、放置すると数字が歪みます。
| 項目 | 推奨される対応 | 放置すると |
|---|---|---|
| スクロール位置 | 遷移ごとに window.scrollTo(0, 0) を呼ぶ | 位置が保持され、新しい画面の先頭が見えない。スクロール計測も狂う |
| ページ内検索(Ctrl+F) | DOMの該当部分を完全・同期的に更新する | 遅延レンダリングや隠されたコンテンツがブラウザの検索で拾われない |
AIにやらせるなら
このテーマは、AIに丸投げすると古いコードが返ってくる確率が高い分野です。
ユニバーサルアナリティクス時代の書き方が大量に出回っていて、それが学習データに入っています。
_gaq.push(['_trackPageview']) や ga('send', 'pageview') が出てきたら、GA4では動きません。
だから、頼むことを選びます。
| 頼むこと | 向くか | 理由 |
|---|---|---|
| ルーターの初期化コードを貼って「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や無限スクロールを入れているサイトは確認しておく価値があります。
まとめ
- 「計測されない」の原因は5つある。最初に見るのはアドレスバーの
#。ここで原因2かどうかが分かれる。 - GA4の自動計測が検知するのは pushState・popState・replaceState の3つだけ。
hashchangeは公式の検知対象に入っていない。 - hashモードは管理画面をいくらいじっても直らない。ルーターを変えるか、手で送るか。
- 拡張計測機能はスイッチだけでは足りない。「ページビュー」の詳細設定まで開く。
- GTMとGA4の自動計測はどちらか1つ。両方オンは二重計測。GTM運用ならGA4側はオフ。
- PVが増えすぎている場合は
replaceStateを疑う。URLの書き換えとPVの送信を切り離す。 - 手動実装では
page_referrer・send_page_view: false・update: trueを忘れない。 - 検証はDebugViewで2件並べて差分を見る。1件だけでは分からない。
ブラウザ側にデータを持たせる話は localStorage の使い方と、2026年に気をつけること、 古い書き方が今も検索結果に残り続けているという同じ型の話は PHPでOGPを取得する と CakePHPでループ中にsaveする にも書きました。 サイトを置く場所から考えるなら レンタルサーバーおすすめ8選 です。
この記事の仕様は2026年10月25日時点のGoogle公式ドキュメントとヘルプの記載を確認したものです。 GA4の管理画面の名称や拡張計測機能の既定値は変更されます。 実装する前に、必ず公式ドキュメントで最新の内容を確認してください。 また、計測が効いているかどうかは推測せず、DebugViewで実際のイベントを見て判断してください。