Arek Nawo Hobbyist. Programmer. Dreamer. Freelancer. JavaScript and TypeScript lover. 👍 World-a-better-place maker. 🌐

Using Web Workers to boost third-party script performance

6 min read 1770

Using Web Workers to boost third-party script performance

Web Workers were met with high expectations after their introduction in 2010. However, as time went on, even though developers used Web Workers for advanced libraries and use-cases (like ML training in TensorFlow.js, or language support in Monaco Editor), they became pretty much a last-resort optimization technique for everyday projects. This status quo became only more apparent as Web Assembly (WASM), and its later support for multi-threading, was introduced.

With that said, there’s a new library around, Partytown, that aims to use Web Workers to solve a common problem: moving intensive third-party scripts execution off the main thread. Let’s take a closer look.

What are third-party scripts?

Before we talk about Partytown, it’s good to understand its goal first.

Third-party scripts are a common sight across the vast majority of websites. They’re responsible for injecting analytics (Google Analytics), ads (Google Adsense), processing payments (Stripe), displaying embeds (Disqus), and more. In general, creating a modern web app is almost impossible without including at least a single third-party script in your code.

But while third-party scripts are extremely helpful, they have one enormous disadvantage: they hurt your website’s performance. With each script often being a multi-kB or even MB-sized “black-box” of code, it’s no wonder that with just a few of them, your website will scramble.

This is where Partytown comes into play.

Introducing Partytown

Partytown is a new, experimental library with a higher goal of speeding up your website by decluttering the main thread from the burden of third-party scripts. It aims to achieve this with the use of Web Workers.

On the surface, it seems like a simple idea. Just take a script and put it inside a Web Worker, right? Not really — issues start to arise when we take into account the limitations of Web Workers.

Web Workers can’t:

  • Access the DOM
  • Fully access window object’s methods and properties
  • Be directly loaded from different origins

As you can imagine, these limitations make most third-party scripts impossible to run from a Web Worker on its own — that is, without making additional adjustments.

We made a custom demo for .
No really. Click here to check it out.

To allow your third-party script to run inside a Web Worker, Partytown uses a few tricks:

  1. It disables the execution of scripts with type="text/partytown" attribute on the main thread, fetches their content, and runs them inside a Web Worker through Blobs;
  2. Inside the Web Worker, it creates dedicated Proxy objects for accessing the DOM and other globals available in the main thread. These objects handle communication with the main thread and execute actions that can’t be done from within a Web Worker;
  3. Lastly, to make communication synchronous (as the third-party scripts expect it to be), Partytown uses a combination of synchronous HTTP requests and Service Workers to intercept and, in a given time, respond to incoming requests

As you can see, there are many workarounds involved to make Partytown possible. Add to that the sandboxing iframe that wraps the Web Worker, and you can see how complex of a project Partytown really is. That’s partially why the library is experimental and might not yet work “out-of-the-box” in many cases.

With that said, there are a couple of integrations already tested by the Partytown development team, and we’ll be taking a look at one of them to see how the library works.

Getting started with Partytown test cases

Currently, Partytown comes with ready test cases for HubSpot Forms, Intercom, and Google Tag Manager (GTM), which also gets a dedicated React component.

Preparing the environment

Before you get to the code, you first need to set up your environment. First, create a new npm project and install the Partytown module:

npm init -y
npm install @builder.io/partytown

Now, that was pretty normal for any modern JavaScript library. However, the next, more unconventional step is to get the lib directory’s content from the @builder.io/partytown npm module available through the /~partytown/ route of your website. If your setup uses, e.g., public directory for that, you can do it with a simple command:

cp -a ./node_modules/@builder.io/partytown/lib/. ./public/~partytown/

To automate this process, use your bundler/build pipeline functionality.

From this point on, if you’re using React and want to integrate only Google Tag Manager (GTM), you can use the provided Partytown components — it’s quite straightforward. Here’s a sample from Partytown’s README, showing how the code would look for Next.js using its custom document feature:

import { Partytown, GoogleTagManager, GoogleTagManagerNoScript } from '@builder.io/partytown/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <GoogleTagManager containerId={'GTM-XXXXX'} />
          <Partytown />
        </Head>
        <body>
          <GoogleTagManagerNoScript containerId={'GTM-XXXXX'} />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Initializing Partytown

With module and lib’s content in place, you can now initialize Partytown. To do that, copy the content of /~partytown/partytown-snippet.js into a <script> tag in your <head>. Your code should look similar to the following:

<!-- ... -->
<head>
  <!-- ... -->
  <!-- prettier-ignore -->
  <script>!function(n,t){var r=n.partytown||{},e=r.forward||[],p=t.createElement("script");function u(t){return function(){(n._ptf=n._ptf||[]).push(t,arguments)}}e.map((t=>{n[t[0]]=1===t[1]?function(n,t){return(t=[]).push=u(n),t}(t):u(t)})),p.async=p.defer=!0,p.src=(r.lib||"/~partytown/")+(r.debug?"debug/":"")+"partytown.js",t.head.appendChild(p)}(window,document);</script>
</head>
<!-- ... -->

If you want to configure Partytown to turn on the debugging mode or customize lib’s content location, for example, you should declare a global partytown object with the necessary options.

<!-- ... -->
<head>
  <!-- ... -->
  <script>
    partytown = {
      debug: true,
    };
  </script>
  <!-- prettier-ignore -->
  <script>/* ... */</script>
</head>
<!-- ... -->

Integrating HubSpot Forms with Partytown

With Partytown correctly set up, we’ll now integrate HubSpot Forms with it.

First, prepare your HubSpot account, an example form, and its embed code. Now, just drop the HubSpot snippet into your HTML file and add the type="text/partytown" attribute to all the <script> tags.

<!-- ... -->
<script
  charset="utf-8"
  type="text/partytown"
  src="//js-eu1.hsforms.net/forms/shell.js"
></script>
<script type="text/partytown">
  hbspt.forms.create({
    region: "eu1",
    portalId: "25225394",
    formId: "cd2ac1fe-aad5-4959-a6ad-5409ba10bc44",
  });
</script>
<!-- ... -->

In the ideal scenario, you’d expect the integration to be already done and everything to work well. Sadly, due to the very experimental, almost “bleeding-edge” nature of Partytown, that’s not the case.

Even if the form appears just fine, open the dev console, and you’ll be greeted with a red wall of errors. Some could be CORS, others rendering-related.

Working around CORS and other issues

CORS errors are thrown when Partytown attempts to fetch a cross-origin resource. As the request is now made through a Web Worker that on its own comes from an iframe, it’s no wonder CORS issues can arise. That’s why Partytown utilizes a proxy service to fall back to after the request fails.

But still, the error message remains.

To make the CORS issues disappear, you can pre-download the file and serve it from your origin. You should do the same for all sub-requests of the pre-downloaded scripts as well. However, keep in mind that not all resources can be pre-downloaded, and sometimes such workarounds can break the third-party scripts because of Content Security Policy (CSP) restrictions.

Other errors aren’t that easy to fix. In the case of HubSpot Forms (which uses React for rendering), there’s no easy fix I’m aware of for an Uncaught Error: Minified exception occurred; error. Thankfully, even with this issue, the embed still works fine (submissions work), except for the message after submission — the form stays the same instead of displaying a “Thank you” message.

Results

With this integration, we can now test if Partytown works.

To the naked eye, it might actually feel slower. That’s because Partytown’s DOM operations are purposefully throttled. This might not always be desired, but it helps with limiting third-party scripts from continuously blocking the main thread.

When we take a look at the Performance tab in Chrome DevTools, we’ll see Partytown shine.

Performance comparison of Hubspot Forms using Partytown
HubSpot Forms – Partytown (left) vs. standard (right)

While differences were negligible at first, with a 6x CPU slowdown, Partytown’s advantage becomes truly visible. With over 4x less time spent on scripting and 0ms vs. 297ms blocking time over the standard method, Partytown has successfully moved the impact of the third-party script away from the main thread.

Drawbacks

So, as you can see from our walkthrough, even though it’s working, Partytown isn’t production-ready right now. While HubSpot Forms might work with only some issues, other untested third-party scripts I’ve tried, like Disqus or Tawk, didn’t work at all.

Disqus outputted TypeErrors, while Tawk returned SyntaxErrors. More interesting, however, was that the issues I faced with PartyTown changed depending on whether I pre-downloaded or formatted the file. This way, I ended up with all sorts of errors — CORS, CSP, TypeErrors, and others — but couldn’t get embeds to work.

Other notable drawbacks include:

  • event.preventDefault() not working
  • Bloated Network DevTools tab due to all the requests used for synchronous cross-thread communication

Bottom line

Partytown works. The idea behind it is excellent, and it achieves its goal. However, given that it’s a very complex library that uses many workarounds to get third-party scripts working inside Web Workers as intended, its “Experimental” status really means experimental. Partytown, while on the right track, isn’t production-ready right now.

Overall, it’s a bit concerning that such a library is necessary. As we increasingly depend on third scripts, and their performance impact only grows, the speed and efficiency of many modern websites starts to be concerning. Naturally, the best — but also most demanding — solution would be for all script vendors to better optimize their code.

Without that, Partytown and others like it seem like the only option to vastly improve the performance of many websites. And it would do so without requiring each third-party script to be optimized individually. Fingers crossed 🤞

Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

https://logrocket.com/signup/

LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — .

Arek Nawo Hobbyist. Programmer. Dreamer. Freelancer. JavaScript and TypeScript lover. 👍 World-a-better-place maker. 🌐

Leave a Reply