RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

タグマネージャのカスタムHTMLで外部JS読み込んだ場合の実行順番を調べてみた

こんにちは、羽山です。

Webエンジニアをやっていると Google Tag Manager に触れる機会ってありませんか?
マーケティング部門から広告タグの相談を受けた り、 タグマネージャから出る謎のJSエラーの調査 を依頼されたりと、今やWebシステムの構成要素としてなくてはならないものとなっています。

一方でエンジニアの管理を離れて自由奔放に運用されたタグマネージャは伏魔殿の様相を呈してくるので、できればお近づきになりたくないですがそうは言っていられません。

そんな中で今回出会ったのはタグマネージャのカスタムHTML内で外部JSファイルを読み込み、直後のインラインscriptでその実行結果を利用するタグでした。
Webエンジニアとしての知識を元にすると エラーが出るはず なのになぜか正常に動作したのでその原因を調査しました。

なぜか正常動作するタグマネージャのカスタムHTML

発端となったタグマネージャのカスタムHTMLは次のような内容でした。(※処理内容を簡略化しています)
外部JavaScriptファイルを読み込みつつ、直後のインラインscriptではその外部JavaScriptファイルの実行結果を利用しています。

<script src="external_10s.js"></script>
<script>
  console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");
</script>

external_10s.js は以下の内容です。

window.externaljs_loaded = 1;

今回は読み込みタイミングの問題を再現するために external_10s.js を配信するサーバーに細工をしていてファイル転送に10秒かかるようにしています。

タグマネージャのカスタムHTMLは動的にscriptタグを生成して実行してくれますが、一般的にJavaScriptのコードで動的生成したscriptタグは async 指定がなくても非同期として扱われます。
つまりこの場合は external_10s.js の実行の前にインラインscriptが実行されるはずです。

(※このあたりの詳しい動作を知りたい場合はフロントエンドのパフォーマンスに関する記事を参照ください)

しかし Google Tag Manager で上記タグを配信したページを開くとコンソールには10秒後に以下の表示が出ました。

externaljs has been loaded

どうやら external_10s.js の読み込みを待ってからインラインscriptタグを実行してくれているようです。
ブラウザ側は動的生成されたscriptタグを非同期実行するはずなのにタグマネージャでは同期実行が必要なコードを問題なく実行できてしまったため、 動かないはずなのに何故動く? というのが本稿のきっかけでした。

動的なscriptタグ追加をタグマネージャを使わずに実行してみる

タグマネージャは理想的な動作しているので何が問題なのか分かりづらい状況です。そこで動的生成されたscriptタグが非同期実行されることをまずは確認してみます。

以下のHTMLおよびJavaScriptコードはscriptタグを動的に2つ生成しています。
前述のタグマネージャのカスタムHTMLと同様に外部のJavaScriptファイルを読み込むタグを生成して、続いて2つめのインラインscriptでは1つめのJavaScriptファイルの実行結果を利用します。

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <script>
    (function() {
        var tag1 = document.createElement('script');
        tag1.src = 'external_10s.js';
        document.head.appendChild(tag1);

        var tag2 = document.createElement('script');
        tag2.innerHTML = 'console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");';
        document.head.appendChild(tag2);
    })();
    </script>
    <title>動的にscriptタグを追加するテスト</title>
</head>
<body>
    動的にscriptタグを追加するテスト
</body>

結果は external_10s.js の読み込み・実行が完了する前にインラインscriptタグが実行されました。コンソール表示も10秒待たずに即時で出力されることが確認できました。

externaljs is not loaded

動的生成したscriptタグは非同期実行される ので、これは妥当な結果です。

HTML内に静的配置すれば同期実行される

次は以下のようにHTML内に直接記述しました。async 指定のないscriptタグは同期実行されるのでインラインscriptは直前のscriptタグの読み込み・実行を待ってから実行されます。

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <script src="external_10s.js"></script>
    <script>
        console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");
    </script>
    <title>htmlに直接記述</title>
</head>
<body>
    htmlに直接記述
</body>

コンソールには以下の表示がされました。

externaljs has been loaded

external_10s.js の読み込みに10秒かかるのでコンソールに表示されたのは10秒経過後でした。

タグマネージャで実行してみる

本題に戻って以下のカスタムHTMLをタグマネージャで配信する動作を確認します。

<script src="external_10s.js"></script>
<script>
  console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");
</script>
externaljs has been loaded

まずは DevTools でDOMツリーの変化を眺めてみます。

表示直後のDOMツリー

<body>
    <!-- Google Tag Manager (noscript) -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-*******"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- End Google Tag Manager (noscript) -->
    タグマネージャで動的に追加するテスト
    <script type="text/javascript" id="" src="external_10s.js"></script>
</body>

10秒経過後のDOMツリー

<body>
    <!-- Google Tag Manager (noscript) -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-*******"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- End Google Tag Manager (noscript) -->
    タグマネージャで動的に追加するテスト
    <script type="text/javascript" id="" src="external_10s.js"></script>
    <script type="text/javascript" id="">console.log(window.externaljs_loaded?"externaljs has been loaded":"externaljs is not loaded");</script>
</body>

<script type="text/javascript" id="" src="external_10s.js"> が追加された10秒経過後にインラインscriptタグが追加されることを確認できました。
つまりタグマネージャは1つめのスクリプトの読み込み完了を待機してから続くインラインscriptタグをDOMに追加することで、擬似的に同期実行を再現しているようです。

以下は Minify されたタグマネージャのソースコードを整形したものです。少々読みづらいですがこの関数で順次scriptタグなどをDOMツリーに追加しています。

function () {
  try {
    if (0 < e.length) {
      var k = e.shift(),
      m = a(d, e, f, g);
      if ('SCRIPT' == String(k.nodeName).toUpperCase() && 'text/gtmscript' == k.type) {
        var n = H.createElement('script');
        n.async = !1;
        n.type = 'text/javascript';
        n.id = k.id;
        n.text = k.text || k.textContent || k.innerHTML || '';
        k.charset && (n.charset = k.charset);
        var p = k.getAttribute('data-gtmsrc');
        p && (n.src = p, jb(n, m));
        d.insertBefore(n, null);
        p || m()
      } else if (k.innerHTML && 0 <= k.innerHTML.toLowerCase().indexOf('<script')) {
        for (var q = [
        ]; k.firstChild; ) q.push(k.removeChild(k.firstChild));
        d.insertBefore(k, null);
        a(k, q, m, g) ()
      } else d.insertBefore(k, null),
      m()
    } else f()
  } catch (r) {
    I(g)
  }
}

e 配列はカスタムHTMLに設定したHTMLをノードに変換したキューとなっていて、e.shift() で取り出された先頭のノードがDOMツリーに追加されます。
例えば以下のカスタムHTMLの場合は script, #text, script, #text の4つのノードに分割されました。

<script src="external_10s.js"></script>
<script>
  console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");
</script>

#text の中身は両方とも \n です。各script終了タグ直後の改行がノードとなってDOMツリーに追加されますが改行だけのテキストノードなので特に影響はありません。

ここで重要なのは external_10s.js のように外部scriptを読み込む場合は、その scriptタグの load イベントにこの関数が設定される点です。
それによって外部scriptファイルの読み込みを待ってから次のノードがDOMツリーに追加される動作となり、実質的に同期処理が保証されています。

async指定したらどうなるか?

カスタムHTMLで外部scriptを読み込みむと同期処理されることを確認しましたが、次は逆に async を付けた場合の動作を確認してみました。

<script src="external_10s.js" async></script>
<script>
  console.log(window.externaljs_loaded ? "externaljs has been loaded" : "externaljs is not loaded");
</script>

async を付けたら先ほどのようなタグマネージャの魔法がかからず素直に非同期動作すると想像していたのですが、結果は残念ながら同期実行されました。

externaljs has been loaded

確かに先ほどのタグマネージャのコード片でも async に対応してそうな雰囲気はないし、以下のように強制的に async=false に設定しているあたりから非同期動作は対応していないようです。

if ('SCRIPT' == String(k.nodeName).toUpperCase() && 'text/gtmscript' == k.type) {
  var n = H.createElement('script');
  n.async = !1;

非同期で読み込みたいタグはタグマネージャから別のカスタムHTMLで配信すればよいだけなので、わざわざ async に対応していないのは仕方なさそうです。

まとめ

システム開発はジェンガを詰んでいくようなものだと思っています。途中のパーツに内在するわずかな動作不安が徐々に蓄積して、最終的には大きくバランスが崩れてしまいます。
今回は 結果として問題なかった のですが、問題ないことを明らかにしておくことも重要です。なぜならば何か問題が起きた際に 疑う先が減る からです。

こういった地道な確認作業が強いシステムを構築する秘訣なのでみなさんも問題や疑問を感じるシーンがあれば、面倒くさがらずしっかり確認しましょう。

関連記事

さて、ラクーングループは一緒に働く仲間を絶賛大募集中です!
フロントからバック、はたまた企画に要件定義など幅広く経験できる環境です。もしご興味を持っていただけましたら、こちらからエントリーお待ちしています!

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc