← 記事一覧へ

ブログ

SPA(React / Next.js / Vue)でコンバージョンが計測されない原因と対処法

spa コンバージョン計測react コンバージョン トラッキングシングルページアプリ 計測next.js コンバージョン計測

「他のページでは動いているのに、サンクスページだけタグが発火しない」——React、Next.js、Vueなど、SPAフレームワークで構築されたサイトでは、これが最もよくあるコンバージョン計測の失敗パターンです。そしてGTMのプレビューモードでは正常に見えるため、問題が見えにくいのが厄介です。

原因はシンプルです。SPAでは本当のページ読み込みが起きません。 ブラウザは最初にシェルを1回読み込み、以降の「ページ」はすべてJavaScriptによるDOM差し替えです。ページ読み込みに依存するタグ——ほとんどがそうですが——は、ナビゲーションが起きたことを認識できません。

このガイドでは、SPAで計測が壊れる理由、フレームワーク別の対処法、本番で動いているかの検証方法を解説します。

1. なぜ従来のタグ発火がSPAで機能しないのか

従来のマルチページサイトでは、ページ遷移のたびにブラウザがHTMLを新たにリクエストし、完全なページ読み込みが発生します。タグ管理システム(GTM、直接埋め込みのgtag、Meta Pixelなど)は、各DOMContentLoadedwindow.onloadイベントで発火します。「すべてのページ」で発火するよう設定されたタグは、文字通りすべてのページで発火します。

SPAでは:

マルチページサイト SPA
リンクをクリック→ブラウザが新しいHTMLをリクエスト→完全なページ読み込み リンクをクリック→JavaScriptがDOMを差し替え→History APIでURL変更
毎回DOMContentLoadedが発火 DOMContentLoadedは1回だけ(初回読み込み時のみ)
GTMの「すべてのページ」トリガーが毎回発火 GTMの「すべてのページ」トリガーは初回読み込み時のみ
/thank-youのCVタグはページ読み込み時に発火 /thank-youのCVタグは発火しない(ページ読み込みが起きない)

つまり「サンクスページが読み込まれたら発火」と設定されたコンバージョンタグは、まったく発火しません。ユーザーはそこに遷移し、URLは変わり、コンテンツは表示されますが、ブラウザの意味での「ページ読み込み」は発生しないからです。

2. 3つの対処アプローチ

アプローチA:データレイヤーに仮想ページビューをプッシュ

GTMベースの設定で最も信頼性の高い方法です。SPAのルートが変わるたびに、データレイヤーにイベントをプッシュします。GTMは組み込みの「ページビュー」トリガーの代わりに、このイベントで発火します。

// ルート変更のたびにdataLayerにプッシュ
dataLayer.push({
  event: 'virtual_pageview',
  page_path: '/thank-you',
  page_title: 'Thank You'
});

GTMでの設定:

  1. イベント名virtual_pageviewカスタムイベントトリガーを作成
  2. page_pathの条件(例:page_path が /thank-you に等しい)でコンバージョンタグを発火させる

このアプローチは「いつ発火するか」の判断をフレームワークから分離します。アプリコードがイベントをプッシュし、GTMがタグロジックを処理します。

アプローチB:GTMのHistory Changeトリガーを使う

GTMにはブラウザのHistory APIが使われたとき(SPAがURLを変更する方法)に発火する組み込みの「履歴の変更」トリガーがあります。アプリのコード変更なしで動作する可能性があります。

設定:

  1. GTMで履歴の変更タイプのトリガーを作成
  2. {{Page Path}}に基づく条件を追加(例:Page Path が /thank-you に等しい
  3. コンバージョンタグをこのトリガーに紐づける

注意点: 履歴の変更トリガーは、ブラウザの戻る/進むナビゲーションを含むすべてのURL変更で発火します。条件を慎重に設定しないと、ユーザーが実際にはコンバージョンせずにサンクスページを戻る/進むで通過したときにもタグが発火し、二重計測の原因になります。そもそもGTMタグが発火しない場合は、GTMタグが計測されない原因と対処法も確認してください。

アプローチC:アプリコード内でタグを直接発火

GTMを使わず直接埋め込みの場合、コンポーネントやルートハンドラ内でトラッキング関数を直接呼び出します。

// React: コンポーネントのマウント時にuseEffectで発火
useEffect(() => {
  gtag('event', 'conversion', {
    'send_to': 'AW-XXXXXXX/YYYYYYY',
    'value': 10000,
    'currency': 'JPY'
  });
}, []);
// Next.js App Router: サンクスページのクライアントコンポーネントで発火
'use client'
import { useEffect } from 'react'

export default function ThankYou() {
  useEffect(() => {
    gtag('event', 'conversion', {
      'send_to': 'AW-XXXXXXX/YYYYYYY',
      'value': 10000,
      'currency': 'JPY'
    });
  }, []);

  return <h1>ご購入ありがとうございます</h1>
}

利点: GTMの複雑さがなく、コンバージョンが意図した場所・タイミングで正確に発火します。

リスク: トラッキングコードがアプリケーションコードに混在し、管理が難しくなる可能性があります。多くのタグを多くのページで使う場合、GTMのほうが保守しやすいです。

GA4の組み込みSPAサポート(拡張計測機能)

独自のSPAトラッキングを構築する前に、GA4の組み込み機能がニーズをカバーできるか確認しましょう。GA4の拡張計測機能には「ブラウザの履歴イベントに基づくページの変更」オプションがあります:

有効化の手順: 管理 → データストリーム → ウェブ → 拡張計測機能 → ページビュー → 詳細設定を表示 → 「ブラウザの履歴イベントに基づくページの変更」を有効化。

有効にすると、GA4はブラウザのHistory APIがURLを更新するたびに自動でpage_viewイベントを発火します。これはまさにSPAで起きることです。コード変更やGTM設定は不要です。

重要な警告:二重カウントに注意。 GTMでルート変更時にpage_viewイベントを送信しつつ、このGA4設定も有効にしていると、SPAナビゲーションのたびにページビューが2回カウントされます。どちらか一方を選んでください:GA4の拡張計測機能がページビューを自動で処理するか、GTMが処理するか——両方は不可です。これはSPA計測の最もよくある設定ミスの一つです。

制限事項: この機能はページビュー計測に対応しており、コンバージョンイベントの計測には対応していません。購入やサインアップなどのコンバージョン固有のイベントは、適切なタイミングでGTMまたはアプリコードから発火する必要があります。

3. フレームワーク別の注意点

React(Create React App、Vite)

  • 問題: React RouterがHistory APIでURLを変更。ページ読み込みイベントは発火しない。
  • 対処: 遷移先コンポーネントでuseEffectを使うか、ルーターのルート変更を監視してデータレイヤーにプッシュ。
  • 注意: useEffectはレンダリング後に実行されるため、DOM更新からわずかに遅れてコンバージョンが発火します。通常は問題ありませんが、ユーザーが非常に速く離脱した場合(即時リダイレクトなど)、タグが完了しない可能性があります。

Next.js

  • App Router: ページはサーバーレンダリング(RSC)またはクライアントレンダリングが可能。コンバージョンタグはクライアントで実行する必要があります。'use client'コンポーネントでuseEffectを使うか、next/scriptコンポーネントのafterInteractive戦略を使用。
  • Pages Router: router.eventsAPIでrouteChangeCompleteを監視し、データレイヤーにプッシュ。
  • 静的エクスポート vs サーバー: output: 'export'の場合、標準的なSPAと同様の動作。SSRの場合、初回ページ読み込みではタグが通常通り発火——クライアントサイドナビゲーションのみSPA対応が必要。
  • 注意: Next.jsはリンクをプリフェッチします。プリフェッチはルート変更をトリガーしませんが、タグが間違ったイベントで発火すると、プリフェッチ関連のDOM操作が誤発火の原因になります。

Vue(Nuxt)

  • Vue Router: router.afterEach()を監視して仮想ページビューをデータレイヤーにプッシュ。
  • Nuxt: page:finishフックまたはルート変更時に発火するプラグインを使用。
  • 注意: VueのトランジションシステムがDOM更新を遅延させることがあります。遷移先ページのDOM要素をチェックするタグは、ルート変更イベント発火時にまだ要素が存在しない可能性があります。

Angular

  • Angular Router: Router.eventsをサブスクライブし、NavigationEndイベントをフィルタ。各NavigationEndでデータレイヤーにプッシュ。
  • 注意: AngularのZone.jsがサードパーティスクリプトと干渉する場合があります。タグが非同期で読み込まれAngularのゾーン外で実行されると、予測しにくい動作になることがあります。

4. 二重計測の罠

SPAではマルチページサイトよりも二重計測が起きやすくなります。主な理由は2つ:

  1. 履歴ナビゲーション: ユーザーが購入完了→/thank-youに到達→戻るボタン→進むボタン——History Changeトリガーが再度発火し、コンバージョンが2回計上される。

  2. コンポーネントの再マウント: Reactでは、サンクスコンポーネントが親の再レンダリングやルートのフリッカーでアンマウント→再マウントすると、useEffectが再度実行されタグが2回発火する。

対処法:

  • ワンタイムフラグを使う:最初の発火後に変数(sessionStorage、React ref、データレイヤー変数)をセットし、再発火前にチェック
  • GTMで「トリガーの発火はページごとに1回」オプションを使う——ただしSPAでは「ページごと」は初回読み込み単位であり、ルート変更単位ではないことに注意
  • プラットフォーム側で重複排除:transaction_idパラメータ(Google広告)やeventID(Meta)を使い、プラットフォームに重複シグナルを無視させる

5. SPA間のデータレイヤー衛生管理

マルチページサイトでは、ナビゲーションのたびにデータレイヤーがリセットされます——各ページ読み込みで最初から始まります。SPAでは、データレイヤーがルート変更をまたいで保持されます。これにより、微妙だが深刻な問題が生まれます:前のページの古い値が現在のページのイベントを汚染する。

古い値が誤ったコンバージョンデータを生む仕組み

ユーザーが商品A(19,900円)を閲覧し、カートに追加、次に商品B(4,900円)に遷移して購入。purchaseイベントの発火時にデータレイヤーにまだ商品Aの値が残っていると、コンバージョンが4,900円ではなく19,900円で報告される可能性があります。さらに悪い場合、商品Aのアイテムデータが商品Bと一緒に含まれることもあります。

対処法:仮想ページビューの前にフラッシュする

ルート変更のたびに、新しいページビューをプッシュする前に、関連するデータレイヤー変数をリセットします:

// ルート変更時——フラッシュしてからプッシュ
dataLayer.push({
  event: 'spa_cleanup',
  ecommerce: null  // GA4のeコマースデータをクリア
});

dataLayer.push({
  event: 'spa_pageview',
  spa_page_path: newPath,
  spa_page_title: newTitle
});

ecommerce: nullのセットは、イベント間の古いeコマースデータをクリアするためにGoogleが公式に推奨している方法です(データレイヤーの完全な構成はGA4 eコマース purchaseイベントの設定ガイドを参照)。eコマース以外の変数については、明示的にundefinedをセットするか、前の値を上書きする新しいオブジェクトをプッシュしてください。

注意すべき点

  • 前回の購入のtransaction_idが残る——次のイベントに古いtransaction_idが付き、本来の新しいコンバージョンが重複排除される可能性
  • 通貨の不一致——USD商品を閲覧後にJPY商品に切り替えた場合、currencyが再プッシュされないと古い値が残る
  • GTMデータレイヤー変数のバージョン——バージョン1の変数は明示的にクリアするまで保持される。バージョン2はイベントスコープだが、読み取りタイミングによっては漏れる可能性がある

6. SPAコンバージョン計測の検証方法

ステップ1:DevToolsのNetworkタブで確認

SPA内でコンバージョンページに遷移します(リロードではなく、他のページから遷移)。Networkタブでコンバージョンリクエストをフィルタ:

  • Google広告:googleadservices.com/pagead/conversion/
  • GA4:google-analytics.com/g/collect
  • Meta:facebook.com/tr

遷移後にリクエストが表示されれば、タグは正しく発火しています。フルページリロード時にのみ表示される場合、タグはページ読み込み専用に設定されており、SPAナビゲーションをキャッチしていません。

ステップ2:戻るボタンのシナリオをテスト

コンバージョンをトリガーした後、戻るボタン→進むボタンを押します。Networkタブを再度確認——コンバージョンリクエストが2回目に発火しましたか?発火した場合、二重計測の問題があります。

ステップ3:GTMプレビューモードの限界

GTMプレビューモードはSPAのテストでは不安定なことがあります。プレビューパネルが、実際にはネットワークリクエストが送信されていないのにタグを「発火済み」と表示したり、History Changeイベントを正しく反映しないことがあります。GTMプレビューだけでなく、必ずNetworkタブでクロスチェックしてください。

ステップ4:クリーンな環境でテスト

SPAはユーザーのアクセス方法によって動作が異なります:

  • URLの直接入力(フルページ読み込み)vs アプリ内ナビゲーション(SPAルート変更)
  • Service Workerがページをキャッシュしているかどうか
  • 広告ブロッカーがタグに干渉しているかどうか

両方のパスをテストしてください:サンクスページへの直接URLアクセスと、アプリ内からのナビゲーション。

7. SPAにおけるサーバーサイドタギング

SPAはクライアントサイドの計測障害に特に脆弱です:広告ブロッカーがgoogleadservices.comfacebook.com/trなどのトラッキングエンドポイントへのリクエストを遮断し、同意管理プラットフォームがタグの発火前にブロックする場合があり、重いクライアントサイドJavaScriptがタグ実行を遅延・妨害する可能性もあります。

サーバーサイドGTM(sGTM)は、トラッキングリクエストを自社ドメイン経由でルーティングすることでこれらの問題に対処します:

課題 クライアントサイドのみ サーバーサイドGTM併用
広告ブロッカーによるCVリクエスト遮断 リクエストがブロックされ、CV損失 リクエストはまず自社ドメインに送信、サーバー側で転送——ブロックされない
SPAでのConsent Mode v2 仮想ページビューのたびに同意状態の再確認が必要(ページリロードで再初期化されない) サーバーサイドコンテナが同意状態を一貫して管理可能
タグ実行速度 複数のサードパーティスクリプトがクライアントで読み込まれ、SPAを遅延させる 自社サーバーへの1リクエストのみ。サーバーが複数プラットフォームへ分配

SPAでサーバーサイドタギングを検討すべきケース:

  • オーディエンスの広告ブロッカー使用率が20〜30%を超える
  • サーバーサイドの注文件数と報告されたCV件数に大きな差がある
  • EU圏のプライバシー規制に対応し、ブラウザから送出されるデータをより厳密に管理したい

実務上の注意: サーバーサイドGTMはクライアントサイドのSPAトラッキングを置き換えるものではなく、補完するものです。SPAコードで適切なタイミングで適切なイベントを発火させる必要がある点は変わりません。サーバーサイドタギングが変えるのは、イベントが「どこで処理されるか」であり、「いつ発火するか」ではありません。

8. SPAのためのGTM設定例

SPAでコンバージョンを計測するための、完全なGTM設定例です。

データレイヤープッシュ(アプリコード側):

// ルート変更のたびに実行
function onRouteChange(path, title) {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    event: 'spa_pageview',
    spa_page_path: path,
    spa_page_title: title
  });
}

GTMトリガー:

  • タイプ:カスタムイベント
  • イベント名:spa_pageview
  • このトリガーの発火タイミング:一部のカスタムイベント
  • 条件:spa_page_path が /thank-you に等しい

GTMタグ:

  • タイプ:Google広告コンバージョントラッキング(またはGA4イベント、Meta Pixelなど)
  • トリガー:上記のSPAコンバージョントリガー

これにより、トラッキングロジックはGTMに集約(管理しやすい)しつつ、ナビゲーションのシグナルはアプリに任せる(SPAで信頼性が高い)構成になります。

よくある質問

Q. マルチページサイトからSPAに移行する場合、GTMの設定を変える必要がありますか? A. はい。組み込みの「すべてのページ」や「ページビュー」トリガーで発火するタグは、SPAナビゲーションでは発火しなくなります。ルート変更で発火すべきすべてのタグに、History Changeトリガーまたはデータレイヤーイベントトリガーを追加する必要があります。

Q. SPAでフォーム送信のコンバージョンが計測されません。何が違いますか? A. SPAではフォーム送信がJavaScript(AJAX/fetch)でページ遷移なしに行われることが多く、ページ読み込みトリガーもHistory Changeトリガーもキャッチしません。フォームの送信成功ハンドラでコンバージョンイベントを直接発火するか、送信完了時にデータレイヤーイベントをプッシュする必要があります。詳しくはフォーム送信が計測されない原因と対処法を参照してください。

Q. フルページリロードではコンバージョンタグが発火するのに、SPAナビゲーションでは発火しません。すぐにできる対処はありますか? A. 最も手早いのはGTMのHistory Changeトリガーです(コード変更不要)。ただし信頼性の面では、アプリコードからデータレイヤーにカスタムイベントをプッシュするほうが優れています。発火のタイミングを正確に制御できるためです。

Q. Googleのgtag.jsはSPAナビゲーションを自動的に処理しますか? A. Googleタグ(gtag.js)にはある程度のSPAサポートがあり、History APIの変更を検知してページビューイベントを送信できます。ただしコンバージョンイベント(gtag('event', 'conversion', ...))は、正しいタイミングで明示的に呼び出す必要があります。コンバージョン計測では自動検知に頼らないでください。

Q. サーバーサイドレンダリング(SSR)を使っています。この問題は関係ありますか? A. 部分的に関係します。初回ページ読み込みは本当のページ読み込み(SSRが完全なHTMLを送信)なので、最初のヒットではタグが通常通り発火します。ただしアプリ内の以降のナビゲーションはクライアントサイドのSPA遷移のままです——それらにはSPA対応が必要です。Next.jsでは、最初のページ読み込みは問題なく動きますが、<Link>をクリックして別ページに移動するのはSPA遷移です。

まとめ:タグは壊れていない——ページ読み込みが起きていないだけ

SPAでコンバージョン計測が失敗するのは、タグが壊れているからではありません。タグ自体は正常です。ただ、初回読み込み以降には発生しないイベント(ページ読み込み)を待ち受けているだけです。対処はつねに同じパターンです:ナビゲーションが起きたことをタグに伝える——データレイヤープッシュ、History Changeリスナー、またはコンポーネントコード内での直接関数呼び出しで。

より微妙な問題は検証です。コンバージョン計測の検証チェックリストをベースにSPA固有のチェックを追加してください。SPAではユーザーがページにたどり着いた経路によって動作が変わるため、あるシナリオでは動くが別のシナリオでは失敗する——GTMプレビューではその違いが見えないことがあります。必ずNetworkタブで検証し、直接URLアクセスとアプリ内ナビゲーションの両方をテストしてください。

ConversionOKは、ページの構築方法に関係なく、独立したブラウザでライブページをテストし、コンバージョンシグナルが実際に送信されているかを確認します。SPAでは、タグは設置されているがコンバージョンページで発火していないケース——ネットワークレベルで見ないと分からない問題——を検出できます。