DEV Community

Cover image for How Partytown's Sync Communication Works
Adam Bradley
Adam Bradley

Posted on

How Partytown's Sync Communication Works

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. 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 has been encouraged for many years now. However, the significant constraint is that the communication between the 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() 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, 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, 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(). 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. 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:

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

The problem was that in the main thread, this blocking call would lock up the webpage until the response came back. According to MDN:

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() API.

However, the web worker’s ability to execute synchronously is at the core of many tools. Mainly 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() 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. 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.

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 as the solution in the long run. Since the awesome OSS community 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!

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 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 and chat ideas, or even help test and file issues in our Github repo!

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!

Top comments (2)

Collapse
 
tetsuobodyhammer profile image
tetsuobodyhammer • Edited

Hi Adam,

Not sure if these posts are something you follow any longer, but for what it's worth i just spent some time building your library into a very simple bootstrap 4 frameworkless website which has been suffering the impact of 3rd party scripts for some years now and it returned an instant 28% improvement on Performance metric in PageSpeed Insights.

Can't thank you enough.

Collapse
 
adardesign profile image
Eliazer Braun • Edited

Great article!
Thanks!
When do you expect this to become useable in production ?