How Partytown's Sync Communication Works
Published: 2021-10-11 17:26:35

Recently we announced the alpha version of Partytown, which is a library that helps relocate third-party scripts into a web worker so that the main thread can be dedicated to just running _your_ code. For more information on why a website would benefit from this I’d encourage you to also read: [Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp). This post is more for the curious and talking through “how” Partytown works.

## Asynchronous postMessage()

Moving long-running and resource intensive tasks into a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) has been encouraged for many years now. However, the significant constraint is that the communication between the [main thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread), and web worker, _must_ be asynchronous. Meaning a message sent by one thread does not wait on the other thread to receive it, nor wait on it to return a value. 

At the core, [postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) is basically a fire-and-forget method. This is perfectly fine, and a communication layer can be built around `postMessage()` so your code can instead use promises, async/await or even callbacks (all of which are asynchronous). 

An awesome project that can help you easily use Web Workers is [Comlink](https://github.com/GoogleChromeLabs/comlink), which _“...removes the mental barrier of thinking about postMessage and hides the fact that you are working with workers.”_ Comlink is great, but at the time of writing this, you still hit the barrier that the calls between the main thread and web worker must be async.

## Why Third-Party Scripts Can’t Use postMessage()

As I laid out in the [first post](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp#running-thirdparty-scripts-within-a-web-worker), in reality you can’t just refactor third-party scripts. They’re hosted from another domain, controlled by another service, and built to handle countless scenarios so they can be executed by billions of different devices worldwide.

However, many scripts like Google Analytics, are just collecting information and posting that data to their servers using  [navigator.sendBeacon()](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon). This is the best case scenario because Google Analytics is really just a background task. It can happily run on its own schedule and lazily collect and post data in another thread. 

The problem, however, is there are still calls to blocking APIs that are not available in the web worker. For example `document.title` and `window.screen.width` are commonly used in scripts, but reading that information is blocking. So while Google Analytics itself is a great candidate to run in the background on another thread, it still requires synchronous communication in order to read values from `document` and `window`.

Since the third-party scripts _must_ stay as-is, and because web workers _must_ have an asynchronous communication, we’ve been in this stand-still where the bulk of our performance issues cannot be offloaded into another thread.

## With this One Weird Synchronous Trick

This is the fun stuff! 🧑‍🔬 Enter the obscure API: [Synchronous XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#synchronous_request). In today’s web development it’s a lesser known API for good reason. Basically in the olden days, when we were rocking Adobe Flash, Java applets, and Dreamweaver’ing DHTML, this was pretty common:

```js
var request = new XMLHttpRequest();
request.open('GET', '/data.xml', false);  // `false` makes the request synchronous
request.send(null);
```

The problem was that in the main thread, this blocking call would lock up the webpage until the response came back. According to [MDN](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#synchronous_request):

> Synchronous XHR is now in deprecation state. The recommendation is that developers move away from the synchronous API and instead use asynchronous requests.

And in today’s web dev landscape, it’s best to instead use the more modern [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. 

However, the _web worker’s_ ability to execute synchronously is at the core of many tools. Mainly [importScripts()](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts), but Synchronous XHR falls in this category too. These synchronous APIs have _not_ been marked as deprecated from within a _web worker_. A quick [Github search for importScripts()](https://github.com/search?q=importscripts&type=code) shows just how widely used they are. But again, it’s _only_ available in a web worker!

So, as it turns out...I guess we _can_ make the web worker blocking...🧐

## Intercepting Synchronous Requests

When code is executed from within a worker, and only a worker, we can make synchronous HTTP requests, which effectively block the web worker thread’s execution until the HTTP response comes back. With that power (and with our mad scientist wig on), we have the ability to execute the web worker code as blocking, and then use the HTTP request to asynchronously call `postMessage()`. Remember, an HTTP request and response is asynchronous. So while the web worker thread may “think” it’s sync, we can intercept the actual HTTP request and have an asynchronous response.

This is where the other weird trick comes in. _Doctors hate it!_

Service workers are able to intercept requests with [onfetch](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/onfetch). This means that the request the web worker makes can also be intercepted and handled by our own code. The requests are not external and do not hit the actual network, but instead are handled locally within the service worker. From within `onfetch`, we can then use `postMessage()` to do the real async communication. 

A service worker still doesn’t have direct access to the main thread yet. But because we’re now communicating asynchronously, from within the service worker we can then use its `postMessage()` to talk to the the main thread, and have the main thread send messages back to the service worker. Then the service worker completes the HTTP response which it already intercepted.

So we still have the same asynchronous constraint, but with the combination of synchronous XHR, and intercepting requests, we can effectively convert an async call into a blocking one. Next, we make it a bit easier to use by wrapping up all the main thread’s access with [Javascript Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).

Additionally, Partytown should still work for legacy browsers. Part of its initialization is that if the browser doesn’t support service workers, then it basically just runs the third-party scripts the traditional way (what we’re all doing today).

## What About Atomics?

Awesome, glad you asked. Personally, I’m hopeful to see [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) as the solution in the long run. Since the [awesome OSS community](https://github.com/BuilderIO/partytown) has stepped up with some great ideas, we’ve already broken ground on having two builds available: atomics and service workers. When the library runs, it’ll decide which to use depending on the browser’s support.

Currently, the plan is that the service worker trick will ultimately become the fallback, but there’s much more Atomics research to do. Good news for the future of Atomics is that Safari Tech Preview just enabled SharedArrayBuffer!

{% twitter 1443650219770617857 %} 

## What’s Next

Partytown is still in alpha and undergoing many changes on each commit 😬. But we're already actively running it on a few pages within [Builder.io](https://www.builder.io/) so we can collect more production data. 

Additionally, we’re working with a few ecommerce sites who have significant third-party script usage, and see if we can help improve their performance and usability. We’d love to have you hop in our [Discord channel](https://discord.gg/hbuEtxdEZ3) and chat ideas, or even help test and file issues in our [Github repo](https://github.com/Builderio/partytown)!

So please stay tuned as we continue this experiment. In follow up posts we’ll continue to dig deeper into other parts of the library, and as we gather more data we’re hoping to present some good hard numbers showing Partytown’s benefits.

Party on, Garth!

- [Partytown Repo](https://github.com/Builderio/partytown)
- [Discord channel](https://discord.gg/hbuEtxdEZ3)
- [Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp)
- [Qwik](https://dev.to/mhevery/series/13467)
- [Builder.io](https://www.builder.io/)