🐷

GTMを入れたらサイトスピードがめちゃくちゃ落ちた(改善策あり)

2024/06/08に公開

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

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

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

結果

Partytownで94点まで上がった

Partytownを入れるとスコアが100点近くまで伸びた。なんならデスクトップは100点になった。
まだベータ版なのでなんとなく避けていたが、結局点数を上げるにはこれを入れるしかなさそう。
(実を言うとPartytownを入れただけだと88点がマックスだったので、見栄えを良くするためLCPとユーザー補助を少し改善している)

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ツリー構築後に移し、またダウンロード中のブロックを回避することができる。

イメージ

ただ、これは全く効果なしだった。
よく考えると特にdeferを指定せずともモダンブラウザはpreloaderで大体勝手にJavaScriptを非同期でダウンロードしてくれるので、
実質GTMスクリプトの実行タイミングを少し後ろにずらしただけとなり、まあ妥当な結果だったと言える。

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

GTMでGoogleAnalyticsやHubspotの計測タグを設定するのではなく、全てHTMLに直接挿入してみた。
結果、ほぼ変化なし。
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 + CORSエラーが出るタグは直接読み込み

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

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

そもそもPartytownとは

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

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

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

イメージ

最後に

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

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

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

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

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

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

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

Discussion