



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp








生徒Webサイトが重くて、Lighthouseのスコアが上がらないんです。Google Analyticsなどのスクリプトが原因みたいで…



よーく聞くんだぞ!それならPartytownが解決してくれるぞい。サードパーティスクリプトをWeb Workerで実行して、メインスレッドへの影響を最小化する革新的な技術なんじゃ!
Partytownは、Builder.ioが開発したオープンソースライブラリで、サードパーティスクリプトをWeb Worker上で実行することでメインスレッドへの影響を最小化します。Total Blocking Time(TBT)を50〜90%削減し、Lighthouseスコアを大幅に改善できます。本記事では仕組み・導入手順・対応スクリプトを完全解説します。
Google Analytics・Google Tag Manager・Facebook Pixelなど、現代のWebサイトには多数の外部スクリプトが組み込まれています。これらはメインスレッドをブロックし、ユーザー体験を悪化させます。
JavaScriptはシングルスレッドで動作するため、サードパーティスクリプトの実行中はユーザーの操作に応答できません。
<!-- 従来の問題:メインスレッドでの実行 -->
<script src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
<!-- この間、メインスレッドはブロックされる
ユーザーのクリックやスクロールに反応できない -->
Lighthouseで測定されるTotal Blocking Time(TBT)は、メインスレッドがブロックされている時間の合計です。典型的なサードパーティスクリプトの影響は以下の通りです。
| スクリプト | メインスレッドブロック時間 |
|---|---|
| Google Analytics | 20〜50ms |
| Google Tag Manager | 50〜150ms |
| Facebook Pixel | 30〜80ms |
| 広告タグ(複数) | 100〜300ms |
| 合計 | 200〜500ms以上 |
FIDは、ユーザーが最初にページとインタラクション(クリックやタップ)してから、ブラウザが実際に応答するまでの時間です。サードパーティスクリプトがメインスレッドを占有していると、FIDが100ms以上になることもあります。
Partytownは既存コードの書き換えなしでWeb Worker上での実行を実現するために、以下の3層構造で動作します。
Web Worker内でDOMインターフェース(document・window・HTMLElementなど)を再実装し、仮想DOM環境を構築します。サードパーティスクリプトはこの仮想DOMを操作し、実際のDOMアクセスは内部で変換されます。
// Web Worker内で動作する仮想DOM
// document.body.clientWidth などのプロパティアクセスをキャッチ
const virtualDocument = {
get body() {
// プロパティアクセスをメインスレッドに問い合わせる
return proxyToMainThread('document.body');
}
};
// サードパーティスクリプトはこの仮想DOMを操作する
// 実際のDOMアクセスは内部で変換される
DOMプロパティへのアクセスを同期的なXMLHttpRequestに変換します。これにより、非同期処理を同期的に見せかけることができます。
// プロパティアクセスをURL化
// 例:document.body.clientWidth
// → /_partytown_/property?path=document.body.clientWidth
const xhr = new XMLHttpRequest();
xhr.open('GET', '/_partytown_/property?path=document.body.clientWidth', false); // false = 同期
xhr.send();
const value = JSON.parse(xhr.responseText);
return value; // 同期的に値を返す
Service WorkerがXMLHttpRequestをインターセプトし、メインスレッドと通信して実際のDOM値を取得します。
// Service Worker(sw.js)
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Partytownのリクエストを検出
if (url.pathname.startsWith('/_partytown_/')) {
event.respondWith(async () => {
// メインスレッドと通信(postMessage)
const result = await sendToMainThread({
type: 'property-get',
path: url.searchParams.get('path')
});
// 結果をレスポンスとして返す
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
});
}());
}
});
document.body.clientWidthにアクセスdocument.body.clientWidthの値を返すサードパーティスクリプトからは通常のDOM操作と同じように見えるため、コードの書き換えが不要です。
# npmの場合
npm install @builder.io/partytown
# yarnの場合
yarn add @builder.io/partytown
# pnpmの場合
pnpm add @builder.io/partytown
/** @type {import('next').NextConfig} */
const nextConfig = {
// Partytownのファイルを静的ファイルとして提供
async headers() {
return [
{
source: '/~partytown/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
module.exports = nextConfig;
# publicディレクトリにPartytownファイルをコピー
npx partytown copylib public/~partytown
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
import Script from 'next/script';
export default function Document() {
return (
<Html lang="ja">
<Head>
{/* Partytownの設定 */}
<Script
id="partytown-config"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
partytown = {
lib: "/~partytown/",
forward: ["dataLayer.push", "gtag"]
};
`,
}}
/>
{/* Google Analytics(Partytownで実行) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
type="text/partytown"
/>
<Script
id="google-analytics"
type="text/partytown"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`,
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
設定のポイント:
# インストール
npm install @builder.io/partytown
# publicディレクトリにファイルコピー
npx partytown copylib public/~partytown
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My React App</title>
<!-- Partytownの設定 -->
<script>
partytown = {
lib: "/~partytown/",
forward: ["dataLayer.push", "gtag"]
};
</script>
<!-- Google Analytics(Partytownで実行) -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>My Website</title>
<!-- Partytownのコアスクリプト -->
<script>
partytown = {
lib: "/~partytown/",
forward: ["dataLayer.push", "gtag"]
};
</script>
<script src="/~partytown/partytown.js"></script>
<!-- サードパーティスクリプト -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
</script>
</head>
<body>
<h1>Welcome to My Website</h1>
</body>
</html>
<script>
partytown = {
lib: "/~partytown/",
forward: ["dataLayer.push", "gtag"]
};
</script>
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
<script>
partytown = {
lib: "/~partytown/",
forward: ["dataLayer.push"]
};
</script>
<script type="text/partytown">
(function(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;j.type='text/partytown';
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
</script>
<script>
partytown = {
lib: "/~partytown/",
forward: ["fbq"]
};
</script>
<script type="text/partytown">
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
Lighthouse Performance Score: 65
First Contentful Paint (FCP): 1.2s
Largest Contentful Paint (LCP): 2.8s
Total Blocking Time (TBT): 450ms ← サードパーティスクリプトが影響
Cumulative Layout Shift (CLS): 0.05
Speed Index: 2.5s
サードパーティスクリプトによるメインスレッドブロック:
- Google Analytics: 45ms
- Google Tag Manager: 120ms
- Facebook Pixel: 75ms
- Hotjar: 90ms
- 広告タグ: 120ms
合計: 450ms
Lighthouse Performance Score: 92
First Contentful Paint (FCP): 1.1s
Largest Contentful Paint (LCP): 2.4s
Total Blocking Time (TBT): 50ms ← 90%削減!
Cumulative Layout Shift (CLS): 0.05
Speed Index: 2.2s
サードパーティスクリプトによるメインスレッドブロック:
- Partytown初期化: 30ms
- その他のオーバーヘッド: 20ms
合計: 50ms(450ms → 50ms、89%削減)
| 指標 | 導入前 | 導入後 | 改善率 |
|---|---|---|---|
| Performance Score | 65 | 92 | +27pt |
| TBT | 450ms | 50ms | 89%削減 |
| LCP | 2.8s | 2.4s | 14%改善 |
| FID | 100ms以上 | 10ms以下 | 90%以上改善 |
| 項目 | 内容 |
|---|---|
| メリット | メインスレッドの解放・既存コード書き換え不要・Core Web Vitals改善・ユーザー体験向上 |
| デメリット・注意点 | 通信オーバーヘッドあり・HTTPS環境必須・一部スクリプト非対応・ベータ段階のライブラリ |
TBT・FID・LCPなどの指標が改善されることで、GoogleのSEOランキングにも好影響を与えます。Googleはページエクスペリエンス(Core Web Vitals)をランキング要因の一つとしています。
<!-- フェーズ1:Google Analyticsのみ -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<!-- フェーズ2:Google Tag Managerを追加 -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtm.js?id=GTM_ID"></script>
<!-- フェーズ3:その他のスクリプトを追加 -->
<script type="text/partytown" src="..."></script>
partytown = {
lib: "/~partytown/",
forward: [
"dataLayer.push", // Google Analytics/GTM
"gtag", // Google Analytics
"fbq", // Facebook Pixel
"hj", // Hotjar
"_hsq.push" // HubSpot
]
};
# Lighthouse CLIでの計測
npx lighthouse https://example.com --view
# 重要な指標
# - Total Blocking Time(TBT)
# - First Input Delay(FID)
# - Performance Score
正しく設定した場合、計測データに影響はありません。forwardオプションにdataLayer.pushとgtagを追加することで、メインスレッドから発火したイベントがWeb Worker上のGA4にも正しく渡されます。導入後は必ずGA4のリアルタイムレポートでデータ送信を確認してください。
Service Worker非対応ブラウザ(旧式ブラウザなど)では、Partytownが自動的にフォールバック動作を行い、従来通りメインスレッドでスクリプトが実行されます。機能が失われることはありませんが、パフォーマンス改善の恩恵は受けられません。現代のブラウザはほぼすべてService Workerに対応しているため、実用上の問題はほとんどありません。
使えます。AstroはPartytown公式インテグレーション(@astrojs/partytown)を提供しており、設定が非常に簡単です。NuxtはPartytownをNuxtモジュールとして利用できます。Gatsbyも公式プラグイン(gatsby-plugin-partytown)が存在します。各フレームワークの公式ドキュメントと合わせて確認してください。
WordPressでの直接的な公式プラグインは現時点で存在しませんが、テーマのheader.phpやfunctions.phpにPartytownのスクリプトを手動で追加することで利用できます。または「Insert Headers and Footers」などのプラグインを使ってカスタムスクリプトを挿入する方法もあります。ただしWordPressはページキャッシュやCDNとの相性を事前に確認することを推奨します。
まずLighthouse CLIでTBTの内訳を確認し、どのスクリプトが残りのブロッキングを起こしているかを特定します。Partytown非対応のスクリプト(document.writeを使うものなど)が含まれている場合は、それらを別の手段(遅延ロード・async/defer属性)で対処します。またforwardオプションに漏れがある場合もスクリプトが正しく動作しないため、コンソールエラーを確認してください。
type="text/partytown"を追加するだけで導入できるPartytownは、サードパーティスクリプトによるパフォーマンス低下を解決する革新的な技術です。メインスレッドを解放することでユーザー体験が大幅に向上し、Core Web VitalsのスコアもGoogle検索のランキングに好影響を与えます。
WithCodeでは、最新のWeb制作技術を体系的に学べます。Partytownのような最先端の技術も含め、実務で即戦力となるスキルを身につけましょう。


副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
公式サイト より
今すぐ
無料カウンセリング
を予約!