Astro กับ Partytown: Third-party Scripts ใน Web Worker
#astro13 เม.ย. 2569
Astro กับ Partytown: Third-party Scripts ใน Web Worker
Partytown เป็น library ที่ช่วยย้าย third-party scripts เช่น Google Analytics, Facebook Pixel, และ chat widgets ไปรันใน Web Worker แทนที่จะรันบน main thread ทำให้ page performance ดีขึ้นอย่างมาก
ปัญหาของ Third-party Scripts
Third-party scripts มักเป็นสาเหตุหลักของ performance issues:
- Main Thread Blocking: scripts รันบน main thread ทำให้ UI ค้าง
- Long Tasks: scripts ขนาดใหญ่สร้าง long tasks
- Poor Core Web Vitals: ส่งผลต่อ LCP, FID, CLS
- Uncontrolled Loading: ไม่สามารถควบคุม timing ได้
ทำไม Partytown ถึงช่วยได้?
Partytown ย้าย scripts ไปรันใน Web Worker:
- Off Main Thread: scripts ไม่บล็อก UI
- Transparent: scripts ทำงานเหมือนเดิม
- Lazy Loading: โหลดเมื่อจำเป็น
- Proxied DOM Access: Web Worker เข้าถึง DOM ผ่าน proxy
การติดตั้ง
npx astro add partytown
หรือติดตั้งแบบ manual:
npm install @astrojs/partytown
แก้ไข astro.config.mjs:
import { defineConfig } from 'astro/config';
import partytown from '@astrojs/partytown';
export default defineConfig({
integrations: [
partytown({
config: {
forward: ['dataLayer.push', 'fbq', 'gtag'],
debug: process.env.NODE_ENV === 'development',
},
}),
],
});
Google Analytics 4
---
// src/layouts/BaseLayout.astro
const GA_ID = import.meta.env.PUBLIC_GA_ID;
---
<html>
<head>
<!-- Google tag (gtag.js) with Partytown -->
{GA_ID && (
<>
<script
type="text/partytown"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
/>
<script type="text/partytown" define:vars={{ GA_ID }}>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', GA_ID, {
page_path: window.location.pathname,
});
</script>
</>
)}
</head>
<body>
<slot />
</body>
</html>
Google Tag Manager
---
const GTM_ID = import.meta.env.PUBLIC_GTM_ID;
---
<html>
<head>
{GTM_ID && (
<script type="text/partytown" define:vars={{ GTM_ID }}>
(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;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer', GTM_ID);
</script>
)}
</head>
<body>
{GTM_ID && (
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style="display:none;visibility:hidden"
/>
</noscript>
)}
<slot />
</body>
</html>
Facebook Pixel
---
const FB_PIXEL_ID = import.meta.env.PUBLIC_FB_PIXEL_ID;
---
{FB_PIXEL_ID && (
<script type="text/partytown" define:vars={{ FB_PIXEL_ID }}>
!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=b.getElementsByTagName(e)[0];
t.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', FB_PIXEL_ID);
fbq('track', 'PageView');
</script>
)}
Hotjar
---
const HOTJAR_ID = import.meta.env.PUBLIC_HOTJAR_ID;
---
{HOTJAR_ID && (
<script type="text/partytown" define:vars={{ HOTJAR_ID }}>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid: HOTJAR_ID, hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
)}
Custom Event Tracking
// src/utils/analytics.ts
export function trackEvent(
eventName: string,
params?: Record<string, unknown>
) {
// Google Analytics
if (typeof window !== 'undefined' && 'gtag' in window) {
(window as any).gtag('event', eventName, params);
}
// Facebook Pixel
if (typeof window !== 'undefined' && 'fbq' in window) {
(window as any).fbq('track', eventName, params);
}
}
export function trackPageView(path: string) {
if (typeof window !== 'undefined' && 'gtag' in window) {
(window as any).gtag('config', import.meta.env.PUBLIC_GA_ID, {
page_path: path,
});
}
}
export function trackPurchase(data: {
transactionId: string;
value: number;
currency: string;
items: Array<{ id: string; name: string; price: number; quantity: number }>;
}) {
trackEvent('purchase', {
transaction_id: data.transactionId,
value: data.value,
currency: data.currency,
items: data.items,
});
}
Partytown Configuration สำหรับ Cloudflare
// astro.config.mjs
import partytown from '@astrojs/partytown';
export default defineConfig({
integrations: [
partytown({
config: {
forward: ['dataLayer.push'],
resolveUrl(url) {
// Proxy URLs ผ่าน Cloudflare Worker
const proxyUrl = new URL('https://your-proxy.workers.dev');
proxyUrl.searchParams.set('url', url.href);
return proxyUrl;
},
},
}),
],
});
Performance Comparison
ก่อนใช้ Partytown:
- Total Blocking Time: ~800ms
- Time to Interactive: ~4.5s
- Main thread work: ~3.2s
หลังใช้ Partytown:
- Total Blocking Time: ~120ms
- Time to Interactive: ~2.1s
- Main thread work: ~0.8s
สรุป
Partytown กับ Astro เป็นการผสมผสานที่ยอดเยี่ยมสำหรับการจัดการ third-party scripts ด้วยการย้าย scripts ไปรันใน Web Worker ทำให้ main thread ว่างสำหรับ user interactions ส่งผลให้ Core Web Vitals ดีขึ้นอย่างมีนัยสำคัญ