DEV Community

Cover image for Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker
Adam Bradley
Adam Bradley

Posted on • Updated on

Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker

A fun location for your third-party scripts to hang out

Performance is always top of mind for any website or web app. It’s of no surprise that a page that loads instantly, has no scroll jank, and responds immediately to any interaction, will provide an all around better user-experience.

Even with a fast and highly tuned site following all of the best practices, it's all too common for your performance wins to be erased the moment third-party scripts are added. By third-party scripts we mean code that is embedded within your site, but not directly under your control. A few examples include: analytics, ad pixels, A/B testing, trackers, etc.

When it comes to improving site performance, resources often explain and document tangible improvements with what you can do to your code, but for the most part our hands are tied when it comes to improving third-party code.

Third-Party Script Performance Issues

The elephant in the room is that third-party scripts are often to blame for eating up a large chunk of the main thread’s precious resources. There’s a few tricks to reduce their upfront damaging effects, like waiting until after the page load to run these scripts.

But regardless, they’re still running hundreds of kilobytes (and commonly, even a few megabytes) of Javascript on your user’s main thread! And end-users’ mobile devices have less resources than the machines developers are building the sites on! This can drastically affect Lighthouse scores, Core Web Vitals, search rankings, and even increase bounce rates and reduce user-engagement due to poor user experience.

All of this has surfaced as we’ve been building out Qwik for Builder.io. The tldr is that we can make interactive sites load immediately with only HTML and CSS, and only pull in the Javascript you need on-demand. But either way, even with the fastest of the fastest frameworks (or no framework at all), third-party scripts continue to drain site performance. So we got to thinking...

Running Third-Party Scripts Within a Web Worker

Partytown's philosophy is that the main thread should be dedicated to your code, and any scripts that are not required to be in the critical path should be relocated to a web worker. Into a sandboxed location, kinda like...a little town for third-party scripts. Some sort of a...Partytown, if you will…

Web workers have been a practical solution that can off-load resource intensive tasks off of the main thread for many years now. The challenge, however, is that workers do not have direct access to main thread APIs, such as window, document, or localStorage. A messaging system can be created between the two worlds, but because postMessage is asynchronous, DOM operations that third-party scripts are packed full of simply won’t succeed with a traditional messaging system.

For example, here’s a snippet of code found in Google Tag Manager:

var w = document.body.clientWidth;
Enter fullscreen mode Exit fullscreen mode

There’s nothing special about this code, actually it’s pretty darn common. But, notice how it has to be synchronous, and there’s three blocking getters:

  1. Get document
  2. Get body
  3. Get clientWidth

If we’re unable to refactor this code to use promises or callbacks instead, then an asynchronous messaging system wouldn’t allow this to “just work.” And I want to emphasize, “unable to refactor this code.”

The same third-party scripts that are being executed by billions of devices, even as you are reading these lines, cannot just be “refactored.” In a perfect world, I’d message Google and say, “Hey, you know that analytics code that gazillions of dollars are dependent on? Please refactor it entirely. Thank you.” Next, I’d have to DM every single service in the world to refactor their code too. Wish me luck, but results may vary.

Take Me To Partytown

Partytown is a lazy loaded 6kb library that helps relocate resource intensive scripts into a web worker and off of the main thread. Its goal is to help speed up sites by dedicating the main thread to your code, and offloading third-party scripts to a web worker.

But, the most important piece it brings to the table is allowing the web worker to synchronously read from the main thread. If code running within the web worker can call blocking DOM APIs with synchronous return values, then that means we can run, unaltered, third-party scripts in a worker. The third-party code happily executes as intended, but within a different thread as to not take resources away from your code.

Sandboxing and Isolation

Third-party scripts are often a black-box with large amounts of Javascript. What's buried within the obfuscated code is difficult to tell. It's minified for good reason, but regardless it becomes very difficult to understand what third-party scripts are executing on your site and your users’ devices, and on the same thread/context as your app's code.

Partytown, on the other hand, is able to sandbox and isolate third-party scripts within a web worker and allow, or deny, access to main thread APIs. This includes cookies, localStorage, userAgent, etc. Because the code must go through Partytown’s proxy in order to access the main thread, Partytown also has the ability to log every read and write, and even restrict access to certain DOM APIs.

Essentially, Partytown lets you:

  • Isolate third-party scripts within a sandbox.
  • Configure which browser APIs specific scripts can and cannot execute.
  • Option to log API calls and arguments in order to give better insight as to what the scripts are doing.

This could be useful for many different use-cases, including:

  • Blocking access to document.cookie
  • Providing a standard navigator.userAgent
  • Not allowing scripts to write to localStorage
  • Turning document.write() into a noop function
  • Block scripts from requesting other scripts

Current Status and What’s Next

Partytown is still in alpha, it is highly experimental and not ready for production. However, we’ve been actively testing it out on a few pages within our production site on Builder.io, and so far so good. Data is being collected as expected and our analytics look unaffected. Our goal is to collect the data now, so that it can be presented in future posts.

In the next post, I’ll be focusing on how the synchronous communication channel works and some of its trade-offs.

Additionally, we’ll show how you can start testing Partytown within a React or Next.js project, or really any website or web app. Here's a quick example of how Partytown can be used within a Next.js document, but much more to come in follow up posts:

import { Partytown, GoogleTagManager } 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>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

If you’d like to learn more, or even help test, please come party with us on our Discord channel, or ping me at @adamdbradley. I’d love to ensure Partytown can work with your service or use-case, so please don’t hesitate to start a chat.

I’d also like to thank some awesome people we’ve been lucky enough to bounce ideas off of, and help validate if this could work IRL: Addy Osmani, Ilya Grigorik, Kristofer Baxter, Shubhie Panicker, Zach Leatherman, Misko Hevery, Steve Sewell and the entire Builder.io team.

Party on, Wayne!

Top comments (24)

Collapse
 
mappmechanic profile image
Rahat Khanna

This is such an intriguing, simple and yet so innovative idea. Thanks for doing this and sharing it with the community all along. I have always loved your work at Ionic and looking forward how you innovate at Builder.io

Collapse
 
vaibhavrajsingh2001 profile image
Vaibhav Raj Singh

Love this idea! It's high time we aandboxed those pesky scripts.

Collapse
 
cyrstron profile image
cyrstron • Edited

But where are the actual numbers? What performance boost did you exactly get with this approach?

Isn't DOM accessing and modifying itself is the most expensive part? Does profit of moving everything else except that to a web-worker really worth it?

Also moving data from the main thread to a web-worker and back takes resources as well (for instance, serialization/deserialization).

Collapse
 
steve8708 profile image
Steve Sewell

We've seen a single third party script for analytics drop a lighthouse score by 20 points. Analytics in particular is a high priority use case as it does not need constant read/write with the main thread (aka it's not needing to constantly serialize/dserialize), it just needs to not throw errors if it tries to run something like if (window.something) { ... }.

With projects like Qwik, Astro, Marko, React server components, everyone is trying to remove (or even eliminate) needing to run JS on the main thread to initialize a site or app, but what we immediately find is your hard work is quickly negated by the amount of 3rd party services that eat the main threads resources that are background oriented anyway (e.g. analytics). Partytown aims to solve for this use case by moving those expensive scripts to the worker and free up the main thread for just your code

Our next step is to gather production data and performance impact of running 3rd party scripts (on real websites) vs not (in partytown or eliminating entirely) to quantify the impact of these scripts more. Will share when we have the data available, so stay tuned :)

Collapse
 
cyrstron profile image
cyrstron

Sounds great!

Are there only specific use cases for this approach or it may be useful for most of the third party libs?

Thread Thread
 
steve8708 profile image
Steve Sewell

Absolutely - right now we’re mostly focused on analytics like google tag manager, google analytics, conversion tracking JS, etc.

Collapse
 
adambullmer profile image
Adam Bullmer

Love this idea! Haven't thought of a polyfill to run scripts like amplitude, branch, datadog, or segment off the main thread. Makes total sense, and would be awesome for wider support for ancillary eventing libraries to clear their queues after a user has bounced from your site.

Collapse
 
132 profile image
Yisar

github.com/yisar/fard/tree/master/...
A long time ago, I used a similar method to run fre to the worker, but it only has 1KB and does not need to simulate dom.

Collapse
 
essentialrandom profile image
Essential Randomness

Very curious to see where this goes. There's definitely some things I'd love to take off the main thread, and this seems like a good non-destructive way of achieving it.

Collapse
 
aspiiire profile image
Aspiiire

This is a fantastic awesome idea really love it! I cannot wait to implement it

Collapse
 
sirseanofloxley profile image
Sean Allin Newell

I like this A LOT A LOT!! 🎉🎊🎉💃💃🎊🎉🥳

Collapse
 
edjevw12 profile image
Edwinvw

Love it!

Collapse
 
souksyp profile image
Souk Syp.

Wow

Collapse
 
fili profile image
Fili

Moving third party scripts off the main thread to a service worker is very progressive! Love the concept.

Collapse
 
soniar4i profile image
Sonia C.

Hey Adam!

Great article I was wondering if this will allow script tags to be easily added or removed from dom tree. I’m currently working with an SPA and handling those third party ser a nightmare since you can’t control what gets injected.

Thanks!

Collapse
 
camaross profile image
Denis

I wonder how much better is this compared to delayed script injection? What I did before for the same purpose was pushing those 3rd party into the DOM, say, 1 sec after window's "load" event. That way, I saw no negative performance impact and the only downside was that a hit would not be captured by analytics if the user leaves the page too soon. Nevertheless, great technique!

Collapse
 
icantunderstand profile image
icantunderstand

great idea,hopelly it will soon work on production

Collapse
 
niyazafazl profile image
niyazafazl

Partytown can use with the other third party libraries like moment, single-spa for React.
How can we set the forward config for those libraries? Can please share the sample code for that?

The below is the Partytown forward config for GTM, I want to set the same for toehr libraries.

import { Partytown } from '@builder.io/partytown/react';

export function Head() {
return (
<>

</>
);
}