Article cover image

Adding Partytown to a SvelteKit Project

    The power of analytics

    Website analytics are a useful tool for business owners and web developers alike. The data gathered by services like Google Tag Manager and Segment can be used to tweak content and ensure it reaches the targeted audience. However, with great power comes great responsibility.

    What’s the catch?

    Ironically, 3rd party analytics scripts can have the greatest negative impact on performance out of all JavaScript served on a website. This is due to the processing power needed to download, parse, and execute these scripts. This results in reduced user retention and increased bounce rates.

    While script tag attributes like async and defer can help, they don’t guarantee improved performance, especially as the number of 3rd party scripts on a site grows.

    Partytown: a home for 3rd party scripts

    Long story short, JavaScript is CPU intensive. When used in excess, it creates a processing bottleneck. This is a common cause of website feeling 'janky' and unresponsive.

    Partytown is an innovative approach to handling 3rd party scripts that solves this problem. It runs scripts in the background using web workers, ensuring that script execution doesn’t delay the TTI.

    Recently, we added Google Tag Manager to two SvelteKit projects (including this website!) using Partytown. Let's walk through the process step-by-step, addressing some of the roadblocks encountered along the way.

    Warning: Partytown is still in beta. Some scripts may not behave correctly when used with it. We suggest opening a GitHub issue or reaching out on the Partytown Discord if you run into problems.

    Adding Partytown to SvelteKit

    Start a fresh SvelteKit project

    Bootstrap a new SvelteKit project by running:

    npm init svelte my-app

    Install Partytown

    cd my-app && pnpm add @builderio/partytown

    Add the Partytown script to src/routes/__layout.svelte

    // src/routes/__layout.svelte
    
    <script>
      import { onMount } from 'svelte'
      import { partytownSnippet } from '@builder.io/partytown/integration'
    
      // Add the Partytown script to the DOM head
      let scriptEl
      onMount(
        () => {
          if (scriptEl) {
            scriptEl.textContent = partytownSnippet()
          }
        }
      )
    </script>
    
    <svelte:head>
      <!-- Config options -->
      <script>
        // Forward the necessary functions to the web worker layer
        partytown = {
          forward: ['dataLayer.push']
        }
      </script>
    
      <!-- `partytownSnippet` is inserted here -->
      <script bind:this={scriptEl}></script>
    </svelte:head>

    Copying Partytown library files to the local filesystem

    Partytown’s internal scripts need to be served from the same origin as your site because it uses a service worker. Thankfully, the library comes with a Vite plugin that accomplishes this.

    // vite.config.js
    
    import { join } from 'path'
    import { sveltekit } from '@sveltejs/kit/vite'
    import { partytownVite } from '@builder.io/partytown/utils'
    
    /** @type {import('vite').UserConfig} */
    const config = {
      plugins: [
        sveltekit(),
        partytownVite({
          // `dest` specifies where files are copied to in production
          dest: join(process.cwd(), 'static', '~partytown')
        })
      ]
    }
    
    export default config

    On the dev server, the files are served locally. In production, they are copied to the path specified in dest.

    Proxying scripts instead

    Some 3rd party scripts run into CORS issues when making HTTP requests through a web worker. This is the case for Google Tag Manager.

    Partytown recommends reverse-proxying the requests to prevent cross-origin errors.

    At Monogram, we deploy our SvelteKit websites using Vercel. Thus, we’ll create a vercel.json file in the root directory and add the reverse proxy config. Google Tag Manager requires two scripts to be proxied because it makes a second request to fetch the Google Analytics script.

    {
      "rewrites": [
        {
          "source": "/proxytown/gtm",
          "destination": "https://www.googletagmanager.com/gtag/js"
        },
        {
          "source": "/proxytown/ga",
          "destination": "https://www.google-analytics.com/analytics.js"
        }
      ]
    }

    To complete the proxy config, add a resolveUrl function to the Partytown config in __layout.svelte:

    // src/routes/__layout.svelte
    
    <script>
      partytown = {
        forward: ['dataLayer.push'],
        resolveUrl: (url) => {
          const siteUrl = 'https://monogram.io/proxytown'
    
          if (url.hostname === 'www.googletagmanager.com') {
            const proxyUrl = new URL(`${siteUrl}/gtm`)
    
            const gtmId = new URL(url).searchParams.get('id')
            gtmId && proxyUrl.searchParams.append('id', gtmId)
    
            return proxyUrl
          } else if (url.hostname === 'www.google-analytics.com') {
            const proxyUrl = new URL(`${siteUrl}/ga`)
    
            return proxyUrl
          }
    
          return url
        }
      }
    </script>

    If you’re unable to use a reverse proxy, you can serve the scripts from the same domain as your website.

    Using 3rd party scripts with Partytown

    The moment we’ve all been waiting for!

    Add any scripts you want to be processed by Partytown to a svelte:head element. Simply place the script there and add the attribute type="text/partytown".

    Instruct SvelteKit to bypass preprocessing for Partytown scripts by adding the following to svelte.config.js:

    // svelte.config.js
    
    const config = {
      preprocess: [
        preprocess({
          preserve: ['partytown']
        })
      ],
      ...
    }

    Our svelte:head element now looks like this:

    // src/routes/__layout.svelte
    
    <svelte:head>
      <script>
        // Config options
        partytown = {
          forward: ['dataLayer.push'],
          resolveUrl: (url) => {
            const siteUrl = 'https://example.com/proxytown'
    
            if (url.hostname === 'www.googletagmanager.com') {
              const proxyUrl = new URL(`${siteUrl}/gtm`)
    
              const gtmId = new URL(url).searchParams.get('id')
              gtmId && proxyUrl.searchParams.append('id', gtmId)
    
              return proxyUrl
            } else if (
              url.hostname === 'www.google-analytics.com'
            ) {
              const proxyUrl = new URL(`${siteUrl}/ga`)
    
              return proxyUrl
            }
    
            return url
          }
        }
      </script>
      <!-- Insert `partytownSnippet` here -->
      <script bind:this={scriptEl}></script>
    
      <!-- GTM script + config -->
      <script
        type="text/partytown"
        src="https://www.googletagmanager.com/gtag/js?id=YOUR-ID-HERE"></script>
      <script type="text/partytown">
        window.dataLayer = window.dataLayer || []
    
        function gtag() {
          dataLayer.push(arguments)
        }
    
        gtag('js', new Date())
        gtag('config', 'YOUR-ID-HERE', {
          page_path: window.location.pathname
        })
      </script>
    </svelte:head>

    Events can now be sent to GTM from the browser using the typical dataLayer.push method. Since we ‘forwarded’ this function call to Partytown via the config option set earlier, we can call this function like normal.

    window.dataLayer.push({ ... })

    That's all there is to it! If you have any comments, or you found a bug/typo, please reach out in the form below!