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.
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.
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
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
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.
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);
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...🧐
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.
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).
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!
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!