【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装

catch-img

Google Tag Manager(以下 GTM)には、実行の順序を制御する方法がいくつかあるかと思います。

本記事ではその中でも3つの方法を紹介しつつ、最後に「非同期処理を待機して、非同期処理が完了してから次のタグ実行をさせる方法」についても紹介できればと思います。

前半の話が長いので、非同期処理を待つ方法のみ知りたい方は目次から飛んでください。

また、記事の中で何度も「同期処理(同期的処理)」「非同期処理(非同期的処理)」というワードが出てきますが、解説(といっても簡易的な説明)は記事の後半になっています。

序盤でも登場するワードなのですが「同期?非同期?なにそれ?」となっても「そういうものがあるんだな」くらいの感じで読み進めてもらって大丈夫です。

GTMタグの順序を制御する方法

有名どころだと3つあると思っています。

  1. 「カスタムイベントで次のタグを呼び出す」
  2. 「タグ配信の優先度」を設定する
  3. 「タグの順序付け」を利用する

の3つですね。

それぞれ簡単に紹介していきます。

カスタムイベントで次のタグを呼び出す

言葉で説明すると

タグAの同期的処理のコードの最後に
カスタムイベントを発生させ、
そのカスタムイベントをトリガーにして
タグBを発火させる

ということになります。(数珠つなぎにするようなイメージです)

言葉にするとよく分かりませんので具体例を見てみましょう。

「タグA」のあとに「タグB」を発火させたいという状況だとします。

■タグA

<script>
  console.log("%c[タグA]", "background: red; color: white;")
</script>

■タグB

<script>
  console.log("%c[タグB]", "background: blue; color: white;")
</script>

このとき、タグAにちょっと追記します。

<script>
  console.log("%c[タグA]", "background: red; color: white;")
</script>
 
<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: "AtagPush"
  });
</script>

そしてタグBのトリガーとしてカスタムイベントで AtagPush を指定します。

これで設定は完了です。シンプルですね。

概念を図にすると以下のような感じになるかと思います。

タグAの処理が開始され、「実行させたい部分」が終了したら
dataLayer.push で AtagPush というイベントを発生させる。

そして、「AtagPush」が発生したら動くトリガーを作り、タグBを発火させることで
「タグA発火 → AtagPushイベント → タグB発火」となるため
同期的な処理をしている限りは、確実に「タグA → タグB」の順序にすることができます。

実際のログも以下のような順序になります。

この方法の良いところは
タグAのあとに発火させたいタグが複数あった際(タグB、タグC、タグDなど)
それらすべて AtagPushというトリガーにすれば要件を満たせるところです。

後述の「タグの順序付け」は1つ1つ setUpや cleanUpを設定するのが少々面倒。。
「タグ配信の優先度」は タグ一覧をパッと見た時にどのような優先度になっているか分かりづらい。

一方でこの方法は複数タグについても扱いやすいですし
タグ一覧でトリガーを見れば「このタグの後に発火させたいのね」ということも分かります。

広告関連で MetaでもTikTokでも何でも良いですが
「ベースコード(1つ)」のあとに「イベントコード(複数ある)」を発火させたい
のような状況で使われている印象です。

タグ配信の優先度

タグ配信の優先度というのは
GTMでタグ設定をする際、「詳細設定」の最初にある項目ですね。

値の大きいタグが値の小さいタグより前に配信されるというシンプルな設定です。
例えば優先度5のタグは 優先度1 や 優先度3のタグより前に配信されます。

コード上の仕組みも非常にシンプルでざっくり以下のようなことをやっています。

発火対象のタグを全部集める
↓
各タグの priority値(優先度の値)を決定
↓
シンプルにソートする(高い priority が前)
↓
ループを回し、ソート順にタグ実行する

と、これだけです。

エンジニア目線でいうとだいたい以下のようなことが行われています。
(さらっと読み飛ばしてもらってOK)

var fireTagsForEvent = function (firingMap, eventContext) {                                                                                                                                                                  
  var pendingTags = [];  // ソート・実行待ちのタグリスト                                                                                                                                                                 
                                                                                                                                                                                                                              
  for (var tagIndex = 0; tagIndex < allTagDefinitions.length; tagIndex++) {                                                                                                                                          
    if (firingMap[tagIndex]) {  // このイベントで発火すべきタグか?                                                                                                                                                          
                                                                                                                                                                                                                              
      var tagDef = allTagDefinitions[tagIndex];  //  タグの定義オブジェクト                                                                                                                                       
      var completionCallback = makeCompletionCallback(eventContext.ld); 
                                                                                                                                                                                                                              
      try {     
        // setUp/cleanUp チェーンを含む「実行クロージャ」を組み立てる                                                                                                                                                       
        var executeClosureOrNull = buildExecuteClosure(                                                                                                                                                                      
          tagIndex,                                                                                                                                                                                                          
          { onSuccess: completionCallback, onFailure: completionCallback, terminate: completionCallback },                                                                                                                   
          eventContext,                                                                                                                                                                                                      
          tagIndex
        );                                                                                                                                                                                                  
                 
        if (executeClosureOrNull) {                                                                                                                                                                                          
          var tagTypeName = tagDef[Ff.Va];  // タグタイプ名(例: "__html", "__googtag" など)
          var templateMeta = sandboxedTemplates[tagTypeName];  // テンプレート定義(ない場合は undefined)                                                                                                       
                                                                                                                                                                                                                              
          pendingTags.push({                                                                                                                                                                                                 
            tagIndex: tagIndex,  // ソートの第2キー(同優先度なら定義順)                                                                                                                                               
            priority: (templateMeta ? templateMeta.priorityOverride || 0 : 0)                                                                                                                                                
                      || getEntityPriority(tagTypeName, 1) 
                      || 0,                                                                                                                                                                                                  
            execute: executeClosureOrNull                                                                                                                                                                                    
          });                                                                                                                                                                                                                
        } else {
          // 発火できない状態(ブロック済みなど)→ 完了通知だけ送る
          reportTagSkipped(tagIndex, eventContext); 
          completionCallback();                                                                                                                                                                                              
        }                                                                                                                                                                                                                    
      } catch (err) {                                                                                                                                                                                                        
        completionCallback();
      }
    }                                                                                                                                                                                                                        
  }
                                                                                                                                                                                                                              
  pendingTags.sort(comparePriority);  // 優先度降順、同値なら tagIndex 昇順                                                                                                                                             
  for (var i = 0; i < pendingTags.length; i++) {
    pendingTags[i].execute();  // 同期的に順番に呼ぶだけ                                                                                                                                                                     
  }                                                                                                                                                                                                                          
 
  return pendingTags.length > 0;                                                                                                                                                                                             
};   

以下のように優先度が 100(タグC), 50(タグD), 25 (タグE)があったら
優先度が高い順に発火するということになります。(トリガーはすべて All Pages(PageView))

ログは次のような感じです。

1点注意としては、これはあくまで「トリガータイミングが同じ」時の優先度になります。

例えば 先程の例ではすべて「PageView」のタイミングだったので「タグC」が優先度100で最初に発火しますが、
タグCをDOMReadyのタイミングにして他のタグを PageViewのタイミングにしたら、当然タグCの方があとに発火することになります。

よくある誤解

「タグ配信の優先度」においてたまに次のように説明している記事などを見かけます。

「タグの処理”開始“の順序を決めているだけで、前のタグの”完了“は待たない」

つまり以下のようなことが起こり得ますと言っているかと思います。
タグA(優先度100)がまだ処理中なのにタグB(優先度50)の処理が始まることがある、と。

これは半分正しいですが半分正しくないです。

JavaScriptはシングルスレッドなので、タグのコードに同期的処理しかない場合は
タグAを処理をしている(メインスレッドを占有している)
途中で別のタグの処理が始まる、みたいなことはありえないです。

これは前述の「カスタムイベントで次のタグを呼び出す」方法でも
後述の「タグの順序付け」の方法でも同様です。

一方で、コードの中に非同期処理が存在する場合は
メインスレッドから処理が切り離されるため
タグAの非同期処理の完了を待たずにタグBが始まる、ということはあります。

ただし、これも前述の「カスタムイベントで次のタグを呼び出す」方法でも
後述の「タグの順序付け」の方法でも同様なんですよね。

「カスタムイベントで次のタグを呼び出す」方法なら非同期処理を待つのか?・・・というと待ちませんし
「タグの順序付け」の方法でも特殊な書き方をしない限り非同期処理は待ちません。
※逆にいうと「タグの順序付け」の場合は特殊な書き方をすると非同期処理も待つことはできるのですが、その話は後ほど。


正確には処理の『思想』としては確かに異なります。

「タグ配信の優先度」は「処理の開始順」を決めているは正しい。
「タグの順序付け」や「カスタムイベントで次のタグを呼び出す」は「処理完了の合図を待つ」という思想です。

ただ、表面的な「タグが動く順番」だけみると
同期的処理しかないのであればどちらの場合でも
「タグAの処理が終わってからタグBを発火」という結果になりますし
非同期処理が入っているのであれば
「非同期処理の完了を待たずにタグBが始まる」という結果も変わりません。

タグの順序付け

「タグの順序付け」もGTMの詳細設定にある設定項目ですね。

メインのタグとして「タグX」があるとします。

setUpとして「タグX – setUp」を指定すると
順序としては 「タグX – setUp」発火→「タグX」発火の順序になる、というものです。

また、cleanUpとして「タグX – cleanUp」を指定すると
順序は「タグX」発火→「タグX – cleanUp」発火になります。

同時に setUpと cleanUpを設定することもできます。

例えば以下のように設定します。

なお、setUpと cleanUpのタグには明示的にトリガーを設定しなくても大丈夫です。

PageViewでも発火させつつ、タグXの前にも発火させたいなどの意図があれば
PageViewのトリガーを設定すれば良いと思いますが
「タグXの前(後)」に発火させたい用途であればトリガー自体は不要でsetUp(cleanUp)の設定がしてあればOKです。

※なのでタグ一覧でみると以下のようにトリガーがないように見えます

このときのログは次のような形です。

これも実装自体はとてもシンプルですね。

ここからエンジニア目線でもう少し深堀りしたいと思います。
コードもわりとバンバン出てくるので結果をなんとなくみたい方は
setTimeoutでラップされることの影響まで飛ばしてください

フェーズとしては主に3つに分かれます。

※なお以下のコードは重要な部分を抽出しているだけですので
実際のコードとは異なるところはありますので、その点ご了承ください

概要

まず概要をお話します。

setUp / cleanUpの話をするにあたり重要なのが「コールバック」です。

GTM内のほぼすべてのタグは同期的処理が完了したタイミングで『自動で』いわゆる「成功コールバック」が呼ばれます。
通常はGTMへの内部通知(タグ処理成功したよーという内部的通知)の関数が実行されるだけです。

しかし、例えばsetUpの設定がされている場合は
setUpタグが完了したときのコールバック関数が「メインのタグそのもの」に差し替わっており
setUp完了→成功コールバック(=メインのタグ)実行
という流れになるというのが大まかな仕組みです。

フェーズ1:チェーン構築

1. 実行キューの作成

発火条件を満たしたメインタグに対して、実行の準備を始めます。
「タグ配信の優先度」の箇所でも記載した以下のコードですね。

var fireTagsForEvent = function (firingMap, eventContext) {                                                                                                                                                                  
    var pendingTags = [];  // ソート・実行待ちのタグリスト                                                                                                                                                                 
                                                                                                                                                                                                                                
    for (var tagIndex = 0; tagIndex < allTagDefinitions.length; tagIndex++) {                                                                                                                                          
      if (firingMap[tagIndex]) {  // このイベントで発火すべきタグか?                                                                                                                                                          
                                                                                                                                                                                                                                
        var tagDef = allTagDefinitions[tagIndex];  // タグの定義オブジェクト                                                                                                                                       
        var completionCallback = makeCompletionCallback(eventContext.ld); 
                                                                                                                                                                                                                                
        try {     
          // setUp/cleanUp チェーンを含む「実行クロージャ」を組み立てる                                                                                                                                                       
          var executeClosureOrNull = buildExecuteClosure(                                                                                                                                                                      
            tagIndex,                                                                                                                                                                                                          
            { onSuccess: completionCallback, onFailure: completionCallback, terminate: completionCallback },                                                                                                                   
            eventContext,                                                                                                                                                                                                      
            tagIndex
          ); 
                   
          if (executeClosureOrNull) {                                                                                                                                                                                          
            var tagTypeName = tagDef[Ff.Va];  // タグタイプ名(例: "__html", "__googtag" など)
            var templateMeta = sandboxedTemplates[tagTypeName];  // テンプレート定義(ない場合は undefined)                                                                                                       
                                                                                                                                                                                                                                
            pendingTags.push({                                                                                                                                                                                                 
              tagIndex: tagIndex,  // ソートの第2キー(同優先度なら定義順)                                                                                                                                               
              priority: (templateMeta ? templateMeta.priorityOverride || 0 : 0)                                                                                                                                                
                        || getEntityPriority(tagTypeName, 1)
                        || 0,                                                                                                                                                                                                  
              execute: executeClosureOrNull                                                                                                                                                                                    
            });                                                                                                                                                                                                                
          } else {
            // 発火できない状態(ブロック済みなど)→ 完了通知だけ送る
            reportTagSkipped(tagIndex, eventContext);
            completionCallback();                                                                                                                                                                                              
          }                                                                                                                                                                                                                    
        } catch (err) {                                                                                                                                                                                                        
          completionCallback();
        }
      }                                                                                                                                                                                                                        
    }
                                                                                                                                                                                                                                
    pendingTags.sort(comparePriority);  // 優先度降順、同値なら tagIndex 昇順                                                                                                                                             
    for (var i = 0; i < pendingTags.length; i++) {
      pendingTags[i].execute();  // 同期的に順番に呼ぶだけ                                                                                                                                                                     
    }                                                                                                                                                                                                                          
   
    return pendingTags.length > 0;                                                                                                                                                                                             
  };   
2. 順序付けの組み立て

上記コードの 「buildExecuteClosure」関数の部分ですね。

タグのデータを読み込み、setUpが設定されているか確認します。

setUpが設定されていた場合、setUpタグの成功コールバックに「メインタグの実行関数」を渡している、という点がミソです。

つまり、setUp完了→成功コールバック(=メインのタグ)実行、という基本形がこの時点でセッティングされます。

var buildExecuteClosure = function (tagIndex, callbacks, eventContext, triggerIndex) {
  var tagDef = allTagDefinitions[tagIndex];
   
  // 1. まず「このタグ自体」を実行するための関数を生成する
  var executeThisTag = createTagExecutionFunction(tagIndex, callbacks, eventContext, triggerIndex);
  if (!executeThisTag) return null;
 
  // 2. Setupタグ(setup_tags)が設定されているかチェック
  var setupTags = evaluateMacros(tagDef["setup_tags"], eventContext, []);
  if (setupTags && setupTags.length) {
    var setupTagRef = setupTags[0];
     
    // 3. ★ Setupタグの実行関数を作るため、再帰呼び出しをする。
    // この時、Setupタグが「成功した時のコールバック(onSuccess)」として、
    // さっき作った「メインタグの実行関数 (executeThisTag)」を直接いれる
    executeThisTag = buildExecuteClosure(
      setupTagRef.index,
      {
        onSuccess: executeThisTag, //  次のバトン(メインタグ)を渡す
        onFailure: setupTagRef.condition === 1 ? callbacks.terminate : executeThisTag,
        terminate: callbacks.terminate
      },
      eventContext,
      triggerIndex
    );
  }
   
  // 最終的に「一番最初に実行すべきタグ(SetupがあればSetup)」の関数を返す
  return executeThisTag;
};
3. コールバックのバインド

上記の「タグ自体を実行する関数」を作成する createTagExcecutionFunction がこちら

var createTagExecutionFunction = function (tagIndex, callbacks, eventContext, triggerIndex) {
  var tagDef = allTagDefinitions[tagIndex];
  var nextTagToRun = callbacks.onSuccess; // buildExecuteClosure から渡された、次に走るタグの関数
   
  var executeFunction = function () {
    var tagPayload = evaluateMacros(tagDef, eventContext, []);
    var isCompleted = false;
 
    // このタグが成功した時にGTM内部から呼ばれるプロパティを定義
    tagPayload.vtp_gtmOnSuccess = function () {
      if (!isCompleted) {
        isCompleted = true;
        // ステータス更新などの内部処理...
         
        // ★ ここで buildExecuteClosure から渡された onSuccess (次のタグ) を実行する
        nextTagToRun(); 
      }
    };
     
    // 仮想マシン(ディスパッチャ)へタグのペイロードを投げて実行開始
    dispatchToSandbox(tagPayload, { event: eventContext, index: tagIndex, type: 1 });
  };
 
  return executeFunction;
};

この関数内で、渡された onSuccess がタグ内部のプロパティ(vtp_gtmOnSuccess)にセットされる形です。

なお、setUpの構築は buildExecuteClosure で走りますが、cleanUpの方はこちらの createTagExecutionFunction の中で構築処理が走ります。が、今回はそちらは省略しています。
以降も、setUpの方に焦点を当てて話を進めていきます。

disPathToSandBox関数では色々行われていますが
大事なのは「vtp_gtmOnSuccess」というプロパティ名が「gtmOnSuccess」という名称にリマップ処理されています。

また、dispatchToSandbox 自体は汎用的なディスパッチャーであり
今回は カスタムHTML の中で使われているため戻り値を捨てています(※※)が
例えばカスタムJavaScriptやマクロ評価にもこのディスパッチャーが使われていて
その場合は戻り値が重要になってきます。

※※ コード中に 以下の記述がありますが、戻り値をどこにも格納していないし、使われていない、ということを言っています

// 仮想マシン(ディスパッチャ)へタグのペイロードを投げて実行開始
dispatchToSandbox(tagPayload, { event: eventContext, index: tagIndex, type: 1 });
4. dispatchToSandbox の内容
var dispatchToSandbox = function (rawTagPayload, executionMetadata) {
  // rawTagPayload["function"] で、実行する機能の種類を取得
  // 例: "__html" (カスタムHTML)、"__gaawe" (GA4タグ)、または "__read_data_layer" などの権限名
  var functionName = rawTagPayload["function"]; 
  var eventContext = executionMetadata && executionMetadata.event;
 
  if (!functionName) throw Error("Error: No function name given for function call.");
 
  // builtInFunctions は、GTM内部の「ネイティブタグ(GA4等)」と「権限生成関数」の辞書。
  // カスタムHTMLなど、実際にサンドボックスで隔離して動かすタグの場合は undefined になる。
  var builtInFunction = builtInFunctions[functionName]; 
   
  // ★ サンドボックス(または組み込み関数)に渡すための「綺麗な新しいオブジェクト」
  var sanitizedPayload = {}; 
 
  // 1. パラメータのリマップ処理 (vtp_ の除去)
  for (var propName in rawTagPayload) {
    if (rawTagPayload.hasOwnProperty(propName)) {
       
      // プロパティ名が "vtp_" で始まっているかチェック
      if (propName.startsWith("vtp_")) {
         
        // 組み込み関数(ネイティブタグや権限生成)に渡す場合なら "vtp_" をそのまま残し、
        // サンドボックスで動かすカスタムタグ(undefined)なら "vtp_" の4文字を取り除く!
        var newPropName = (builtInFunction !== undefined) ? propName : propName.substring(4);
         
        // 綺麗なオブジェクトに詰め直す。
        // ★★★ サンドボックス行きの場合、ここで "vtp_gtmOnSuccess" が "gtmOnSuccess" に変換される! ★★★
        sanitizedPayload[newPropName] = rawTagPayload[propName];
         
      } else {
        // "vtp_" 以外のメタデータ処理 
        // ...
      }
    }
  }
 
  // 2. メタデータの補完 (タグの名前などをセット)
  // ...
 
  // 3. ディスパッチ (実行の振り分け)
  // 組み込み関数(ネイティブタグや権限生成)ならそれを直接実行し、
  // そうでなければ、サンドボックス(sandboxDispatcher) に綺麗なペイロードを渡す!
  return builtInFunction !== undefined 
    ? builtInFunction(sanitizedPayload) 
    : sandboxDispatcher(functionName, sanitizedPayload, executionMetadata); 
};

フェーズ2:実行フェーズ

sandboxDispatcher は以下のコードで返却される関数です。

var createSandboxDispatcher = function () {
    
  // 返されるこのクロージャが、sandboxDispatcher の正体
  return function (functionName, tagPayload, executionContext) {
      
    // 1. サンドボックス内で実際にタグの処理を始める「直前」に、
    // 必ずコールバックの setTimeout ラップ処理を適用する。
    wrapCallbacksWithSetTimeout(tagPayload); 
      
    // 2. 下準備が終わったら、隔離されたサンドボックス環境(GTM独自のインタープリタ)で
    // 実際にタグのコードを評価・実行する
    var result = executeInSandbox(sandboxInterpreter, executionContext, [functionName, tagPayload]); 
      
    return result;
  };
};

大事なのが wrapCallbacksWithSetTimeout の中身

var wrapCallbacksWithSetTimeout = function (tagPayload) {
  var originalSuccessCallback = tagPayload.gtmOnSuccess;
   
  if (typeof originalSuccessCallback === "function") {
    // 元のコールバック(=次のタグを実行する関数)を、
    // setTimeout(..., 0) で実行するように上書き(ラップ)する
    tagPayload.gtmOnSuccess = function () {
      deferToMacroTask(originalSuccessCallback);
    };
  }
};
var deferToMacroTask = function (callback) {
  window.setTimeout(callback, 0); // JavaScriptのタスクキューの最後尾へ送る
};

今回の一連のコードで一番大事な部分がここです!!

端的に記載すると

tagPayload.gtmOnSuccess = function(){
  window.setTimeout([メインタグの実行関数], 0);
}

みたいなことをやっています。

つまり gtmOnSuccess(成功コールバック) が呼ばれる と
その「メインタグの実行関数」を setTimeout の中に入れて実行するってことです。

ここの重要性はまた後述します。先にフェーズ3の説明します。

フェーズ3:DOM挿入・展開

フェーズ3は今回の話では重要な部分ではないのでさらっといきます。

実際に setUpタグが DOMに挿入され展開されることで setUpタグで記述したコードが実行されます。

そして同期的な処理が完了すると上記で定義していた
「成功コールバック(tagPayload.gtmOnSuccess)」が実行される = メインのタグが実行される
ということになります。

※改めてですがメインタグの setUp(設定タグ)に setUpタグを設定している想定です

一連の流れを整理

これまでコード + テキストで 若干ごちゃごちゃしていたと思うので
一旦テキストベースである程度整理したいと思います。

  1. fireTagsForEvent が呼ばれる
  2. buildExecuteClosure が呼ばれる。
  3. createTagExecutionFunction が呼ばれる。
  4. createTagExecutionFunction の中で executeFunction が定義され、これが返却される (= executeThisTag)。(※定義されただけで、実行はされていない)
  5. buildExecuteClosure の中で setUp があった場合、buildExecuteClosure を再帰的に呼んで、戻り値を上書きする。
  6. 最終的に一番最初の executeFunction(= executeThisTag) が buildExecuteClosure の戻り値として、fireTagsForEvent に返却される。
  7. fireTagsForEvent で execute() される。つまり、返却された一番最初の executeFunction(= executeThisTag) が実行される。(setUpタグの実行が走る)
  8. executeFunction(= executeThisTag) が実行されると「この瞬間」に evaluateMacros が走り、最新のデータが取得される。
  9. tagPayload.vtp_gtmOnSuccess に「次のタグ(nextTagToRun)」をキックする関数が定義される。
  10. dispatchToSandbox が呼ばれる。
  11. dispatchToSandbox の中で vtp_gtmOnSuccess が gtmOnSuccess にリマップ(変換)される。
  12. sandboxDispatcher が呼ばれる。
  13. wrapCallbacksWithSetTimeout が呼ばれる。
  14. その中で deferToMacroTask が使われ、gtmOnSuccess が setTimeout(..., 0) を呼ぶように上書きされる。
  15. 実際にDOMに挿入されることで、setUpタグに記述されていたコードが同期的に実行される。
  16. 同期処理が完了すると、GTM内部から gtmOnSuccess が呼ばれるが、中身は setTimeout なのでタスクキュー(マクロタスク)に積まれる。
  17. コールスタック(GTMの現在の処理や dataLayer.push の消化)がすべて空になり、タスクキューの出番が来たら、次のタグ(メインタグ)が安全に実行される。

成功コールバックが setTimeoutでラップされる件

まずこの記事の中で、何度も「同期処理」や「非同期処理」というワードが出てきました。

普段からJavaScriptを使用されている方であればどんなものか理解できるかと思いますが、
簡単に説明します。

JavaScriptはシングルスレッドですので、メインスレッドが占有されている間は基本的には他の処理が動きません。

例外として非同期処理が絡む場合は、それはメインスレッドから切り離されたところで管理されるため
他の処理が動くことができます。

非同期処理の代表例として

  • setTimeout, setInterval のようなマクロタスク
  • DOM操作系(element.click()など)のマクロタスク
  • Promise、fetch(Promise)、MutationObserverなどのマイクロタスク

などが存在します。

マクロタスクとマイクロタスクの説明などを始めると
1本記事ができるレベルのため割愛しますが
これらの非同期処理はメインの処理と切り離されて管理されます(WebAPI管理)。

具体的にはコールスタックと呼ばれるスタックに処理のコンテキストがつまれていき
その中で非同期処理があったらWebAPIに管理権がいき
マクロタスクであればタスクキューに
マイクロタスクであればジョブキュー(マイクロタスクキュー)に積まれていきます。

そして、コールスタックが空にならない限り
タスクキューやジョブキューに積まれている関数は実行されません
。

一方、非同期処理はWebAPIに管理が移るので
他のコールスタック内の処理はブロックされずに実行できます。

とてもざっくり書くと以下のようなイメージ図です。
詳しい説明端折っていて申し訳ないですが、長くなるのでこんな感じで。。。
(需要があれば別途記事にいたします)

今回の話に戻しましょう

メインタグにsetUpタグを設定した場合
setUpタグが実行されたあとに、成功コールバック(=メインタグの実行関数)が呼ばれます。

この成功コールバックは setTimeout([メインタグ実行の関数]) のような形で setTimeoutで包まれる形になって呼ばれるのでした。

つまり、「メインタグ実行の関数」はタスクキューに積まれることになります。

これが何を意味しているのかといいますと
コールスタックが空にならない限り、このメインタグ実行は行われない、ということです。

少々感覚的な話ですが
おそらく皆さんが想像するよりも、メインタグが実行されるのは「遅い」と思います。

setTimeoutでラップされることの影響

例えば、メインタグのトリガーを「PageViewのタイミング」にしていて
setUpタグを設定しているとします。

別で「ウィンドウの読み込みのタイミング」にしている windowLoadタグがあるとします。

普通に考えたら

setUp → メインタグ → windowLoadタグ

の順番で発火して欲しいと思います。

しかし、ページの読みこみが非常に早いページなどでは
windowLoadタグがメインタグより先に発火することも全然起こり得ます。

例えば、gtm.jsのロード中に gtm.loadのイベントが発火してdataLayerに格納されていたら、
gtm.loadの処理も含めてコールスタックが空になるまで
タスクキューに積まれたマクロタスクは実行されないからですね。

実験としてとても軽いサイトを用意しました。
そこで以下の3つのタグを作成します。

  • メインのタグ:PageViewタイミング / setUpで「setUpのタグ」を指定
  • setUpのタグ:メインのタグに対して setUpとして設定
  • window.loadのタグ:ウィンドウの読み込みタイミングで発火

これを実行すると、、、

GTMのプレビュー上では PageView(コンテナの読み込み)タイミングに
「メインのタグ」と「setUpのタグ」が発火しています。

そして、ウィンドウの読み込みタイミングで window.loadのタグが発火しています。

しかし、ログを見てみると以下のようになっており、window.loadのタグの方が早く実行されていることが分かります。

先にお話したように setUpのタグの成功コールバック(=メインのタグ)が
setTimeoutでラップされることでマクロタスク化してタスクキューに積まれるため
コールスタックが空になってからなでないと実行されないのです。

よってタイミングやサイトによっては
PageViewタイミングで設定したはずのタグが
ウィンドウの読み込みタイミングより遅いということもありえます。

GTMの setUp / cleanUp の設定はあくまでも
『設定した該当の2つのタグに対して、順序を保証する』というものです。

この設定をした時点で非同期化されてしまい他のタグとの順序関係は崩れます。
PageViewタイミングで設定したタグに setUp をセットすると
今回の例で見たように、window.loadや DOMReadyのタグが
PageViewタイミングのタグより先に発火することもあるということですね。

また、「設定した2つのタグについては順序を保証する」と書きましたが
これもあくまで同期処理しかない場合です。

非同期処理がはさまる場合、例えば setTimeoutで3秒待つ、などする場合は
この3秒を待たずに次のタグに処理が進みます。

非同期処理を待つ方法は次の章で。

ちなみに、成功コールバックを setTimeoutでラップするのは
setUpやcleanUpを使った時だけではなく
GTMのほぼすべて(カスタムHTML場合は必ず)の成功コールバックは setTimeoutでラップされます
。

しかし、前述したように、通常の成功コールバックは「GTM内部に通知するだけの関数」ですので
これが setTimeoutでラップされたところで ユーザー体験的には何も問題ない場合がほとんどです
。

非同期処理を待つ方法

お待たせしました。
ある意味ここからが本題です。

ただ、長くなりすぎたためここでは『実際の方法だけ紹介する』に留めて
コードベースのロジックはまた別の機会に記事にしたいと思います。

さて、何回も書いているように
GTMは基本的に非同期処理を待つということはしません。

これは「カスタムイベントで次のタグを呼び出す」方法でも「タグ配信の優先度」でも「タグの順序付け」を使う方法でも基本的には同じです。

ただ、「タグの順序付け」を使う方法では非同期処理を待つ方法が存在します。
これが「タグの順序付け」を使う一番のメリットといっても過言ではありません。

まずは普通に書いた場合どうなるのかというのを見てみましょう。

通常の「タグの順序付け」

2つのタグを用意します。

■メインタグ – 2秒待機

setTimeout(非同期処理)で2秒待ったあとに、ログを出力します。
setUp(設定タグ)には 後述の 「setUp – 3秒待機」をセットします。

<script>
  (function(){
    var start = window.__start;
   
    setTimeout(function(){
      console.log("%c[メイン - 2秒待機]",
                  "background: red; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 2000);
  })();
</script>

■setUp – 3秒待機

setTimeout(非同期処理)で3秒待ったあとに、ログを出力します。
メインタグの setUp(設定タグ)としてセットされています。

<script>
  (function(){
    var start = window.__start = performance.now();
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 3000);
  })();
</script>

タグ一覧

今までの説明から 「setUp – 3秒待機」が先に動くのは間違いありません。
そして 処理が完了したら成功コールバック = 「メイン – 2秒待機」タグ(がsetTimeoutにラップされた形)で実行されます。

ここの話では 成功コールバックがー・・・とか、setTimeoutでラップされるとー・・・、とか考えると面倒なので、一旦シンプルに 「setUpのタグ」が先に動いて、次に「メインのタグ」が動くと考えてもらって大丈夫です。

我々の期待としては

「setUp – 3秒待機」タグが動く
↓
3秒後にログが出力
↓
と同時に「setUp – 3秒待機」タグの処理が終了
↓
「メイン – 2秒待機」タグが動く
↓
その2秒後(つまり最初から数えると3 + 2 の5秒後)にログが出力

だと思います。

では実際はどうでしょうか。

こうなります。

「メイン – 2秒待機」タグのログが2秒後に出力されてますね。
「setUp – 3秒待機」タグのログは3秒後に出力されています。

これはどういうことでしょうか?

繰り返しですが、「setUp – 3秒待機」のタグが先に動き始めるのは間違いないです。

このタグの中で、処理がsetTimeoutに到着すると
setTimeoutは非同期処理なのでメインスレッドから処理が切り離されます。

そして、setTimeoutは脇においておいて後続の同期処理は普通に終了していましまいます。

同期処理が終わったのでGTMはタグの処理が終了したと判断して
成功コールバック(=「メイン – 2秒待機」)をすぐに呼びます。
(この間は時間にしては一瞬です)

なので「setUp – 3秒待機」と「メイン – 2秒待機」のタグは
ほぼ同時に処理開始することになり、結局2秒後と3秒後にログが出ることになります。

<script>
  (function(){
    var start = window.__start = performance.now();
   
    setTimeout(function(){         // ←非同期処理なのでこれはWebAPIに管理が移る
      console.log("%c[setUp - 3秒待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 3000);
  })();  // ←非同期処理はすっ飛ばして、ここに到着した時点でこのタグは「処理完了」となる
</script>

非同期処理を待つには?

まずは下準備として「変数」の組み込み変数から
「Container ID」と「HTML ID」を有効にしておいてください。

■メイン – 2秒待機 – 非同期待機のコード

こちらはコードの中身は先程と変わりません(わかりやすいようにタグの名前を変えただけです)

<script>
  (function(){
    var start = window.__start;
   
    setTimeout(function(){
      console.log("%c[メイン - 2秒待機 - 非同期待機]",
                  "background: red; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 2000);
  })();
</script>

■setUp – 3秒待機 – 非同期待機のコード

<script>
  (function(){
    var start = window.__start = performance.now();
     
    var gtm = window.google_tag_manager[{{Container ID}}];
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機 - 非同期待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
 
      gtm.onHtmlSuccess({{HTML ID}});  // ★!! ここが重要 !!★
 
    }, 3000);
  })();
</script>

何をしているかというと、google_tag_managerオブジェクトから
自身のコンテナIDのオブジェクトを取得して、onHtmlSuccessメソッドを呼んでいます。

onHtmlSuccessメソッドを手動で呼ぶ記述をしている場合
GTMはこの onHtmlSuccessが呼ばれるまで次のタグの処理を待機
します。

今までの言葉でいいますと onHtmlSuccessを呼ぶことで
「成功コールバック(=「メイン – 2秒待機 – 非同期待機」をsetTimeoutでラップした関数)」
を手動で呼び出すということをしています。

※手動で呼び出す前に、同期処理が終わった段階で
自動の成功コールバックが呼ばれるんじゃない?と思いますが
それは内部でうまいこと処理しています。(このあたりは別の記事で)

結果、以下のようなログ出力になります。

3秒後に「setUp – 3秒待機 – 非同期待機」タグのログが出力されて
そこまでの処理が終わってから(setTimeoutで待つ処理も終わってから)
「メイン – 2秒待機 – 非同期待機」タグの処理が開始されます。

そしてさらに2秒後(最初から数えると 3 + 2 = 5秒後)に
「メイン – 2秒待機 – 非同期待機」タグのログが出力されます。

まとめ

はい、いかがでしたでしょうか?

GTMでタグの実行順番を制御する方法を
コードも交えつつ同期処理と非同期処理の挙動にも言及しながら
紹介させていただきました。

最後に setTimeout などの非同期の処理を待ってから
次のタグの処理へ進ませる方法も紹介いたしました。

非同期待機のロジックについてはまた別の機会に。

また、今回はGTMの話でしたが
Adobe Data Collection(Launch)においても
非同期処理を待つ方法というのは存在しています。

こちらも気になる方がいればお問い合わせください。

Tomonaga
Tomonaga

人気記事ランキング

タグ一覧