Graph on a computer screen

Adding Partytown to a SvelteKit Project

Utilize the full power of your analytics suite while keeping Google Lighthouse scores in the green.

Back to blog

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 target audience.

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 I encountered along the way.

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 project by running

npm init svelte my-app

Install Partytown

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(
    () =>
      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>

Copy 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.

// svelte.config.js

import adapter from '@sveltejs/adapter-auto'
import { partytownVite } from '@builder.io/partytown/utils'
import path from 'path'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
  adapter: adapter(),

    // The Vite config is accessible inside the SvelteKit config
    vite: {
      plugins: [
        partytownVite({
          // `dest` specifies where files are copied to in production
          dest: path.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

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 using the typical dataLayer.push method, since we're ‘forwarding’ those function calls to Partytown.

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!