Packaging for Performance

An interesting topic of discussion in recent times has been around static resource (JS/CSS) packaging for web applications. Craig Silverstein’s and Rebecca Murphey’s write-ups on this topic provide great insights into the reality of packaging in today’s frontend engineering world. The main question that comes up is, should the strategy for JavaScript and CSS bundling (which is a current performance best practice) change when migrating to HTTP/2? Although in theory the benefit of bundling—which is to reduce the number of HTTP requests—becomes void with HTTP/2, the reality is that we are not there yet. The above articles are proof of that reality. At eBay we did similar research a few months back when prepping the site for HTTPS, and our findings were the same. In this post, I will outline the approach that we have taken towards packaging and the performance benefits of our approach.

The majority of eBay pages followed a naïve pattern for bundling resources. All the CSS and JavaScript for a page were bundled into one resource each, with the CSS included in the head tag and JS at the bottom. Though it was good in terms of reducing the number of HTTP requests, there were still opportunities to improve — the main one being a better use of browser caching. As users navigate through eBay pages, every unvisited page needs to download the whole JS and CSS, which includes the same core libraries (jQuery etc.) used in previous pages. With our plan towards moving to HTTPS (and HTTP/2), we knew this coarse-grained packaging would not be effective. Our study, and others’ studies, also indicated that avoiding bundling altogether and loading resources individually would still not be effective in terms of performance. We needed a balance, and that’s when we came up with our own packaging solution.

Inception

Our first step was to identify the core JS and CSS libraries used across all eBay pages and to aggregate them as one resource. To do this, we created an internal Node.js module called Inception. This module includes all the common JS and CSS modules and will be added as a dependent by each domain team (owners of the various eBay pages). The identified core JS libraries were jQuery, marko (templating engine), marko-widgets (UI component abstraction), and in-house analytics and tracking libraries. For CSS, we have our own library called Skin, from which we picked the core, button, icons, dialog, and form modules. The package bundler we use at eBay is Lasso. The Inception module, which plugs in along with Lasso, provides the following functionalities:

  • Enforces all domains (buying, selling, browse, checkout, etc.) to follow the exact version of the core JS and CSS libraries. Non-compliance will result in build failures.
  • Bundles the Inception resources as one URL with the same URL address across all domains. Examples are inception-hashcode.js and inception-hashcode.css.
  • Enables domain teams to still include any of the Inception JS/CSS libraries as a part of their own module dependencies. The Lasso optimizer will de-dupe libraries and ensure only one copy is sent to the browser. This functionality is critical for two reasons . First, we want to promote module-level encapsulation, so that when domain teams are building modules, they are free to add a dependency on a core library (say skin-button) without worrying about duplication. This also makes the module work standalone. Secondly, domain teams should not bear the overhead of tracking what is or isn’t present in Inception. They should be able to include any dependency they want, and the tooling can take care of the optimization.

Now with Inception in place, we started seeing these benefits:

  • Browser caching: One of the drawbacks mentioned earlier—bundling all resources as one URL—is poor leverage of browser caching. Inception fixes this drawback. Since the same URL is used across all domains for the core JS and CSS libraries (which BTW is the majority of the payload), browser caching is heavily utilized as users navigate through various eBay experiences. This caching is a massive improvement in terms of performance, especially for slow connections. In addition, with newer browser versions supporting code caching, we might also avoid the parse and compile times for the big Inception JS bundle.
  • Library consistency: Another problem that we saw in our previous bundling system was lack of consistency in core library versions used across domains. Since domains were maintaining the core libraries, users navigating from one domain to another might, for instance, get different versions of jQuery or button styles. The result is not only UI inconsistency but also implementation inconsistency across domains. This issue is also fixed with Inception, as it is a central place to manage core libraries.
  • Path to Progressive Web Apps: With all domain pages having the same core library dependencies, transition between them becomes easy, since only the application-specific JS and CSS has to be downloaded on each navigation. This approach will enable us to build our web apps using an Application Shell Architecture, thus paving the way to making eBay a Progressive Web App. We have already explored a similar route in the past (within a domain) using the Structured Page Fragments approach, and we have seen our perceived performance increase immensely.
  • Easy upgrade path: Finally, Inception also enables us to upgrade to newer versions of core libraries from a central place. Inception itself follows semantic versioning, so all domain teams depending on Inception will get the updates uniformly in a semantic manner. Upgrades were problematic previously, as we had to chase individual teams to do the upgrades manually.

Domains

Now with the core libraries being taken care of through Inception, what about the remaining resources in a page—i.e., application/domain-specific CSS and JS? For each domain we again came up with a packaging approach where we split the resources into two buckets: constants and variables.

  • Constants: The CSS and JS resources that remain the same on every request are bucketed as constants. These mainly pertain to the key UI components in each domain, which remain unaltered to various request parameters. The constant modules are bundled as one resource, and they again benefit from browser caching. When a user revisits a page, this bundle usually hits the browser cache and gets a performance advantage.
  • Variables: A small portion of the resources in each page vary based on request attributes. These variations might be due to experimentation, user sign-in status, business logic, etc. Such resources are bucketed as variables and have their own bundling, which happens at runtime. They will have the least cache-hit ratio and probably will need to be downloaded over the network for a new session.

Summary

To summarize, every page will have six resource bundles (3 for CSS and 3 for JS), and each bundle will have its own purpose. All URLs are hashed based on content; thus cache busting is automatically taken care of.

  1. Inception — bundles the core CSS and JS libraries. Highest in payload.
  2. Constants — bundles the unchanging application CSS and JS. Middle-range in payload.
  3. Variables — bundles the varying CSS and JS in an application. Least in payload.

In the current state, this packaging strategy seems to be the best fit for us in terms of performance. It has created the right balance between number of HTTP requests and browser caching. As we start migrating towards HTTP/2 next year, we will further evaluate this approach and try to come up with more fine-grained bundling solutions, and of course with performance being the key.