Sleek and Fast: Speeding Up your Fat Web Client

Once our RUM measurements and Session-weighted p90 were established so that we would know when we were at least as good as our existing site, we were almost ready to start the hard work of becoming sleek and fast.

Knowing that RUM metrics and Session-weighted p90 were only going to be available once a daily Hadoop job completed, and that data from our beta group of internal employees was a bit noisy due to lower volume, we decided to set up some leading-indicator metrics to help us understand how we were tracking on a per-commit basis. For the leading-indicator metrics, we needed to look at things that would contribute to both the network-bound problems and the CPU/memory-bound problems. We decided on the following metrics to track on a per-commit basis:

Uncompressed CSS size for the app

Uncompressed JS size for the app

Uncompressed compiled template size for the app (which is also just JavaScript)

Number of AMD modules in the app

Number of CSS selectors in the app

CSS selector length score

CSS selector complexity score

Becoming sleek and fast

Once we had our measurements established, it was time to begin the hard work of getting our application to be faster. The work would cut across framework and application code, and had to be prioritized. If not for the tireless work of our framework engineers, as well as engineers across the entire application team, success would have been difficult to achieve.

There were two significant framework-level changes we wanted to achieve in order to make our application faster. First, we had to split the application into several mini-applications, or engines. We did the work in Ember CLI to enable lazy loading of engines and their assets. Additionally, we completed work on the Glimmer rendering engine for Ember, allowing for greatly reduced template size over the wire, and transformed the wire format to a data structure, which cut down on JavaScript parse/eval time. Lazy engines reduced asset size at load time by 50%, and Glimmer reduced uncompressed template size by 40%.

In the application, we made significant changes to the CSS layer. The CSS had not been structured before the start of the project, so CSS was located haphazardly in the directory structure, and often repeated unnecessarily. Additionally, our liberal use of Sass led to significantly more verbosity than was needed to achieve the styling of the application. We decided to employ a BEM (block element modifier) architecture for the application. We also collapsed all frequently used classes for our UI pattern library to a single file to eliminate duplication. The result was a reduction of 2mb in uncompressed CSS size. Additionally, we reduced the selector count by 10,000 and the selector complexity score by 90%.

Another significant area of opportunity in terms of run-time costs was the view layer within Ember. We found that we had a deeply-nested component hierarchy. We decided to move to a flatter compositional model instead of an inheritance model. This is frequently a performance bottleneck in client-rendered, component-based applications, so we decided to eliminate this aspect. We were also able to eliminate dead code paths, which reduced our uncompressed JavaScript and uncompressed template size.

After addressing the view layer, we tackled some issues in the data layer. We were over-modeling and doing too much work in the client due to an early decision to code-gen models/adapters/serializers based on the entire surface area of the API server represented as Rest.liPDSCs. Additionally, we had decided to persist every model instance in memory based on the data returned from the API.

In order to incrementally remove the performance penalty of these prior design decisions , we made a couple of immediate changes. First, we reduced the number of records we would fetch at one time, which not only reduced the size of data returned, but also reduced the number of model instances in the client memory in Ember Data. An example of this would be fetching data for six feed items instead of 20 at initial render time. The second change we made was to reduce the number of distinct types in the system. We found that many collection types were structured so similarly that we could collapse all the model collections to a single collection type. These changes greatly reduced the number of AMD modules, as well as the run-time costs in the application.