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. E.g. 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 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.
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.
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.
- Inception — bundles the core CSS and JS libraries. Highest in payload.
- Constants — bundles the unchanging application CSS and JS. Mediacore in payload.
- 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.