Shared Dependencies

This is the results of analyzing the dependencies in the 22 micro-frontends in the Open edX GitHub organization.

Approach

  1. Opening the package.json of each of the 22 repos.

  2. Copying out the dependencies and dev dependencies into one big text file.

  3. Sort the lines alphabetically.

    1. Adjusted for any “@edx” to “@openedx” inconsistencies as we migrate orgs.

  4. Scan the list (800 lines or so) for dependencies used by all or nearly all of the MFEs.

  5. Write them down.

  6. Augment that raw data with domain knowledge about:

    1. Packages that contain/expose others (@edx/frontend-platform provides history, react-intl, @reduxjs/toolkit exposes both redux and reselect, paragon has icons now, etc.)

    2. Packages we want to move away from (i.e., please no more redux-saga, @fortawesome/*, etc.)

    3. Packages we want to move toward (i.e., all MFEs should have @edx/frontend-component-header and @edx/frontend-component-footer dependencies even if they don't for some reason)

  7. Use bundlephobia.com to get bundle sizes for the dependencies to add more context.

Findings

Common Dependencies

The following packages are used by all or nearly all Open edX micro-frontends. This is only including dependencies - the assumption is that devDependencies are likely necessary for an MFE to build itself, and therefore less interesting for this exercise since they can’t be meaningfully ‘combined’ in most cases. If they were, they’re generally already part of @edx/frontend-build.

Package

Size (un-gzipped)

Notes

Package

Size (un-gzipped)

Notes

@edx/brand

Variable

 

@edx/frontend-component-footer

88.1k

 

@edx/frontend-component-header

156.9k

 

@edx/frontend-platform

355.3k

 

@openedx/paragon (or @edx/paragon)

Big

bundlephobia.com choked on the paragon build, but we expect it’s the largest thing here. That said, today, Paragon components are dropped during treeshaking

classnames

0.8k

 

core-js

241.1k

 

prop-types

0.9k

 

react

6.4k

 

react-dom

130.2k

 

react-redux

11.2k

 

react-router

58.9k

 

react-router-dom

77.1k

 

redux

3.7k

 

regenerator-runtime

6.6k

 

Total: 892.2k

Recommendation

All of the above packages should be shared by the shell application.

Third-Party Dependencies

The third party dependencies in particular - i.e., the packages that aren’t part of @edx or @openedx, are all used by all or nearly all MFEs in existence, and are nearly all required for all MFEs to function.

Open edX Dependencies

The Open edX packages (brand, frontend-component-footer, frontend-component-header, frontend-platform, and paragon) are a bit more interesting, each for their own reason, mostly around customization.

@edx/brand

It’s worth noting that this is a stand-in for a real brand package, which will be operator-specific. It should obviously be shared.

@edx/frontend-component-footer

Similar to the brand, we override this with an npm alias today so that it can be customized by operators. This build-time approach could be replaced by dynamically loading the footer from a customized MFE.

@edx/frontend-component-header

Similar to the brand and footer, we override this with an npm alias today so that it can be customized by operators. This build-time approach could be replaced by dynamically loading the header from a customized MFE.

Further, this repository contains a variety of headers for different use cases. Normally we should be able to use treeshaking to remove the headers a particular MFE doesn’t need - that won’t work with the shell application, which needs to have them all available in case an MFE needs one, which is something it can’t predict at build time. This means we’re increasing our initial load time because of all those extra bytes unless we dynamically load individual headers as needed, including the first one.

@edx/frontend-platform

Sharing frontend-platform as-is means that domain MFEs (i.e., not the shell) will receive the build-time configuration from the shell, not from their own build. This is problematic, but not insurmountable. It means, for instance, that an MFE trying to access its BASE_URL would get the BASE_URL of the shell instead of itself. Or if I had an MFE-specific environment variable, it wouldn’t be present in config.

The tentative proposal would be to split off a chunk of frontend-platform that is not shared so that domain MFEs can continue to access their own build-time configuration. This will require a bit of work to arrange.

Finally, adding frontend composability is exciting from a non-UI perspective of potentially allowing operators to specify analytics and logging service implementations at runtime. Today this is not something that can be overridden, though we’re close to making it possible at build-time via JS file config.

@openedx/paragon

Today, individual MFEs benefit from treeshaking which occurs in our webpack build, removing any Paragon modules/components which aren’t used by the MFE, significantly reducing Paragon’s contribution to overall bundle size.

Module federation shared dependencies are incompatible with treeshaking. This means that if the shell is going to share Paragon with domain MFEs, it needs to bundle the whole thing since it can’t predict what will be used. It’s possible that we can find a way of doing some additional bundle splitting/lazy loading with Paragon components, but it’s not obvious what that’d look like, and it’s definitely additional work/complexity.

Paragon is likely one of our largest runtime dependencies, so this adds a not-insignificant amount of overhead to loading the shell the first time (assuming browser caching helps us out later). Assuming everything in Paragon is actively in use by at least one of our core MFEs, the expectation is that this will be worth it overall, but it’s a big contributor to why our Performance evaluation will be an “apples to oranges” comparison. It may also be a good reason to consider packaging Paragon components individually, similar to what lodash does.