Controlling Your Webpack/React Project: Part 1 of the Tutorial

When embarking on a new React project, a plethora of templates are at your disposal: Create React App, react-boilerplate, and React Starter Kit are just a few examples.

Widely embraced by developers, these templates provide robust support for large-scale application development. However, they often impose default configurations on the developer experience and bundle output, which might not always align with your specific needs.

For those seeking greater control over their build process, a custom Webpack configuration can be a worthwhile endeavor. This Webpack React tutorial will guide you through the process, demonstrating that it’s not overly complex and that the acquired knowledge can even prove valuable when deciphering configurations created by others.

Webpack: Embarking on Your Journey

Modern JavaScript development often diverges from the code directly executable by browsers. We leverage various resources, transpiled languages, and cutting-edge features not yet universally supported. Webpack, a module bundler for JavaScript, bridges this gap by generating cross-browser compatible code without compromising developer experience.

Before delving in, note that the code snippets in this tutorial are part of a comprehensive Webpack/React example available configuration file on GitHub. Feel free to explore the complete example and refer back to this article for any clarifications.

Configuring Webpack for React

Webpack, since version 4 (Legato), boasts a zero-configuration capability. Selecting a build mode automatically applies a set of suitable defaults for the target environment. However, in line with this article’s objective, we’ll deviate from these defaults and craft our own tailored configuration for each target environment.

Our initial step involves installing webpack and webpack-cli:

1
npm install -D webpack webpack-cli

Next, we’ll populate webpack.config.js with a configuration encompassing the following options:

  • devtool: Activates source-map generation during development.
  • entry: Designates the primary file of our React application.
  • output.path: Specifies the root directory for storing generated output files.
  • output.filename: Defines the filename pattern for the generated files.
  • output.publicPath: Sets the path to the root directory where files will reside on the web server.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const path = require("path");

module.exports = function(_env, argv) {
  const isProduction = argv.mode === "production";
  const isDevelopment = !isProduction;

  return {
    devtool: isDevelopment && "cheap-module-source-map",
    entry: "./src/index.js",
    output: {
      path: path.resolve(__dirname, "dist"),
      filename: "assets/js/[name].[contenthash:8].js",
      publicPath: "/"
    }
  };
};

This configuration suffices for standard JavaScript files. However, React projects necessitate additional transformations before deployment. In the upcoming section, we’ll incorporate Babel to modify how Webpack handles JavaScript files.

Integrating the JS Loader

Babel is a versatile JavaScript compiler with numerous plugins for code transformation. We’ll introduce it as a loader within our Webpack configuration, enabling it to transform modern JavaScript code into a format understandable by widely used browsers.

First, let’s install babel-loader and @babel/core:

1
npm install -D @babel/core babel-loader

Next, we’ll augment our Webpack configuration with a module section, assigning babel-loader the responsibility of handling JavaScript files:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@@ -11,6 +11,25 @@ module.exports = function(_env, argv) {
       path: path.resolve(__dirname, "dist"),
       filename: "assets/js/[name].[contenthash:8].js",
       publicPath: "/"
+    },
+    module: {
+      rules: [
+        {
+          test: /\.jsx?$/,
+          exclude: /node_modules/,
+          use: {
+            loader: "babel-loader",
+            options: {
+              cacheDirectory: true,
+              cacheCompression: false,
+              envName: isProduction ? "production" : "development"
+            }
+          }
+        }
+      ]
+    },
+    resolve: {
+      extensions: [".js", ".jsx"]
     }
   };
 };

Babel’s configuration will reside in a dedicated file, babel.config.js, utilizing the following features:

  • @babel/preset-env: Converts modern JavaScript features into backward-compatible code.
  • @babel/preset-react: Transforms JSX syntax into standard JavaScript function calls.
  • @babel/plugin-transform-runtime: Reduces code duplication by extracting Babel helpers into shared modules.
  • @babel/plugin-syntax-dynamic-import: Enables dynamic import() syntax in browsers lacking native Promise support.
  • @babel/plugin-proposal-class-properties: Enables support for the public instance field syntax proposal, facilitating the writing of class-based React components.

We’ll also enable specific React production optimizations:

The following command installs all the necessary dependencies:

1
npm install -D @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-class-properties babel-plugin-transform-react-remove-prop-types @babel/plugin-transform-react-inline-elements @babel/plugin-transform-react-constant-elements

Now, let’s populate babel.config.js with these settings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        modules: false
      }
    ],
    "@babel/preset-react"
  ],
  plugins: [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties"
  ],
  env: {
    production: {
      only: ["src"],
      plugins: [
        [
          "transform-react-remove-prop-types",
          {
            removeImport: true
          }
        ],
        "@babel/plugin-transform-react-inline-elements",
        "@babel/plugin-transform-react-constant-elements"
      ]
    }
  }
};

This configuration empowers us to write modern JavaScript that seamlessly functions across relevant browsers. Subsequent sections will address handling other resource types commonly used in React applications.

Incorporating the CSS Loader

Styling React applications requires, at a minimum, the inclusion of standard CSS files. We’ll achieve this in Webpack using these loaders:

  • css-loader: Parses CSS files, resolving external resources like images, fonts, and style imports.
  • style-loader: Injects loaded styles into the document dynamically during development.
  • mini-css-extract-plugin: Extracts loaded styles into separate files for production, leveraging browser caching.

Let’s install these CSS loaders:

1
npm install -D css-loader style-loader mini-css-extract-plugin

Now, we’ll append a new rule to the module.rules section of our Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@@ -1,4 +1,5 @@
 const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -25,6 +26,13 @@ module.exports = function(_env, argv) {
               envName: isProduction ? "production" : "development"
             }
           }
+        },
+        {
+          test: /\.css$/,
+          use: [
+            isProduction ? MiniCssExtractPlugin.loader : "style-loader",
+            "css-loader"
+          ]
         }
       ]
     },

We’ll also integrate MiniCssExtractPlugin into the plugins section, enabling it exclusively in production mode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@@ -38,6 +38,13 @@ module.exports = function(_env, argv) {
     },
     resolve: {
       extensions: [".js", ".jsx"]
-    }
+    },
+    plugins: [
+      isProduction &&
+        new MiniCssExtractPlugin({
+          filename: "assets/css/[name].[contenthash:8].css",
+          chunkFilename: "assets/css/[name].[contenthash:8].chunk.css"
+        })
+    ].filter(Boolean)
   };
 };

This configuration effectively handles plain CSS files and can be extended to work with CSS preprocessors like Sass and PostCSS, a topic for the next article.

Integrating the Image Loader

Webpack efficiently manages static resources like images, videos, and binary files. A common approach involves using file-loader or url-loader to provide URL references to these resources.

This section focuses on incorporating url-loader for handling common image formats. What distinguishes url-loader from file-loader is its ability to embed files smaller than a defined threshold directly into the URL as base64-encoded content, reducing requests.

Let’s install url-loader:

1
npm install -D url-loader

Now, we’ll add a new rule to the module.rules section of our Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@@ -34,6 +34,16 @@ module.exports = function(_env, argv) {
             isProduction ? MiniCssExtractPlugin.loader : "style-loader",
             "css-loader"
           ]
+        },
+        {
+          test: /\.(png|jpg|gif)$/i,
+          use: {
+            loader: "url-loader",
+            options: {
+              limit: 8192,
+              name: "static/media/[name].[hash:8].[ext]"
+            }
+          }
         }
       ]
     },

Handling SVG

For SVG images, we’ll employ the @svgr/webpack loader, which transforms imported files into React components.

Let’s install @svgr/webpack:

1
npm install -D @svgr/webpack

Next, add a new rule to the module.rules section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@@ -44,6 +44,10 @@ module.exports = function(_env, argv) {
               name: "static/media/[name].[hash:8].[ext]"
             }
           }
+        },
+        {
+          test: /\.svg$/,
+          use: ["@svgr/webpack"]
         }
       ]
     },

Representing SVG images as React components offers convenience, and @svgr/webpack optimizes them using SVGO.

Note: Manipulating SVGs with JavaScript might be necessary for certain animations or hover effects. Fortunately, @svgr/webpack embeds SVG content directly into the JavaScript bundle, circumventing security restrictions.

Leveraging File-loader

The versatile file-loader handles referencing other file types. Similar to url-loader, it provides asset URLs but without optimization attempts.

As always, we’ll start by installing the file-loader Node.js module:

1
npm install -D file-loader

Then, we’ll append a new rule to the module.rules section of our Webpack configuration. For instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@@ -48,6 +48,13 @@ module.exports = function(_env, argv) {
         {
           test: /\.svg$/,
           use: ["@svgr/webpack"]
+        },
+        {
+          test: /\.(eot|otf|ttf|woff|woff2)$/,
+          loader: require.resolve("file-loader"),
+          options: {
+            name: "static/media/[name].[hash:8].[ext]"
+          }
         }
       ]
     },

Here, we’ve incorporated file-loader for loading fonts, which you can reference from your CSS files. This example extends to loading any other required file types.

Utilizing the Environment Plugin

Webpack’s DefinePlugin() allows us to expose environment variables from the build environment to our application code. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@@ -1,5 +1,6 @@
 const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
 
 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -65,7 +66,12 @@ module.exports = function(_env, argv) {
         new MiniCssExtractPlugin({
           filename: "assets/css/[name].[contenthash:8].css",
           chunkFilename: "assets/css/[name].[contenthash:8].chunk.css"
-        })
+        }),
+      new webpack.DefinePlugin({
+        "process.env.NODE_ENV": JSON.stringify(
+          isProduction ? "production" : "development"
+        )
+      })
     ].filter(Boolean)
   };
 };

In this snippet, we’ve replaced process.env.NODE_ENV with a string representing the build mode: "development" or "production".

Integrating the HTML Plugin

Without an index.html file, our JavaScript bundle remains inaccessible. We’ll introduce html-webpack-plugin to generate this HTML file automatically.

Let’s install html-webpack-plugin:

1
npm install -D html-webpack-plugin

Now, we’ll add html-webpack-plugin to the plugins section of our Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@@ -1,6 +1,7 @@
 const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const webpack = require("webpack");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
 
 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -71,6 +72,10 @@ module.exports = function(_env, argv) {
         "process.env.NODE_ENV": JSON.stringify(
           isProduction ? "production" : "development"
         )
+      }),
+      new HtmlWebpackPlugin({
+        template: path.resolve(__dirname, "public/index.html"),
+        inject: true
       })
     ].filter(Boolean)
   };

The generated public/index.html file will load our bundle and bootstrap our application.

Optimizing Your Build

Several optimization techniques can be applied to our build process. Let’s begin with code minification, reducing bundle size without affecting functionality. We’ll employ two plugins: terser-webpack-plugin for JavaScript and optimize-css-assets-webpack-plugin for CSS.

Let’s install them:

1
npm install -D terser-webpack-plugin optimize-css-assets-webpack-plugin

Now, we’ll add an optimization section to our configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@@ -2,6 +2,8 @@ const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const webpack = require("webpack");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
+const TerserWebpackPlugin = require("terser-webpack-plugin");
+const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");

 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -75,6 +77,27 @@ module.exports = function(_env, argv) {
           isProduction ? "production" : "development"
         )
       })
-    ].filter(Boolean)
+    ].filter(Boolean),
+    optimization: {
+      minimize: isProduction,
+      minimizer: [
+        new TerserWebpackPlugin({
+          terserOptions: {
+            compress: {
+              comparisons: false
+            },
+            mangle: {
+              safari10: true
+            },
+            output: {
+              comments: false,
+              ascii_only: true
+            },
+            warnings: false
+          }
+        }),
+        new OptimizeCssAssetsPlugin()
+      ]
+    }
   };
 };

These settings ensure code compatibility with modern browsers.

Implementing Code Splitting

Code splitting, another valuable technique, encompasses two approaches for enhancing application performance:

  1. Using a dynamic import() statement, we can extract substantial parts of the application and load them on demand.
  2. We can separate less frequently changing code, taking advantage of browser caching to improve performance for returning visitors.

Let’s configure the optimization.splitChunks section to extract third-party dependencies and common chunks into separate files:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@@ -99,7 +99,29 @@ module.exports = function(_env, argv) {
           sourceMap: true
         }),
         new OptimizeCssAssetsPlugin()
-      ]
+      ],
+      splitChunks: {
+        chunks: "all",
+        minSize: 0,
+        maxInitialRequests: 20,
+        maxAsyncRequests: 20,
+        cacheGroups: {
+          vendors: {
+            test: /[\\/]node_modules[\\/]/,
+            name(module, chunks, cacheGroupKey) {
+              const packageName = module.context.match(
+                /[\\/]node_modules[\\/](.*?)([\\/]|$)/
+              )[1];
+              return `${cacheGroupKey}.${packageName.replace("@", "")}`;
+            }
+          },
+          common: {
+            minChunks: 2,
+            priority: -10
+          }
+        }
+      },
+      runtimeChunk: "single"
     }
   };
 };

Let’s examine the options used:

  • chunks: "all": Extends common chunk extraction to entry-point loading, not just dynamic import().
  • minSize: 0: Enables optimization for all common code, regardless of size.
  • maxInitialRequests: 20 and maxAsyncChunks: 20: Increase the maximum parallel source file loads for entry-point and split-point imports.

Additionally, we’ve defined cacheGroups:

  • vendors: Extracts third-party modules.
    • test: /[\\/]node_modules[\\/]/: Matches third-party dependency filenames.
    • name(module, chunks, cacheGroupKey): Groups chunks from the same module with a common name.
  • common: Extracts common chunks from application code.
    • minChunks: 2: Considers a chunk common if referenced by at least two modules.
    • priority: -10: Prioritizes vendors over common during chunk extraction.

We’ve also enabled single-chunk extraction for Webpack runtime code (runtimeChunk: "single") for sharing across entry points.

Introducing the Dev Server

Webpack provides its own web server, webpack-dev-server, offering live reloading and error reporting for development. Let’s install it:

1
npm install -D webpack-dev-server

Now, we’ll add a devServer section to our Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@@ -120,6 +120,12 @@ module.exports = function(_env, argv) {
         }
       },
       runtimeChunk: "single"
+    },
+    devServer: {
+      compress: true,
+      historyApiFallback: true,
+      open: true,
+      overlay: true
     }
   };
 };

We’ve enabled these options:

  • compress: true: Enables asset compression for faster reloads.
  • historyApiFallback: true: Enables fallback to index.html for history-based routing.
  • open: true: Automatically opens the browser after the dev server starts.
  • overlay: true: Displays Webpack errors directly in the browser.

You might also need to configure proxy settings to forward API requests to your backend server.

Webpack and React: A Powerful Combination

This first part of our React/Webpack tutorial covered loading various resource types, using Webpack with React for development, and optimizing production builds. Refer to the complete configuration file](https://github.com/mpontus/webpack-react) for inspiration. Mastering these skills is crucial for anyone providing [React development services].

The next installment will delve into more specific use cases, including TypeScript, CSS preprocessors, and advanced optimizations involving server-side rendering and ServiceWorkers. Stay tuned to become a Webpack expert and confidently take your React applications to production!

Licensed under CC BY-NC-SA 4.0