🐷

GTMを入れたらパフォーマンスがめちゃくちゃ落ちていたので改善した

2024/06/08に公開

TBTが異様に低い?原因はGTMにあり

以前Astroで作った静的サイトのPage Speed Insightsスコアを見てみたら50点台になっていた。

画像の圧縮やCLSの低減など基本的な対策はしていて、最初90点以上で安定していたのになぜ急に?
と不思議に思いレポートの詳細を見てみると、GTMの読み込みと、そこから配信されるタグ(HubSpot, Clarity, GA4)が主要な原因っぽい。
なんならLCPやSpeedIndexまでついでに下がっていて、GTM経由で読み込んだタグの処理がLCP前に走るようになっていることで、色々な指標が連動して下がっている模様。
ということで、全ての元凶であるGTM読み込みを改善するべく試行錯誤してみた。

結果

Partytownで88点まで上がった

Partytownを入れるとスコアがグンと伸びた。
まだベータ版なのでなんとなく避けていたが、結局点数を上げるにはこれを入れるしかなさそう。
今回は88点がマックスだったが、CSSを調整してFCPとLCPを上げれば95点以上も全然いけると思った。

Partytownと直接タグ埋め込みが最強

上述の通りベータ版ということもあり?、Partytownを導入すると一部のタグでCORSエラーが出て、GTMプレビューでもCORSエラーで使えなくなったりと色々不安な面もある。
回避する手もあるが、それをするとなぜかPartytownの読み込み自体に異常に時間がかかるようになり点数が上がりきらない。
そのため、CORSエラーが出たタグは変に回避せず、GTM経由ではなく直接読み込むことで安定して高得点が取れるようになった。

TBT(Total Blocking Time)とは

そもそも今回下がっていたTBT指標とは何なのか?
Lighthouseの公式ドキュメントによると、
・サードパーティ製のスクリプトが、メインスレッドをブロックしていた時間の合計がTBT
・50ms以上かかっている処理の、50msからはみ出した部分をBlocking Timeとして計上している
・計測期間はTTI(Time To Interactive)までで、TTI後のBlocking Timeは無視される
らしい。

図にするとこうなる。1つの処理ブロックの50msからはみでた部分(茶色い部分)の合計が、TBTとして計上される。

やったこと

defer属性を付けてみる

JavaScriptはダウンロード中と実行中にHTMLのパースをブロックしてしまう問題がある。(参考
パースをブロックしてしまう問題点として、FCP,LCPが遅くなったり、コンテキストスイッチが多くなることで全体の処理時間が延びてしまうことが挙げられる。
そこでJavaScriptを読み込む際にdefer属性を付けると、実行タイミングをDOM構築後に移し、またダウンロード中のブロックを回避することができる。

ただ、これは全く効果なしだった。
GTMスクリプトのダウンロード時間を削減して実行タイミングを少し後ろにずらしただけなので、まあ妥当な結果と言える。

GTM経由ではなく直接タグを挿入

ほぼ変化なし。
GTMの読み込み時間だけ無くなり他のタグ処理時間はそのままTBTに計上されるため、効果は薄かった模様。
deferを付けても変化なし。これはGTMをdeferにしたときと同じ理由と推測できる。
(DOMに影響を与えるスクリプトを実行するケースであれば、deferにすると効果が良さそうな気がしている)

js実行時間の詳細は撮り忘れた

LCP後に発火させてみる

TBTは一旦諦め、FCPとLCPスコアを上げられないか試してみる。
GTMと関連タグの読み込み処理がLCP前に行われてレンダリングがブロックされているのなら、諸々の処理をLCP後に実行したらよいのでは?という考え。
ということで、GTMの読み込みスクリプトをLCP後に発火させるよう以下のコードを先に実行するよう調整してみた。もちろんdeferは付ける。

<script is:inline defer>
  function initGtm(w, d, s, l, i) {
    w[l] = w[l] || []
    w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' })
    var f = d.getElementsByTagName(s)[0],
      j = d.createElement(s),
      dl = l != 'dataLayer' ? '&l=' + l : ''
    j.async = true
    j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl
    f.parentNode.insertBefore(j, f)
  }
  // LCPイベントのタイミングでinitGtmを呼び出す
  if (window.PerformanceObserver) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'largest-contentful-paint') {
          initGtm(window, document, 'script', 'dataLayer', 'GTM-XXXXXXX')
          observer.disconnect()
        }
      }
    })
    observer.observe({ type: 'largest-contentful-paint', buffered: true })
  }
</script>

結果、何回計測してもほとんど効果なし。(js実行時間のスクショは他と同じなので割愛)
実際にパフォーマンスタブで見るとLCPの後に諸々の処理が移動しているのでFCP,LCPまでの時間は短くなるはずだが、なぜか上がらない。謎だ...

LCPから6秒後(TTI後)にGTMタグを発火させてみる

ちょっと変化球。とりあえずGTM関連のタグ処理を全てスコア計測外に飛ばしたらどうなるのか?と気になった。
先ほどのinitGtmの呼び出し箇所にsetTimeoutを設定してみる。

setTimeout(() => {
  initGtm(window, document, 'script', 'dataLayer', 'GTM-XXXXXXX')
  observer.disconnect()
}, 6000)

6秒後としているのは、TTIイベントの確定後に処理を開始させるため(TBTの計測はTTIまでなので)。
結果、バツグンに効果ありだった。もうタグを無くしているのとほぼ同義だしね。
6秒後に発火はするが、その前にページを離れられたり計測対象の操作をされても検知できないので、残念ながらこれは実用に値しない。

TTI(Time To Interactive)とは

TTIはなかなか特殊な指標。
ユーザーが操作可能になるまでの時間のことだが計測方法が独特で、CPUのアイドル状態が5秒以上続いた時、その開始地点をTTIだったと見なすらしい。

ここで言うアイドル状態とは、以下の状態のことを指す。(参考
・処理中のGETリクエストが2つ以下
・Long Task(50ms以上のタスク)が無い

Partytownを導入

TBT計測対象のメインスレッドからWorkerスレッドに処理を移す。
TBTに関してはこれを入れるだけでほぼ解決するはず。
と思ったら、むしろ下がった。

そもそものPartytownの読み込みにだいぶ時間が取られている模様。

ちなみに、Partytownを入れるとCORSエラーが出たので、今回はCloudFrontのビヘイビアを利用して回避した。

Partytownとは

JavaScriptをWorkerスレッド(Web Worker)で実行できるようにするライブラリ。
ブラウザは基本的にシングルスレッドで同期的に処理を行うが、実はバックグラウンドで実行できるWorkerスレッドというものがある。

ただしWorkerスレッドではDOMを参照できないため、これまではMockServiceWorkerのようにプロキシ的な役割を持たせるくらいしか活用方法が無かった。

しかしPartytownを利用すると、なんともハック的な仕組みでWorkerスレッドからでもDOMが参照できるようになるそうで、今後様々な場面で利用機会が増えるかもと思っている。(参考

Partytown + CORSエラーが出るタグは直接読み込み

CORSエラーを頑張って回避したが、そもそもCORSエラーが出るタグを直接埋め込んだらどうなんだ?となんとなく試してみたら、めちゃくちゃ上がった。
何回か計測したが点数も安定している。

Partytownの読み込み時間が短くなったことによるもので、CORSエラー回避のための設定を消したからたぶんその影響だと思うが、内部的にどんな要因で短くなったのかはわからない。
ただ点数が上がったので良しとしよう。

最後に

スコアは上がるが、Partytownは入れるか悩む

Partytownを入れることでスコア自体は上がったが、仕事で利用する場合は慎重に考えなければならない。
というのも、まだベータ版と謳っているので適用対象のスクリプトは選ぶ必要があり、実際に検証中もいくつかのリクエストがCORSエラーになりタグが無意味なものになってしまった。

これはhead内にスクリプトタグを埋め込みそこを起点としてスクリプトを取得・実行するタグの場合(GTMやGAがそう)、Partytownを利用するとfetch関数でリクエストを送る形になるため、単純リクエストでは無くなり一部CORSエラーが発生するらしい。(公式
GTMのプレビューも使えなくなってしまう。

回避策としてリバースプロキシを設置する方法があるが、構成によっては結構面倒なのでこれだけで入れるか悩むレベル。
そのため、使うなら対象のスクリプトはユーザー操作に影響しないもののみに絞り(計測系のみ?)、かつしっかり動作するか確かめる必要がある。

Partytownを入れない場合、PSIは70点が限界?

サイトの規模が大きくなると入れるタグも多くなっていくものだと思うので、Partytownを入れない場合はTBTは30点中0点になることも覚悟しなければいけない。
そのため70点が限界と言えるが、タグが多くなるとHTMLの解析時間も伸びるため、LCPとSpeedIndexにも悪影響を及ぼして全体の点数が下がりかねない。

よって実際は50〜60点くらいが限界値になるのかなという感覚で、SEOにも効果があるか怪しいことを考えると、スコアの追求はコスパが微妙だなあと思ったり思わなかったり。
やっぱりユーザーの体感パフォーマンスが一番大事かなあ。
この検証に意味を持たせるためにも、GoogleさんにはもっとSEOにおけるPSIスコアの重みを上げて欲しい。

Discussion