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 ดีขึ้นอย่างมีนัยสำคัญ