Skip to content

Bundle Splitting via Webpack in @edx/frontend-build #173

@adamstankiewicz

Description

@adamstankiewicz

Description

Context: Defining the performance KPI "Largest Contentful Paint"

An important KPI from frontend performance is the Core Web Vital called Largest Contentful Paint (LCP), which measures loading performance:

“time from when the page starts loading to when the largest text block or image element is rendered on the screen.”

The "best practice" for LCP is as follows:

  • Good. <= 2.5 s
  • Needs improvement. 2.5 s - 4 s
  • Poor. > 4s

Generally, I believe the majority of our edx.org MFEs fall in the "Poor" category for LCP.

About Webpack Production Builds

image

Webpack Bundle Analyzer

When running npm run build, a Webpack Bundle Analyzer report is generated: dist/report.html:

image

(Using standard Webpack configuration from @edx/frontend-build)

Finding performance bottlenecks for Open edX MFEs

Based on Chrome's DevTools' performance debugging tools, I've identified that one (large) bottleneck in terms of loading performance for at least a handful of Open edX MFEs is loading the JavaScript assets, and specifically large JS chunks for as vendor node_modules, from the network after the initial download of the index.html file.

The below screenshot utilizes Chrome's "Performance Insights", throttled to "Fast 3G" to simulate slower network speeds on poor network connections.

image

As shown above, the 542.[hash].js file takes over 12+ seconds to download on the simulated poor network speeds. In this time, users are shown a blank white screen with no indication of progress and is still before any network API calls have been made to any Django services.

An incremental step towards mitigate performance bottlenecks such as shown above is be to modify the webpack.prod.config.js configuration file in @edx/frontend-build to ensure Webpack is configured to appropriately split up vendor and application chunks according to more explicit rules that may improve performance and set the foundation for consuming MFEs to implement code splitting with React.lazy and Suspense.

For example, the following Webpack configuration will update the previous Webpack bundle report to contain significantly many more generated files:

image

The resulting Webpack bundle report:

image

Note that instead of all node_modules (vendor) getting consolidated into a single JS file, Webpack now splits out many more chunks when appropriate to ensure they are able to get loaded performantly by the browser.

Take for example, plotly.js. Gzipped, it's size is 1.03 MB. Currently, the MFE forces the user to download all 1.03 MB of plotly.js even if their user session never renders any component that uses plotly.js. That additional bandwidth is expensive for the user in terms of performance.

Instead, we could enable plotly.js to get split out (as seen in above screenshot) into it's own distinct bundle that may be code split by the consuming MFE utilizing React.lazy and Suspense, e.g.:

image

See this demo from React.

### Tasks
- [ ] Benchmark the performance of current Webpack production builds for a few MFEs in order to create a before/after comparison locally.
- [ ] Implement above (or similar) Webpack configuration in `@edx/frontend-build`'s `webpack.prod.config.js` file (note the given `maxSize` is based on Webpack's recommendation of max chunk size of 244 kb; seen from a console warning).
- [ ] Compare the performance of updated Webpack production builds of the same MFEs in order to see any performance improvements/regressions without any actual code splitting (only bundle splitting). My understanding is that many small files are faster to download in parallel than 1 large file serially.
- [ ] If there are no performance regressions, release Webpack configuration changes and inform the appropriate Slack channels and stakeholders about the opportunity to start using `React.lazy` and `Suspense` in Open edX MFEs.
- [ ] [consider] Is there an NRQL query to get at the average LCP metric across all MFEs? Or display a table of all MFE's LCP (and other Core Web Vitals)? Perhaps a custom New Relic dashboard for benchmarks across all MFEs? Another idea is to provide an example New Relic dashboard for a single MFE that shows their LCP metric over time, if New Relic can support that.
- [ ] [consider] Should any default bundle splitting configuration be opt-in to not change any existing MFE behavior / de-risk the work? Or should it be released a breaking change? Or do we feel it's safe enough with enough benefit that it should be enabled by default, and if MFEs want to disable its behavior, they can by overriding the default `@edx/frontend-build` config? Worth considering the different possible strategies.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRelates to new features or improvements to existing featuresepicLarge unit of work, consisting of multiple tasks

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions