Bundle splitting is something we can do in MFEs today using import()
, and React.lazy
.
That said, we rarely use it, mostly for lack of trying. The learning MFE makes a token effort at using React.lazy
to defer loading of some alerts and dialogs that are intermittently shown in less common situations. It could instead, for instance, lazily load the in-course experience, or each tab on course home. Doing this would make a big dent in our bundle size at any given time. To my knowledge those are all part of the same big bundle today (I’d be pleasantly surprised if they weren’t).
import()
The import()
function allows us to dynamically load files, returning a promise that resolves once the browser has loaded the module. Webpack handles setting up these separate bundles for us automatically whenever it encounters an import()
function in the code.
React.lazy
This is a sub-set of use cases for import()
, as React.lazy
relies on import()
to do the actual loading and underlying bundle splitting (via webpack).
Best Practices
Every time we use bundle splitting, we’re deferring some of our initial load time by having the user download that chunk of code later under the assumption that they don’t need it up front. This means we’re trading part of our initial page load delay for other, smaller asynchronous code loading delays later.
Each of these delays incurs a penalty in multiple ways. For developers, they need to work to prevent page layout jank, where the page “moves around” as dynamic content loads in, handle loading errors, create fallback content, etc. The added complexity can occasionally lead to regressions or issues, which are inherently more difficult to diagnose and fix when asynchronously loaded code is involved. Users, meanwhile, will see loading spinners, skeletons, and perceptible delays as they navigate around.
This means we should be intentional about using bundle splitting in impactful places - not just everywhere we can.
User Journey
From a user experience perspective, using bundle splitting and lazy loading where there are natural breaks in the user journey within an MFE’s domain feels natural. Using the learning MFE as an example again, the Course Home page and the Courseware page are two naturally separate parts of the user journey, and a user may deep link into the MFE into either of the two and spend time there before crossing the threshold to the other. Likewise, to a lesser extent, each tab on course home could arguably be its own bundle - each of these would likely result in a hefty amount of code deferred until its needed by the user.
Code Fracture Planes
Finally, splitting bundles around fracture planes in the code is a natural choice. Think of these as code sub-domains, which may feel like they correlate strongly to the user journey. The Course Home and Courseware pages, for instance, are largely different codebases in the same MFE, and correlate strongly to the user journey.
Less obvious, perhaps, might be splitting around usage of the special-exams library, which is used within Courseware… depending on how big it is.
Chunk Size Threshold
A chunk should be of sufficient size to make splitting it worth the trouble. The learning MFE, for instance, splits some infrequently used alerts and dialogs out into their own chunks, each of which is arguably tiny. While this is probably fine from a user experience perspective, it’s barely worth the effort, and has definitely contributed to difficult debugging in the past.
Deferring a chunk that shaves off 100ms of loading time may make a perceptible change in user experience - deferring a chunk that shaves off 10ms is probably a waste of time.