openedx / wg-frontend

Open edX Frontend Working Group
4 stars 0 forks source link

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

Open adamstankiewicz opened 1 year ago

adamstankiewicz commented 1 year ago

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:

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.
Syed-Ali-Abbas-Zaidi commented 1 year ago

Hi @adamstankiewicz, We did some RnD (on Account and Discussions MFE) on our end and the following are our findings:

It seems like the bundle splitting didn't affect load time but the total build time got improved.

NOTE: We used the above given webpack configuration

adamstankiewicz commented 1 year ago

@Syed-Ali-Abbas-Zaidi Thanks for the update!

Interesting about the improvement to build time. Regarding load time, I wouldn't expect too much of a change just with the above given Webpack configuration since the browser is still downloading all the same code; just a higher quantity of small files versus 1 larger file. That said, if/when MFEs start code splitting through dynamic imports and/or React.lazy and Suspense, these smaller, independent chunks could be dynamically loaded as the user interacts with the application.

A few follow-up questions/comments:

  1. [clarification] What methodology did you use to benchmark performance locally? For example, were you running Webpack Dev Server or serving the already-compiled MFE production bundle (e.g., through npm run serve, see docs)? I've noticed some discrepancies when doing performance benchmarks locally utilizing Webpack Dev Server vs. the production build directly.
  2. If a consumer wants to introduce code splitting today, can they do so without modifying the Webpack configuration? That is, are the proposed Webpack config changes a prerequisite for code splitting in a consuming MFE? For example, given the default Webpack configuration generates a single chunk for node_modules today and the consuming MFE opted to code split an import of one of these node_modules through a dynamic import and/or React.lazy and Suspense, will Webpack already support extracting that package out as its own chunk? Or do we need to introduce Webpack configuration changes as shown above to split out chunks before consumers can implement code splitting?
  3. [curious] Was there any exploration in what other permutations/options of generic Webpack configurations might be worth trying in support of bundle splitting for consumers?
  4. [suggestion] We might consider experimenting with a CI check for bundle size in our frontend repos (e.g., bundlewatch, bundlesize) CI check in GitHub to have better observability into ongoing changes to bundle size at the PR level. Example:

image

adamstankiewicz commented 1 year ago

[suggestion] We might consider experimenting with a CI check for bundle size in our frontend repos (e.g., bundlewatch, bundlesize) CI check in GitHub to have better observability into ongoing changes to bundle size at the PR level.

[inform] Filed this GitHub issue related to bundlewatch/bundlesize: https://github.com/openedx/axim-engineering/issues/837

Syed-Ali-Abbas-Zaidi commented 1 year ago
  1. We used npm run serve to benchmark the performance locally. (It works like a charm :) )
  2. Yes, the Consumer can introduce code-splitting without modifying the webpack config. Discussion MFE is already using Suspense and React.lazy. (Here is one of the example)
  3. Yes, I explored some other permutations too, but they were not that effective. I am currently working on this and will share it here If I find something interesting.
  4. Sure, We will explore this.