Code-splitting is hard
Tags: webperf, react, codenewbie, webpack
Published: 2020-11-17 11:02:47

---
canonical_url: https://imkev.dev/code-splitting-is-hard
---

Occasionally code-splitting and chunking is avoided as a performance optimisation because it is thought of as complex or "black-magic". I hope to address those fears, because in most cases code-splitting could be done with relative ease and may reward you with considerable gains; especially for apps which are heavily reliant on JavaScript.

The hardest part about code-splitting may be deciding on _how_ to split your code & bundles. This is not convered in this article, but there is lots of material on this topic. You may want to read: [Improved Next.js and Gatsby page load performance with granular chunking](https://web.dev/granular-chunking-nextjs/) or [The 100% correct way to split your chunks with Webpack](https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758)

The code for this demo is available on [GitHub](https://github.com/kevinfarrugia/code-splitting-demo). Contributions & feedback are always welcome.

The demo is built using Webpack and ReactJS, using `React.lazy` [route-based code-splitting](https://reactjs.org/docs/code-splitting.html#route-based-code-splitting) and [react-router-dom](https://reactrouter.com/web/guides/quick-start).

## Will stylesheets be bundled in separate chunks?

If one or more stylesheets are imported in a single module or its dependencies, then a reference to the bundled stylesheet will only be included in that module's chunk. For clarity, a chunk may consist of more than one file (JS & CSS).

In our example, [**Page1**](https://github.com/kevinfarrugia/code-splitting-demo/blob/master/src/js/components/page1/index.jsx) imports a stylesheet:

      import styles from "./style.scss";

As it is the only file which references this stylesheet, then it will be chunked. Additionally, **Page1** is the only module which imports the [**Glider**](https://github.com/kevinfarrugia/code-splitting-demo/blob/master/src/js/components/glider/index.jsx) component; which in turn imports another two stylesheets and also has a dependency on the third-party [glider-js](https://github.com/NickPiscitelli/Glider.js).

**src/js/components/glider/index.jsx**

      import "glider-js";
      import "glider-js/glider.min.css";

All these will be included in a single chunk, together with the output of `style.scss` above.

On the contrary, if a stylesheet is imported in more than one module, then the bundler will output a single stylsheet referenced by both modules.

In our example, [**Page2**](https://github.com/kevinfarrugia/code-splitting-demo/blob/master/src/js/components/page2/index.jsx) imports a shared stylesheet:

      import sharedStyles from "../home/style.scss";

This stylesheet is also imported in the [**Home**](https://github.com/kevinfarrugia/code-splitting-demo/blob/master/src/js/components/home/index.jsx) module and therefore is not included in the **Page2** chunk.

## What about images?

By design, images are only downloaded when needed and present in the DOM. This means that images should have no impact on your bundle sizes.

If you are importing your images using file-loader's [`esModule`](https://webpack.js.org/loaders/file-loader/#esmodule) then you will also benefit from module concatenation and tree-shaking on your images; but this is not specifically code-splitting.

However, if you are using [url-loader](https://webpack.js.org/loaders/url-loader/) and your images are being encoded into Base64 or SVG strings, then they will be encoded into each chunk resulting in duplicate code.

## May I use CommonJS imports?

Yes, CommonJS & ES6 module imports work equally well.

In our example, in **Page2** the below two lines would result in equivalent chunks:

      const styles = require("./style.scss");
      //import stylesfrom "./style.scss");

## When using route-based code-splitting, is it possible to have some routes lazy-loaded while others loaded regularly?

Yes, definitely.

In this demo, the **Home** module is loaded regularly while the other pages are loaded lazily.

      import Home from "../home";
      const Page1 = React.lazy(() => import("../page1"));
      const Page2 = React.lazy(() => import("../page2"));

      <Suspense fallback={null}>
        <Switch>
          <Route path="/1" exact>
            <Page1 />
          </Route>
          <Route path="/2" exact>
            <Page2 />
          </Route>
          <Route>
            <Home />
          </Route>
        </Switch>
      </Suspense>

## Does code-splitting work with named exports?

`React.lazy` requires you to have a default export, however you may still use named exports for other components, even for those which are being referenced by the lazily loaded component.

## What about re-exporting? Will `export * from "./my-module"` be tree-shaken?

Using `export * from "./my-module"` means that any export in **./my-module**, regardless of whether it is used or unused, would need to be evaluated and executed in case one of those exports has side-effects. As a result, you need to explicitly inform Webpack that the file has no side-effects using the `sideEffects` **package.json** property. Sean Larkin has an excellent explanation on [Stack Overflow](https://stackoverflow.com/a/49203452/2315681).

The example code in our demo includes a component **Page3** which exports an unused component **../glider-named-export**. Without `sideEffects: false`, the resultant chunk includes the contents of **../glider-named-export**, even if it is never actually being used.

## Does this work with critical (inlined) CSS?

Yes it does.

The configuration used in this demo inlines a single critical CSS file which includes all critical CSS defined across the project. This is done using the following code inside **scripts/webpack.config.js**:

    criticalStyles: {
      name: "critical",
      test: /critical\.(sa|sc|c)ss$/,
      chunks: "initial",
      enforce: true,
    }

The output of this chunk is then inlined in **src/templates/index.hbs**:

    <% if (/critical(\..*)?\.css$/.test(htmlWebpackPlugin.files.cssindex])) { %>
      <style>
        <%= compilation.assets[htmlWebpackPlugin.files.cssindex].substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
      </style>
    <% } %>

## Will users have to wait to download the chunks on successive routes?

In theory yes, because these files were not downloaded yet and will only be downloaded once the user requests them by visiting the page; but this could be mitigated through the use of a service-worker precache which will download the remaining chunks after the initial page load.

In the demo I am using the highly recommended [WorkboxPlugin](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin):

    new WorkboxPlugin.InjectManifest({
      swSrc: `${ROOT_DIR}/src/sw.js`,
      swDest: "sw.js",
      include: [/\.js$/, /\.css$/],
    })

* * *

Thank you for reading and please leave me your feedback. As always, it would be very welcome & appreciated. If you want to get in touch, you could reach me on Twitter [@imkevdev](https://twitter.com/imkevdev).