docs/JavaScript-Bundling-Strategy.mdx

Summary

Maintainability
Test Coverage
import { Meta } from '@storybook/blocks';

<Meta title="docs/JavaScript-Bundling-Strategy" />

# JavaScript Bundling

## Differential serving - modern and legacy bundles using module/nomodule scripts

Simorgh creates 2 client-side JavaScript bundles. The 2 bundles are made of multiple scripts (or chunks) that are prefixed with `legacy.` and `modern.`, for example, `legacy.main-49d0a293.a47dd2b9.js` and `modern.main-49d0a293.abb18c4e.js`.

### Why create modern and legacy bundles?

Legacy browsers need the JavaScript we write to be transformed into something the browser is able to understand and often needs polyfills packaged along with it for missing features of the language. This bloats the JavaScript bundle size and reduces performance for modern browsers that do not need as many transformations or polyfills. At the time of writing, roughly 95% of browsers in use have support for ES2017 syntax and understand most of the code we write without using as many transformations or polyfills. By building 2 separate bundles and conditionally loading and executing only 1 we are increasing the performance for roughly 95% of our users while still providing support to legacy browsers.

### How is this achieved in Simorgh?

Simorgh will conditionally load and execute all scripts prefixed with either `legacy.` or `modern.` depending on your browser. Currently, Simorgh considers a browser to be modern if it supports ES2017 syntax and transpiles legacy JavaScript to ES5 syntax for browsers such as IE11 and Opera Mini.

Simorgh uses Webpack to build 2 different client-side bundles. Most of the client-side Webpack configuration is found in `webpack.client.js`. This config is run with a `BUNDLE_TYPE` argument that returns config for a `modern` or `legacy` browser. The Webpack config uses config from `.babelrc.js` to provide the appropriate JavaScript transformations and polyfills. `.babelrc.js` also needs to dynamically return modern or legacy config but does so using the `process.ENV` variable that is conditionally set using `envName` in the `babel-loader` options in the Webpack config.

Now that we have the mechanism for generating 2 separate bundles we need to include them in the HTML document. Both modern and legacy bundles need added to the document but the conditional loading and executing is handled using the [module/nomodule](https://3perf.com/blog/polyfills/#modulenomodule) pattern. For example:

```html
<!-- legacy browsers ignore scripts with type="module" -->
<script type="module" src="modern.main-49d0a293.abb18c4e.js"></script>

<!-- modern browsers ignore scripts with nomodule -->
<script nomodule src="legacy.main-49d0a293.a47dd2b9.js"></script>
```

Simorgh uses Loadable Components (a library Simorgh uses for code-splitting) to handle generating script elements. On the server-side, Loadable Components analyses 2 stats files (modern and legacy) generated by Webpack so it can generate the script elements needed in the HTML document. On the client-side, the Loadable Components library queries the DOM for a json script tag (by tag ID `legacy__LOADABLE_REQUIRED_CHUNKS__` or `modern__LOADABLE_REQUIRED_CHUNKS__` which is set using the `namespace` option on the server-side) that contains the JavaScript chunk IDs that Loadable Components will asynchronously load.

### Gotchas

* Safari 10.1 supports modules, but does not support the `nomodule` attribute. This results in Safari downloading and executing both legacy and modern bundles. A polyfill has been included to prevent this behaviour.
* IE11 downloads both modern and legacy bundles but only executes the legacy bundle. This worsens performance for IE11 users because they will have to download almost twice the amount of JavaScript. IE11 accounts for around 0.06% of page visits across the World Service sites. Based on this we decided the impact is not high enough to prevent us from providing a better experience to the vast majority of users.
* The Webpack dev server run using `yarn dev` currently only uses modern JavaScript. If you are cross-browser testing locally make sure that you build Simorgh with `yarn build` and start the Express server with `yarn start`.
* The bundle analyser script that runs after builds and displays bundle size information by default will run on the modern bundle. If you would like to see bundle size information for the legacy bundle you can run a build (`yarn build`) and then run `bundleType=legacy node scripts/bundleSize`.

### More on legacy vs modern bundles

* [Publish, ship, and install modern JavaScript for faster applications](https://web.dev/publish-modern-javascript/)
* [Deploying ES2015+ Code in Production Today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)

## Code-splitting

Because we make multiple releases per day with updated application and library (node\_module) code we split our client-side JavaScript bundle into multiple chunks to improve cache efficiency so that the amount of cache-invalidated chunks after each deployment is kept to a minimum.

Currently, our chunking strategy is as follows:

* **A minimal chunk for each service**

  This includes a chunk for service specific config, translations and moment locales.

* **A minimal commons chunk for each page type**

  This includes only code used across all page types so our pages share a chunk that doesn't contain any unnecessary code.

* **A framework chunk**

  A chunk for `react` and `react-dom`. By isolating the framework code we ensure that it will not be cache-invalidated by irrelevant changes made to application code.

* **Library chunks for any large node\_module dependencies**

  A chunk each for any large libraries such as `moment` to avoid potential cache invalidations made by changes made to application code.

* **Shared chunks**

  As many shared chunks (used by 2 or more pages) as possible, optimising for overall application size and initial load speed.

This strategy was mostly inspired by the Next.js and Gatsby approach detailed [here](https://web.dev/granular-chunking-nextjs/).

### Webpack

We use Webpack to configure our client-side JavaScript bundling strategy. The client-side Webpack config can be found [here](https://github.com/bbc/simorgh/blob/latest/webpack.config.client.js) and the bundling strategy described above is defined in the `splitChunks` object.

### Loadable

We use `@loadable/component` to code-split service specific and page type code into separate chunks that are loaded only in the corresponding service and page type. `@loadable/component` integrates with Webpack. To ensure the commons chunk is configured properly we need to define the number of page types we have in Webpack's `splitChunks.commons.minChunks` configuration.

### Total bundles sizes for each page type and service

We use a custom script to output the total JavaScript bundle size for each service and page type after every build. You can view this information in your terminal after running `yarn build`.