Performance

What is the performance impact on learners and operators of enabling frontend composability in these areas?

  • Shared dependencies

  • Lazy loading of MFEs

This will not be an easy ‘apples to apples' comparison for a variety of reasons.

Caveats and Notes

Module Federation and Treeshaking

Module federation is not inherently compatible with treeshaking, particularly when loading modules dynamically at runtime. This means that while an individual MFE can use treeshaking to remove dependencies on any Paragon components it doesn’t use, for instance, the ‘shell’ application needs to assume they’ll all be used and bundle the entire library. This will increase how much paragon contributes to initial bundle size when compared with any MFE today, with benefits increasing as a user navigates from MFE to MFE.

Full Page Refresh vs. Progressive Loading

Adopting a frontend composability approach will result in fewer full page refreshes and much more progressive loading of the Open edX frontend. While we can use Lighthouse to measure core web vitals for full page refreshes easily, it’s not as helpful for measuring progressive loading of content once the page has loaded. Since we’re shifting to the latter, it’s difficult to engineer a straight comparison via Lighthouse.

Regarding Bundle Splitting

Bundle splitting (within an MFE) is something we can do today - it has nothing to do with adopting a frontend composability approach. It can be accomplished by using React.lazy or import(), and some MFEs are making (limited) use of it today. Regardless of our frontend composability approach, we should invest more regularly in using bundle splitting. For this reason, I’m not including it in this performance test. Please see the Bundle Splitting page for guidance on when to use bundle splitting.

Hypothesis

Learners

For learners, we expect that we should see improvements in time to Largest Contentful Paint (or progressive loading equivalent) based on:

  • Sending fewer bytes to the client via shared libraries.

  • Fewer full page refreshes via lazy loading MFEs.

  • Deferring parts of the UI until later via bundle splitting.

Operators

For operators, we expect that we should see improvements in build time based on:

  • Fewer dependencies in non-shell MFEs, resulting in smaller bundle sizes.

  • Negligible difference in build time from the added complexity incurred by webpack of managing shared dependencies.

Method

Setup

We’ll test this hypothesis by creating a few Open edX micro-frontend applications:

  • A shell application which will provide shared dependencies and shared UI elements (like the header and footer)

  • A guest application which will be lazy loaded into the shell at runtime, using the shell to supply those shared dependencies.

Production bundles from the two applications will be loaded as static assets onto two different express servers (using fedx-scripts serve) to simulate a production-like environment.

We’ll test in Chrome with “Disable cache” turned on to understanding the effect of the shared libraries and different bundling approach, and we’ll test with the cache turned on to get a sense of the experience of repeat visitors.

We’ll use throttling to simulate real world conditions, and will run the test multiple times to weed out any outliers/create an average set of metrics.

Measurements

Bundle Size

Measurement will be done by observing the overall download size and extrapolating the overall impact of shared dependencies on a larger system with a dozen or so MFEs. This is dependent on the cache setting, but not on throttling or even testing multiple times.

Build Time

We’ll compare build time between the domain MFEs with and without their shared dependencies. We’ll run this test multiple times to produce an average build time.

“Largest Contentful Paint”

For simple MFEs, we’ll consider the full page load to be the “largest contentful paint” and use the network tab to understand how quickly that happens with various levels of throttling. This will let us understand overall load time regardless of whether we’re using a full page load (current MFEs) or progressive loading (composed MFEs) of content. Loading a domain MFE into the shell after the shell has loaded is considered ‘equivalent’ to loading a current MFE via a full page load for all but the first MFE loaded.

To compare the load time of the guest MFE with that of traditional MFEs, we’ll measure: 1) the time it takes for the host/guest to load vs 2) the combined time of loading the host MFE as a “traditional” MFE, plus the time it takes the guest to load as a “traditional” MFE, simulating a user clicking between the two experiences and getting a full page refresh on the guest MFE.