Dynamic Bundling-Trials in performance

Itai Chejanovsky
Wix Engineering
Published in
7 min readDec 23, 2021

A lot of times we discuss website performance and enhancements in the scope of what your code should and shouldn’t do and a lot of best practices. The reality of making a website performant is that sometimes, it is accomplished by trial and error.

So this story is about one such case of improving performance by trying things. But first let’s explain the context of our system, just a little bit.

Applications

The Wix business model is built around what we call “companies”.
Each company is responsible for a specific domain in the business (or technical) world. Each company has it’s own management, developers, product, UX, well you get it-it’s like a company within a company. Some examples of companies are: Ecommerce, Chat, Bookings, Identity and quite a few others.

Each company has a representation of one or more applications.
In our world, the technical representation of an application is a collection of:

  • Microservices (servers)
  • Static Artifacts
  • Configurations

Each microservice and static artifact is deployed on it’s own according to the team’s velocity and features being delivered. So for example, if the E-commerce team delivers a new feature in their Product widget, they can deploy only that client artifact to production.

OK, so what does all this have to do with the title?!??!?!

Hang on, we’re almost there..

Host Applications

So we have different applications being built and deployed independently, but where do they actually run?
Most apps (not all, we have what we call “standalone” apps too) are run by what we call host applications.
Host applications are applications that load their own code and run, then they load and run other applications front end code.

Let’s look at one example: our dashboard application (called “business manager”).

This is the flow that covers what happens until the user can fully interact with the page. It starts with a user loading the site in a browser.
1. The browser calls the Business Manager server (our host application server)
2. That server calls other servers to get the configurations via RPC (for simplicity I called them “Integration”)
3. They all get their data from their data stores
4. The business manager HTML returns from the server, it loads the JS client code
5. The client code loads all the installed application’s client code and runs them

This is obviously a simplified flow so we can discuss the use case :) .

Where our problem begins

So in step 5 of the previous diagram, we loaded the code of all the applications that we plan to run. That may be 30 different code packages.
So we make 30 requests from the client to get the Javascript packages and run them.

The question we came across is, what’s actually better for performance?

  1. Many requests for small client code bundles
  2. A few requests for medium sized bundles?
  3. One request for all the client code bundles?

Our default model was the first one:
Load a lot of rather small bundles (we usually try to cap them at around 20kb gzipped). It was trivial for us to achieve since our artifacts were built and deployed separately by many teams that run independently.

But we had an icky feeling inside that told us that this pattern may not be great for the end user experience.

We built a nice server to try and see what works better: A Dynamic JavasScript Bundler.

Dynamic Script Bundling

When we talk about bundling, most times the assumption we have is that it happens in build time. There are quite a few amazing tools for bundling efficiently, tree shaking unused code and optimizing bundles in the world and we use them too. We mostly rely on webpack for our code bundling.

The problem is that these bundles are static since we built them in different repos and they result in different artifacts. So we don’t have a single webpack bundle that actually contains all of our code.

A second problem is that different sites need different bundles, based on what applications our users installed. So even if we had a monorepo of all the apps, building all the permutations for testing seems like a very complex task.

Enter dynamic script bundling.
We built a pretty simple server that gets a request for specific bundles, joins them and sends back a new Javascript bundle.

Let’s look at the API example:

The bundles query parameter is a comma separated list of client artifacts we need to load. The server fetches files from our CDN and bundles them together.
Any file that was loaded in the server is cached in Redis and in memory with limited TTLs, so that new requests for the same bundle are more performant.

API Request Flow
The API Request Flow

All successful responses, meaning those that managed to load all the files requested, are cached in our CDN tier. That way, any following request to these files does not actually reach the server, but gets a response from our CDN tier.

The bundler supports three different methods of getting the data:

  1. As a JSON object of artifact URL to string Javascript
  2. As a concatenated Javascript file of all the requested bundles (with a .js suffix)
  3. As a JSONP (essentially a Javascript file) call that invokes a globally available function and passes it an artifact URL key to wrapped anonymous function bundle (with a .jsonp suffix)

From my previous work in Liveperson, my assumption was that either JSON or JSONP would be the favorite use cases, but I was wrong ¯\_(ヅ)_/¯.

The first use case was the business manager, it started loading the application bundles concatenated.

So what did we learn?
Lowering the number of bundle requests to fewer slightly larger bundles actually improves load performance by 5–9%.
Quite a nice gain from a fairly simple system!

Ehm, I can’t DEBUG my code in production…

So yes, we gained performance but then DEV complaints started pouring in about the code no longer being debug friendly in production.

So it turned out one of the things we lost along the way was SourceMaps.

SourceMaps are the standard practice for debugging client code in production. Each bundle contained a reference to the sourceMap file hosted on the CDN. If anyone wants to debug something, the browser would load the sourceMap and allow them to debug the original code they wrote and not some minified, obfuscated dynamic bundle.

So we set off to solve this as well. It seemed the SourceMap revision 3 spec had this use case in mind. I was so happy!
All you needed was adding a URL (see below on line 5) to each section to make the specific source map load for this section! Easy!

Configuration structure for sourceMap post processing
SourceMap Post Processing Spec

So we cleaned up the previous source map annotations and added a new one on the generated bundles.

Then we tried it out and Chrome developer console had this to say about it:

“SourceMap “/api/v1/cache.js.map?bundles" contains unsupported “URL” field in one of its sections.”

So after searching around a bit we found this reference that explains this feature hasn’t actually been implemented in Chrome.

We went back to the drawing board and decided we can reuse the same concepts we did in the bundling of Javascript: we made it a dynamic sourceMap bundle request.

We added a new api call that gets a sourceMap request and builds a concatenated source map based on the bundle that was built.
At the end of the dynamic bundle file we added a single sourceMap URL which is the actual API call.

Like this:
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map

In order to do this we added info where each bundle begins and what source maps it uses to the request, then we bundled those SourceMaps and sent them back according to the spec (same as you see in line 6 in the above sample).

Summation

So after a short detour you now know how we tried squeezing more performance out of our system while not changing the way our DEVs work (for now). For me, as an attempt at (almost) free performance gains, it was a cool learning experience.

If you’ve made it this far, I hope you got something from our experience too.

Cheers,

IC

--

--

Itai Chejanovsky
Wix Engineering

I love planning and executing systems. I'm an architect at heart. SAAS has been my friend for the past 12 years.