サードパーティスクリプトは1本平均で34KBのgzipペイロードと150ms以上のメインスレッドブロックを引き起こし、e-commerceではTBT悪化の最大要因です(HTTP Archive 2026年データ)。
Partytown v0.10以降はService Worker + Web Worker方式でGoogle Tag Manager・Meta Pixel・Google Analytics 4を隔離でき、メインスレッド占有をほぼゼロにできます。
YouTube・Intercom・Facebookプラグインなどは「ファサードパターン」で静的プレースホルダーを表示し、ユーザーが操作した瞬間にだけ本体をロードします。
async/defer/type=moduleの使い分けと、fetchpriority=lowによる優先度制御でネットワーク層でも遅延を稼げます。
Long Animation Frames API(LoAF)で「どのサードパーティが何ms奪ったか」を本番RUMで実測し、CIバジェットで回帰を防止します。
目次
サードパーティスクリプトはなぜ遅い?TBTとINPへの影響
現状把握:スクリプトの棚卸しとバジェット設定
async・defer・moduleの正しい使い分け
リソースヒントとfetchpriorityで接続を先回りする
ファサードパターン:YouTube・チャット・SNS埋め込み
Partytownで重いタグをWeb Workerに逃がす
Google Tag Managerを軽くする5つの実践テク
遅延実行とインタラクション連動ロード
監視と回帰防止:LoAF APIとCIバジェット
よくある質問
サードパーティスクリプトはなぜ遅い?TBTとINPへの影響
サードパーティスクリプトが体感速度を壊す理由は、ネットワーク・パース・実行の三重苦にあります。まずネットワーク層では、自ドメインとは別のオリジンにDNS解決・TLSハンドシェイク・TCP接続を張るため、コールドキャッシュだと1リクエストあたり200〜400msの追加RTTが発生します。次にJavaScriptエンジンがパースするフェーズで、圧縮解除・AST構築・バイトコード生成が走り、これがモバイル端末では50〜300msのメインスレッドブロックになります。さらに実行時にはDOM操作・イベントリスナー登録・iframe挿入・setTimeoutの多用など、そのタグが「勝手にやりたいこと」を全部メインスレッドで処理します。
結果として何が起こるか。ユーザーがタップしたのにボタンが反応しない、いわゆるINP(Interaction to Next Paint) の劣化です。Chrome UX Report 2026年Q2データでは、サードパーティスクリプトが5本以上あるページは3本以下のページと比べてINP p75が平均で230ms悪化していました(けっこう衝撃的な数字です)。私のECサイトでも、旧Universal Analyticsを外してGA4 + Partytownに移行した週にINP p75が412msから187msに落ち、コンバージョン率が0.9pt上昇しました。TBTベースで見ても、INP最適化の完全ガイド で解説した「50ms以上のロングタスク」の70%以上がサードパーティ発でした。まずは「どのタグがどれだけ奪っているか」を数字で把握することが第一歩です。
現状把握:スクリプトの棚卸しとバジェット設定
最適化の前に必ずやるべきなのがインベントリ作成です。私は以下のnode.jsスクリプトを社内のパフォーマンスCIに組み込み、PR時点で新規スクリプトが増えていないかを機械的にチェックしています。
// scripts/audit-third-party.mjs
import puppeteer from 'puppeteer';
const FIRST_PARTY = ['example.com', 'cdn.example.com'];
const BUDGET_KB = 180; // gzip合計の上限
const BUDGET_MS = 200; // メインスレッド合計の上限
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setCacheEnabled(false);
const scripts = [];
page.on('response', async (res) => {
const ct = res.headers()['content-type'] ?? '';
if (!ct.includes('javascript')) return;
const url = new URL(res.url());
const isThirdParty = !FIRST_PARTY.some((d) => url.hostname.endsWith(d));
if (!isThirdParty) return;
const buf = await res.buffer().catch(() => null);
scripts.push({
host: url.hostname,
path: url.pathname,
bytes: buf?.length ?? 0,
});
});
await page.goto('https://example.com/', { waitUntil: 'networkidle2' });
const metrics = await page.metrics();
const totalKb = scripts.reduce((n, s) => n + s.bytes, 0) / 1024;
const scriptMs = metrics.ScriptDuration * 1000;
console.table(scripts.sort((a, b) => b.bytes - a.bytes).slice(0, 10));
console.log(`合計 ${totalKb.toFixed(1)}KB / ${scriptMs.toFixed(0)}ms`);
await browser.close();
if (totalKb > BUDGET_KB || scriptMs > BUDGET_MS) {
console.error('バジェット超過。PRをブロックします。');
process.exit(1);
}
このスクリプトを走らせると、上位10ホストと合計サイズ・合計実行時間が一目でわかります。私はここに「1本あたりgzip 30KB以下」「合計180KB以下」「合計メインスレッド200ms以下」というバジェットを設定し、CIで自動チェックしています。TTFB削減とJSバンドル最適化 と同じ考え方をサードパーティに適用するイメージです。棚卸し表ができたら、次章以降のテクニックを「効果順」に当てはめていきます。
注意: タグマネージャー配下のスクリプトは「タグマネージャー本体」しか見えないことが多いため、必ずタグマネージャーを実行させた後の状態でネットワークタブをキャプチャしてください。Chrome DevToolsの「Performance insights」→「Third parties」パネルが実務では最速です。
async・defer・moduleの正しい使い分け
まずは<script>タグに付ける属性の基本を再確認しておきます。asyncはダウンロードと並行してHTMLパースを続け、ダウンロード完了時点で即実行(順序保証なし)。deferもHTMLパースと並行にダウンロードしますが、実行はDOM構築完了後で、記述順を守ります。type="module"はデフォルトでdefer相当の挙動になります。詳細はMDNのscript要素リファレンス にまとまっています。
実務での判断ルールは次の通りです。Consent Management Platform(CMP) のような「他のタグより先に必ず実行してほしいもの」はdefer。アナリティクスビーコン のように「順序不問・単独動作」ならasync。チャットウィジェット のように「そもそも初期表示に不要」なら後述のファサード。ここでよくある事故は、document.write()を含む古い広告タグをasyncにしてしまい、DOMが破壊されて真っ白画面になるパターンです。document.writeが残っているタグは、業者に代替APIを問い合わせるか、iframe sandboxで隔離してください。
<!-- 順序保証が必要:CMP、フィーチャーフラグ -->
<script defer src="https://cdn.example.com/cmp.js"></script>
<script defer src="https://cdn.example.com/flags.js"></script>
<!-- 独立・単発発火:計測ビーコン -->
<script async src="https://analytics.example.com/beacon.js"></script>
<!-- Modernモジュール:defer相当。fetchpriorityで優先度を下げる -->
<script type="module" fetchpriority="low"
src="/js/nice-to-have.js"></script>
リソースヒントとfetchpriorityで接続を先回りする
スクリプト本体をどこでロードするか決めたら、次はネットワーク層のチューニングです。<link rel="preconnect">はDNS解決+TLSハンドシェイク+TCP接続を先取りし、続くリクエストのRTTを丸ごと節約します。dns-prefetchはDNS解決だけを先行させる軽量版で、HTTP/3が主流の2026年でも「実際にリクエストが飛ぶか不確定」な低優先度ドメインに有効です。
<!-- 確実に叩くドメインはpreconnect(crossoriginを忘れずに) -->
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
<link rel="preconnect" href="https://region1.google-analytics.com" crossorigin>
<!-- 使うかもしれないドメインはdns-prefetchで十分 -->
<link rel="dns-prefetch" href="https://connect.facebook.net">
<link rel="dns-prefetch" href="https://static.hotjar.com">
2023年にすべての主要ブラウザで安定したfetchpriority属性は、サードパーティにこそ活きます。LCP画像にfetchpriority="high"を、逆にヒートマップやリコメンドウィジェットのような後回しで良いスクリプトにfetchpriority="low"を付ければ、Chromeの内部スケジューラが帯域を賢く配分してくれます。私のサイトではLCPが8%改善し、その分の帯域を分析タグが横取りしていた事実が可視化されました。優先度制御はweb.devのfetch priorityガイド が最新の実装ガイドラインとして参考になります。
ファサードパターン:YouTube・チャット・SNS埋め込み
ファサードパターンは、重い埋め込みコンポーネントの「見た目だけの静的HTML」を最初に表示し、ユーザーが実際にクリックしたときにだけ本体スクリプトをロードする手法です。Googleのlite-youtube-embedは代表例で、通常のYouTube埋め込みが約1.5MBのJSを要求するのに対し、ファサードは4KBのカスタム要素だけで済みます。私のECの商品ページではYouTube埋め込みが5〜6本混ざるレビュー動画コーナーがあり、ファサード導入前後で初期JSが3.8MB→38KBまで100倍圧縮できました。
<!-- lite-youtube-embed 使用例 -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected] /src/lite-yt-embed.css">
<script defer
src="https://cdn.jsdelivr.net/npm/[email protected] /src/lite-yt-embed.js"></script>
<lite-youtube videoid="dQw4w9WgXcQ" playlabel="レビュー動画を再生">
</lite-youtube>
同じ発想はIntercom・Zendesk Chat・Drift・LINE公式アカウントプラグイン・Facebook Like button・Twitter埋め込み・Instagram埋め込みにも適用できます。基本はサムネイル画像とロード用ボタンだけをSSRで出しておき、クリックイベントで初めて<script>を注入する構造です。以下は汎用ファサードの雛形です。
// components/ChatFacade.tsx
'use client';
import { useState } from 'react';
export function ChatFacade() {
const [loaded, setLoaded] = useState(false);
const boot = () => {
if (loaded) return;
setLoaded(true);
const s = document.createElement('script');
s.src = 'https://widget.example-chat.com/loader.js';
s.async = true;
document.body.appendChild(s);
};
return (
<button
type="button"
aria-label="チャットを開く"
className="chat-launcher"
onClick={boot}
onPointerEnter={boot} /* ホバー時に先読み */
>
💬 チャット
</button>
);
}
onPointerEnterで先読みしておくと、実際にクリックする頃には本体JSが揃っているのでUX面の遅延もほぼ感じません。Google Chromeチームが公開している埋め込みのベストプラクティス もこのパターンを推奨しています。
Partytownで重いタグをWeb Workerに逃がす
Partytownは、サードパーティスクリプトを丸ごとWeb Workerに追い出す小さなライブラリです(v0.10.3が2026年5月にリリース、Node 20対応)。仕組みはService Workerが同期XHRをインターセプトし、WorkerからのDOMアクセスをメインスレッドにブロッキングリクエストとしてプロキシする、というかなり大胆な設計。「タグは動くけどメインスレッドは奪われない」状態を作れるのが最大の価値です。
// next.config.mjs
export default {
experimental: { nextScriptWorkers: true },
};
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
{children}
{/* strategy="worker" が Partytown 経由に切り替わる */}
<Script
strategy="worker"
src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"
/>
</body>
</html>
);
}
Next.jsやAstroならstrategy="worker"という1属性で済みます。素のHTMLで使うときはpartytown-vanillaを読み込み、<script type="text/partytown">で対象タグを囲むだけです。ただし注意点があり、document.writeを使う古い広告タグや、requestAnimationFrameで重い描画をするA/BテストタグはPartytownと相性が悪いです。実運用では「GTM・GA4・Meta Pixel・Hotjarの計測部分」までを対象にし、DOM操作の激しいタグはファサードや遅延実行に回すのが安全です。導入前後の効果測定はweb-vitals.jsによるRUM実装 で必ずINPとTBTを追ってください。
補足: Partytownはユニバーサル対応ではありません。iOS Safariではnavigator.serviceWorkerの挙動差でごく稀にタグが黙って止まることがあるため、Sentry等でPartytownワーカー内エラーを別チャンネルに集めておくと運用が楽です。
Google Tag Managerを軽くする5つの実践テク
大抵のECサイトでサードパーティの元凶になっているのがGTMです。GTM本体は約80KBですが、その中で読み込む個別タグ(GA4・Meta Pixel・広告リマーケティング・A/Bテスト・ヒートマップ)が積み重なって500KB〜1.5MBに膨れ上がります。私が最も効果を実感した5つのテクは以下です。
サーバーサイドGTMへ移す :Google Tag Manager Server-Side(sGTM)でクライアントからGTMコンテナ本体を消し、Cloud Runの独自サブドメインで受ける構成に変えると、初期JSが80KB以上減ります。
コンテナを分割する :「マーケ用GTM」と「サイト機能用GTM」を分け、CMP同意前は前者を丸ごとブロック。同意率が低い地域ほど効きます。
トリガーを"ページ読込"から"インタラクション"へ :スクロール50%・カート追加・チェックアウト開始などのイベント時にだけ発火するように変更するだけでTBTが半減することがあります。
カスタムHTMLタグを禁止する :GTMのカスタムHTMLは検証も遅延もされないので、原則コード側で管理し、GTM内では変数とビルトインタグだけを使うルールに。
Partytownに乗せる :前章の通り、GTM本体をWorkerに逃がして残った時間バジェットを他機能に回します。
sGTMの設定手順はGoogle公式のServer-side tagging docs が正確です。私はCloud Run + カスタムドメインで月$40程度の運用コストになっていますが、その代わりクライアントJSの80KB削減 × 全PVでかなり返ってきています。
遅延実行とインタラクション連動ロード
初期表示に不要なスクリプトは、極端に言えば「ページを見た人の70%は使わない」ものが多いです。ヒートマップ、A/Bテスト、レビュー投稿、ソーシャルシェアボタン、リコメンドウィジェット……これらをwindow.load後のrequestIdleCallbackで流し込む、もしくは最初のユーザー操作イベント(scroll・click・mousemove・touchstart)を検知してから流すと、初期のTBT・LCPを守れます。
// lib/lazy-load-scripts.ts
type ThirdParty = { src: string; onLoad?: () => void };
const queue: ThirdParty[] = [
{ src: 'https://cdn.example.com/heatmap.js' },
{ src: 'https://cdn.example.com/recommender.js' },
];
const flush = () => {
queue.forEach((t) => {
const s = document.createElement('script');
s.src = t.src;
s.async = true;
if (t.onLoad) s.onload = t.onLoad;
document.head.appendChild(s);
});
queue.length = 0;
};
// 1) ページロード完了後のアイドルタイム
const scheduleIdle = () =>
('requestIdleCallback' in window)
? requestIdleCallback(flush, { timeout: 4000 })
: setTimeout(flush, 2000);
// 2) もしくは最初のインタラクション
const events = ['scroll', 'pointerdown', 'keydown'];
const onFirstInteract = () => {
flush();
events.forEach((e) => window.removeEventListener(e, onFirstInteract));
};
window.addEventListener('load', scheduleIdle, { once: true });
events.forEach((e) =>
window.addEventListener(e, onFirstInteract, { once: true, passive: true }),
);
このパターンで注意すべきは「イベントが来ないユーザー」の存在です。RSSクローラや検索ボットはscrollを発火させないので、setTimeout(flush, 4000)のような最終フォールバックを必ず入れておきます。逆にBotが本当に不要な計測タグなら、User Agentで発火自体を止めればさらに軽くなります。
監視と回帰防止:LoAF APIとCIバジェット
2024年に安定化したLong Animation Frames API(LoAF) は、ロングタスクよりも粒度が細かく、「そのフレームで何のスクリプトが何ms走ったか」をscriptsプロパティで返します。サードパーティの犯人特定には最適です。以下のスニペットをRUMに仕込んで、犯人ドメインをp75で集計しましょう。
// public/rum-loaf.js
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration < 50) continue;
for (const s of entry.scripts) {
navigator.sendBeacon('/rum/loaf', JSON.stringify({
url: s.sourceURL || s.invoker,
duration: Math.round(s.duration),
type: s.invokerType,
blockingDuration: Math.round(entry.blockingDuration),
}));
}
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
収集したデータをBigQueryやDatadogで「ドメイン別blockingDuration合計」でソートすると、笑ってしまうくらい正確に犯人が並びます(毎回同じ顔ぶれです)。私のECサイトの直近ダッシュボードでは、上位3ホストがconnect.facebook.net/static.hotjar.com/www.googletagmanager.comで、この3本を先述のテクニックで潰したところp75 INPが半減しました。
最後に、二度と重い実装を戻さないためのバジェットCI です。前掲のPuppeteer監査スクリプトをGitHub ActionsのPull Requestジョブに組み込み、gzip 180KB/実行200msの閾値を超えたら自動でマージブロック。マーケ担当が新しいタグを追加するときはPRを立てて、パフォーマンス予算のトレードオフを議論する運用にすると、口約束のガバナンスより遥かに効きます。
よくある質問
サードパーティスクリプトとは具体的に何を指しますか?
自ドメイン以外から配信されるJavaScriptの総称で、Google Tag Manager・Google Analytics・Meta Pixel・チャットウィジェット・A/Bテストツール・広告リマーケティング・YouTubeやSNSの埋め込み・地図SDK・決済スクリプトなどが該当します。CDNから配信される自社JSは基本的に「ファーストパーティ扱い」です。
asyncとdeferの違いは何ですか?どちらを使うべきですか?
どちらもHTMLパースを止めない点は同じですが、asyncはダウンロード完了時点で即実行・順序保証なし、deferはDOM構築完了後に記述順どおり実行します。順序依存があるならdefer、独立発火の計測ビーコンならasyncが基本です。type="module"はデフォルトでdefer相当の挙動になります。
Partytownはすべてのサードパーティタグに使えますか?
いいえ。document.writeを使う古い広告や、requestAnimationFrameで頻繁にDOM操作するA/Bテストツールは相性が悪く、動作しない・遅くなるケースがあります。実務ではGA4・GTM・Meta Pixel・Hotjarのような計測系から段階導入し、RUMでINP・TBTを比較しながら対象を拡げるのが安全です。
GTMが重いと感じたときの一番効く対策は何ですか?
効果順に「Server-side GTMへの移行」→「トリガーをページ読込からインタラクションへ変更」→「Partytownに乗せる」の3つです。特にsGTMはクライアントJSから80KB前後を丸ごと消せるため、モバイル環境では体感で分かるほど軽くなります。
ファサードパターンを導入するとSEOやトラッキングに悪影響はありませんか?
基本的にありません。ファサードは「見た目のHTMLとサムネイル画像はサーバー側で出す」ため、Googlebotはコンテンツを正しくインデックスできます。トラッキングも「ユーザーがクリック/ホバーした時点で本体をロード」する設計なので、実際の利用イベントは従来どおり計測されます。