Granular chunks and JavaScript modules for faster page loads
Tags: webdev, webperf, webpack, javascript
Published: 2020-05-10 09:37:07

---
title: Granular chunks and JavaScript modules for faster page loads
published: true
description: Build a Webpack configuration to improved page load performance with granular chunking strategy and JavaScript modules for modern browsers
tags: webdev, webperf, webpack, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/sowa8ru3q3w4l5n31k0m.jpg
---

The race to performance increases from year to year and the front-end ecosystem is evolving more than never.

This article covers how to build a Webpack configuration to improve page load performance. Learn how to set up a granular chunking strategy to split common code. Then, serve modern code with JavaScript modules to modern browsers.

## Webpack configuration

To start, the configuration has the following features:

* Multiple Page Application
* Development and production environment
* JavaScript transpilation with Babel and `preset-env`
* CSS extraction
* Default optimization behavior

First, let's write our Webpack starter configuration.

**webpack.config.js**

```javascript
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// Export a function for environment flexibility
module.exports = (env, argv) => {

  // Webpack mode from the npm script
  const isProduction = argv.mode === 'production';

  return {
    watch: !isProduction,

    // Object entry for Multiple Page Application
    entry: {
      home: 'home.js',
      news: 'news.js'
    },

    output: {
      path: path.resolve(__dirname, './dist/assets'),
      filename: '[name].js'
    },

    module: {
      rules: [
        // Babel transpilation for JavaScript files
        {
          test: /\.js$/,
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  targets: {
                    browsers: ['last 2 versions']
                  },
                  // Include polyfills from core-js package
                  useBuiltIns: 'usage',
                  corejs: 3
                }
              ]
            ]
          }
        },

        // Extract content for CSS files
        {
          test: /\.css$/i,
          use: [MiniCssExtractPlugin.loader, 'css-loader']
        }
      ]
    },

    resolve: {
      extensions: ['.js', '.css']
    },

    plugins: [
      // Configure CSS extraction
      new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].css'
      })
    ],

    // Default optimization behavior depending on environment
    optimization: {
      minimize: isProduction
    }
  }
};
```

For more flexibility, the configuration exports a function, but other [configuration types](https://webpack.js.org/configuration/configuration-types/#exporting-a-function) are available.

The `entry` key is an object to accept multiple entries (Multiple Page Application). Each entry contains the code for a specific page of the site (ex: home, news, etc.).

The `module.rules` key is an array with two rules, one for the JavaScript files and one for the CSS files.

The `babel-loader` is used to transpile JavaScript with the presets from `@babel/preset-env`.

> 💡 Browsers list
> The `last 2 versions` transpiles the code for the last two versions of every browsers. It's not the best option, choose instead a browsers list to match your real audience.

The `css-loader` is used to interpret CSS files and `MiniCssExtractPlugin` to extract CSS content in a dedicated file.

The `plugins` array has a unique plugin `MiniCssExtractPlugin` to extract CSS content.

The `optimization` object has the default behavior; the `minimize` option depends of the Webpack `mode` (development or production).

> 💡 Minimizer
> The `minimize` key can be replaced by `minimizer` to perform optimization with [TerserPlugin](https://webpack.js.org/plugins/terser-webpack-plugin).

Let's add the npm scripts that will start and build Webpack:

**package.json**

```json
{
  "start": "webpack --mode=development",
  "build": "webpack --mode=production"
}
```

---

## Granular chunks

### Split common code

Webpack `splitChunks` allows to split common code used inside all entrypoints.

This generates one entrypoint file for JavaScript and CSS plus multiple chunk files which contain common code.

Imagine the pages share some common code for the header. Without the optimization, common code is duplicated across every entrypoints.

![Webpack — SplitChunks disabled](https://dev-to-uploads.s3.amazonaws.com/i/kivf1zj3o9k7d7skd90p.jpg)

With the optimization, a chunk is automatically created with the shared code.

![Webpack — SplitChunks enabled](https://dev-to-uploads.s3.amazonaws.com/i/yjv86h5f1gy1b8eax2m3.jpg)

To use this option with multiple entrypoints, the easiest is to install the `chunks-webpack-plugin`.

```bash
npm install chunks-webpack-plugin --save-dev
```

Then, update the Webpack configuration to add the plugin.

```javascript
const ChunksWebpackPlugin = require('chunks-webpack-plugin');

module.exports = (env, argv) => {
  return {
    // ...
    plugins: [
      new ChunksWebpackPlugin({
        outputPath: path.resolve(__dirname, './dist/templates'),
        fileExtension: '.html.twig',
        templateStyle: '<link rel="stylesheet" href="{{chunk}}" />',
        templateScript: '<script defer src="{{chunk}}"></script>'
      })
    ]
  };
};
```

Enable the `optimization.splitChunks` to target `all` type of chunks.

```javascript
module.exports = (env, argv) => {
  return {
    // ...
    optimization: {
      splitChunks: {
        chunks: 'all',
        name: false
      }
    }
  };
};
```

> 💡 Build performance
> Enable `splitChunks` only for production. I strongly advise you to enable the `name` option for debugging.

That's all, granular chunking is done, no more configuration 🎉

### Include chunk templates

Now that everything is set up, include the generated templates in the page templates.

With a multiple page application, a base layout is commonly used and pages override blocks. The layout defines the blocks. The pages includes specific files inside these blocks.

**base.html.twig**

```twig
<!DOCTYPE html>
<html>
  <head>
    {% block styles %}{% endblock %}
    {% block scripts %}{% endblock %}
  </head>
  <body>
    {% block body %}
      {# Application code here #}
    {% endblock %}
  </body>
</html>
```

**home.html.twig**

```twig
{% extends 'base.html.twig' %}

{% block styles %}
  {{ include "dist/templates/home-styles.html.twig" }}
{% endblock %}

{% block body %}{% endblock %}

{% block scripts %}
  {{ include "dist/templates/home-script.html.twig" }}
{% endblock %}
```

**news.html.twig**

```twig
{% extends 'base.html.twig' %}

{% block styles %}
  {{ include "dist/templates/news-styles.html.twig" }}
{% endblock %}

{% block body %}{% endblock %}

{% block scripts %}
  {{ include "dist/templates/news-script.html.twig" }}
{% endblock %}
```

Content of these generated templates will looks like this:

**home-styles.html.twig**

```twig
<link rel="stylesheet" href="dist/assets/vendors~home~news.css" />
<link rel="stylesheet" href="dist/assets/home.css" />
```

**home-scripts.html.twig**

```twig
<script src="dist/assets/vendors~home~news.js"></script>
<script src="dist/assets/home.js"></script>
```

---

## Script type module & nomodule

Many polyfills are not needed for modern browsers. By using modules, Babel transpilation can be avoided and bundle sizes are reduced.

HTML provides useful attributes for the `<script>` tag to detect modern browsers and JavaScript modules' support.

### `<script type="module">`

Serve JavaScript modules with ES2015+ syntax for modern browsers (without Babel transpilation).

```html
<script src="dist/assets/modern/home.js" type="module"></script>
```

### `<script nomodule>`

Serve JavaScript with ES5 syntax for older browsers (with Babel transpilation).

```html
<script src="dist/assets/legacy/home.js" nomodule></script>
```

### Browsers support

Browsers that support modules ignore scripts with the `nomodule` attribute. And vice versa, browsers that do not support modules ignore scripts with the `type="module"` attribute.

This feature is supported by all latest versions of modern browsers, see on [Can I use](https://caniuse.com/#feat=es6-module).

> ⚠️ Bad guys
> IE 11 and Safari 10 are a problematic and download both bundles. Discussion is in progress to fix it on Safari, see the [Github Gist](https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc).

### Multiple Webpack configurations

Instead of exporting a single Webpack configuration, you may export multiple configurations. Simply wrap the different object configurations inside an array.

Let's create a function to avoid code duplication between our configurations.

**config-generator.js**

```javascript
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ChunksWebpackPlugin = require('chunks-webpack-plugin');

const configGenerator = ({ browsers, isProduction, presets }) => {
  // Custom attribute depending the browsers
  const scriptAttribute = browsers === 'modern' ? 'type="module"' : 'nomodule';

  return {
    // The name of the configuration
    name: browsers,

    watch: !isProduction,
    entry: {
      home: 'home.js',
      news: 'news.js'
    },
    output: {
      path: path.resolve(__dirname, `./dist/assets/${browsers}`),
      filename: '[name].js'
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          loader: 'babel-loader',
          options: {
            // Presets depending the browsers
            presets
          }
        },
        {
          test: /\.css$/i,
          use: [MiniCssExtractPlugin.loader, 'css-loader']
        }
      ]
    },
    resolve: {
      extensions: ['.js', '.css']
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].css'
      }),
      new ChunksWebpackPlugin({
        outputPath: path.resolve(__dirname, `./dist/templates/${browsers}`),
        fileExtension: '.html.twig',
        templateStyle: '<link rel="stylesheet" href="{{chunk}}" />',
        // Custom tags depending the browsers
        templateScript: `<script defer ${scriptAttribute} src="{{chunk}}"></script>`
      })
    ],
    optimization: {
      splitChunks: {
        chunks: 'all',
        name: false
      }
    }
  };
};
```

Next, the `webpack.config.js` needs to export two configuration with the `configGenerator` function. The first for modern browsers and the second for legacy browsers, with the different Babel presets. The presets target [`esmodules`](https://babeljs.io/docs/en/babel-preset-env#targetsesmodules) browsers instead of a browsers list.

**webpack.config.js**

```javascript
import configGenerator from './config-generator';

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';

  // Modern browsers that support Javascript modules
  const configModern = configGenerator({
    browsers: 'modern',
    isProduction,
    presets: [
      [
        '@babel/preset-env',
        {
          targets: {
            esmodules: true
          }
        }
      ]
    ]
  });

  // Legacy browsers that do not support Javascript modules
  const configLegacy = configGenerator({
    browsers: 'legacy',
    isProduction,
    presets: [
      [
        '@babel/preset-env',
        {
          targets: {
            esmodules: false
          },
          useBuiltIns: 'usage',
          corejs: 3
        }
      ]
    ]
  });

  return [configModern, configLegacy];
};
```

When running Webpack, all configurations are built.

> 💡 Run a specific configuration
> Add `--config-name=<BROWSERS>` at the end of the npm script. Replace `<BROWSERS>` by the name of the Webpack configuration (`modern` or `legacy` in the example above).

### Update chunk templates

Include both bundles for JavaScript to target modern and legacy browsers. For CSS, the configuration is identical for both browsers, you can import one or the other.

**home.html.twig**

```twig
{% extends 'base.html.twig' %}

{% block styles %}
  {{ include "dist/templates/modern/home-styles.html.twig" }}
{% endblock %}

{% block body %}{% endblock %}

{% block scripts %}
  {{ include "dist/templates/modern/home-script.html.twig" }}
  {{ include "dist/templates/legacy/home-script.html.twig" }}
{% endblock %}
```

**news.html.twig**

```twig
{% extends 'base.html.twig' %}

{% block styles %}
  {{ include "dist/templates/modern/news-styles.html.twig" }}
{% endblock %}

{% block body %}{% endblock %}

{% block scripts %}
  {{ include "dist/templates/modern/news-script.html.twig" }}
  {{ include "dist/templates/legacy/news-script.html.twig" }}
{% endblock %}
```

## Conclusion

You now understand how to customize the Webpack configuration to improve page load performance.

Granular chunks with Webpack and `chunks-webpack-plugin` offer a better strategy to shares common code.

Next, JavaScript modules provide minimal polyfills and smaller bundles for modern browsers.

The complete example is available on [Github](https://github.com/yoriiis/webpack-splitchunks-esmodule), so you can have fun with it! 🧑‍💻

## Additional reading

* [Improved Next.js and Gatsby page load performance with granular chunking](https://web.dev/granular-chunking-nextjs)
* [Serve modern code to modern browsers for faster page loads](https://web.dev/codelab-serve-modern-code)
* [last 2 versions" considered harmful](https://jamie.build/last-2-versions)
* [The real power of Webpack 4 SplitChunks Plugin](https://medium.com/@Yoriiis/the-real-power-of-webpack-4-splitchunks-plugin-fad097c45ba0)

_Photo by @dylan_nolte on Unsplash_
_With thanks to Emilie Gervais for her review_