高速化をアピール?
こんにちは、中山です。
DMP を活用したマーケティングが注目を集めていますね。
DMP は Web サイトに設置されたタグと呼ばれるコードスニペットを経由して情報を収集します。
そしてタグは Google タグマネージャのようなツールを活用することで、効率的な管理が可能になります。
今回はタグマネージャに関連するお話です。
複数のベンダがタグマネージャを提供していますが、タグの効率的な管理に加えて高速化をアピールするプロダクトもあります。
高速化に寄与する主な要因として
- 複数タグを一括取得することで通信コストの削減
- サーバーダイレクト方式を活用したブラウザ側処理コスト(通信コストを含む)の削減
- 非同期実行によりページ描画を優先することでユーザーの体感速度を改善
がありますが今回 3. を掘り下げてみます。
体感速度に悪影響
Web サイトの表示速度改善のため JavaScript は出来るだけ </body> 直前に設置、というセオリーがあります。
ブラウザは <script> を検出すると、ページ描画(</script> 以降の HTML 解釈)を一旦保留して JavaScript の読み込みや処理を同期実行するためです。
(参考 : What is a non-blocking script?)
しかし、ページ先頭付近への設置が推奨されるタグもあります。例えば計測用途のタグなどです。
そもそもタグやタグマネージャは Web サイト管理者の都合で設置されるため、セオリー通り </body> 直前であることは担保されません。
その結果、設置箇所でページ描画が保留され、ユーザーの体感速度に悪影響が生じる場合があります。
非同期実行機能
これを回避する工夫がタグマネージャの非同期実行機能です。
ページ描画を保留する「同期」実行を出来るだけ減らして「非同期」実行(後回し)にすることが、この機能の基本的な考え方です。
例えばこのような delay.php に対して
1 |
<?php sleep(3); ?> |
以下の場合「Hello, world!」の表示前に 3 秒間待たなければなりません。
1 2 3 4 5 6 7 8 9 10 11 |
<script> function hoge() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'delay.php', false); /* 同期リクエスト */ xhr.send(); } hoge(); </script> <span>Hello, world!</span> |
以下の場合は同期リクエストのレスポンスを待たずに「Hello, world!」が表示されます。
1 2 3 4 5 6 7 8 9 10 11 |
<script> function hoge() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'delay.php', false); /* 同期リクエスト */ xhr.send(); } window.setTimeout(hoge, 0); </script> <span>Hello, world!</span> |
この考え方を活用して、タグマネージャの管理するタグを非同期実行にすることが出来れば、ユーザーの体感速度を改善できそうです。
document.write() の存在
しかし、ここで問題になるのは document.write() の存在です。
例えば、以下の場合「Hello, world!」と表示されます。
1 2 3 4 5 6 7 8 9 |
<script> function hoge() { document.write('<span>Hello,</span>'); } hoge(); </script> <span>world!</span> |
これはブラウザが
1 2 |
<span>Hello,</span> <span>world!</span> |
のような HTTP ストリームと同様に処理するためです。
(参考 : overview-of-the-parsing-model)
しかし、以下の場合は「Hello,」しか表示されません。
1 2 3 4 5 6 7 8 9 |
<script> function hoge() { document.write('<span>Hello,</span>'); } window.setTimeout(hoge, 0); </script> <span>world!</span> |
暗黙的な document.open() の呼び出しで hoge() 関数実行前の文書を破棄してしまうためです。
(参考 : https://developer.mozilla.org/ja/docs/Web/API/document.write)
実際、タグの中には document.write() を使うものがしばしば存在します。
そのようなタグを非同期実行にするためにはどうすればよいでしょうか?
置き換える
問題になるのが document.write() ならば、それを別なオブジェクトに置き換えてしまいます。
そして、自力でコンテンツをパースし <script> を検出した場合、新たに SCRIPT 要素を生成し非同期実行を実現します。
以下はそれを実現する appendHtml のサンプルコードです。
(再帰的に appendHtml が呼ばれるケースなどは検証不十分ですがご容赦ください)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<script> /* 要素の複製(子ノードは除外) */ HTMLElement.prototype.cloneTop = function() { var el = document.createElement(this.nodeName); for (var i = 0; i < this.attributes.length; i++) { var attr = this.attributes.item(i).name; el[attr] = this[attr]; } return el; }; /* 非同期タグを要素に追加 */ HTMLElement.prototype.appendHtml = function(in_html) { document.write = (function(in_self) { return function(in_text) { in_self.appendHtml(in_text); }; })(this); var buff = document.createElement('DIV'); buff.innerHTML = in_html; for (var i = 0; i < buff.childNodes.length; i ++) { var cl = buff.childNodes.item(i).cloneNode(true); if (cl.nodeName == 'SCRIPT') { var script = cl.cloneTop(); this.appendChild(script); if (cl.text) { script.text = cl.text; } } else { if ((cl.nodeType == 1) && (cl.getElementsByTagName('SCRIPT').length > 0)) { var el = cl.cloneTop(); this.appendChild(el); el.appendHtml(cl.innerHTML); } else { this.appendChild(cl); } } } }; </script> |
例えば document.write() を用いるタグ testTag が存在したとして、以下のようなコードで「Hello, world!」の表示が確認できます。
1 2 3 4 5 6 7 |
<script> var testTag = '<script>document.write("<span>Hello, world!</span>");</script>'; window.setTimeout(function() { document.body.appendHtml(testTag); }, 0); </script> |
蛇足ですが、多くの場合 document.write() で書き出されるのは SCRIPT タグもしくはビーコンと呼ばれる小さな画像を取得する IMG タグです。
まとめ
タグマネージャは概ね上記のような工夫で、タグの非同期実行をサポートしています。
とは言っても、ブラウザの処理と差異が生じてしまう場合もあります。
レアケースですが、以下のようなタグ
1 2 3 |
<script> document.write('<h1'); </script> style='color:red;'>hello</h1> |
が存在した場合、タグマネージャの非同期実行では期待動作は得られません。
当然ですが、同期的に実行されることを前提にしたタグの場合も期待動作は得られません。
タグ毎に同期実行、非同期実行の切り替えが設定できることが理想的ですね。
Comments
追記 :
あらためてネットを検索してみると async-document-write.js というものがあったので内容確認してみます。