25

Maintain Control: A Guide to Webpack and React, Pt. 1

 4 years ago
source link: https://www.toptal.com/react/webpack-react-tutorial-pt-1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

When starting a newReact project, you have many templates to choose from: Create React App , react-boilerplate , and React Starter Kit , to name a few.

These templates, adopted by thousands of developers, are able to support application development at a very large scale. But they leave the developer experience and bundle output saddled with various defaults, which may not be ideal.

If you want to maintain a greater degree of control over your build process, then you might choose to invest in a custom Webpack configuration. As you’ll learn from this Webpack tutorial, this task is not very complicated, and the knowledge might even be useful when troubleshooting other people’s configurations.

Webpack: Getting Started

The way we write JavaScript today is different from the code that the browser can execute. We frequently rely on other types of resources, transpiled languages, and experimental features which are yet to be supported in modern browsers. Webpack is a module bundler for JavaScript that can bridge this gap and produce cross browser–compatible code at no expense when it comes to developer experience.

Before we get started, you should keep in mind that all code presented in this Webpack tutorial is also available in the form of a complete Webpack/React example configuration file on GitHub . Please feel free to refer to it there and come back to this article if you have any questions.

Base Config

Since Legato (version 4), Webpack does not require any configuration to run. Choosing a build mode will apply a set of defaults more suitable to the target environment. In the spirit of this article, we are going to brush those defaults aside and implement a sensible configuration for each target environment ourselves.

First, we need to install webpack and webpack-cli :

npm install -D webpack webpack-cli

Then we need to populate webpack.config.js with a configuration featuring the following options:

devtool
entry
output.path
output.filename
output.publicPath
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: "/"
    }
  };
};

The above configuration works fine for plain JavaScript files. But when using Webpack and React, we will need to perform additional transformations before shipping code to our users. In the next section, we will use Babel to change the way Webpack loads JavaScript files.

JS Loader

Babel is a JavaScript compiler with many plugins for code transformation. In this section, we will introduce it as a loader into our Webpack configuration and configure it for transforming modern JavaScript code into such that is understood by common browsers.

First, we will need to install babel-loader and @babel/core :

npm install -D @babel/core babel-loader

Then we’ll add a module section to our Webpack config, making babel-loader responsible for loading JavaScript files:

@@ -10,6 +10,22 @@ 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"
+            }
+          }
+        }
+      ]
     }
   };
 };

We are going to configure Babel using a separate configuration file, babel.config.js . It will use the following features:

  • @babel/preset-env : Transforms modern JavaScript features into backwards-compatible code.
  • @babel/preset-react : Transforms JSX syntax into plain-vanilla 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, for writing class-based React components.

We’ll also enable a few React-specific production optimizations :

The command below will install all the necessarydependencies:

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

Then we’ll populate babel.config.js with these settings:

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 allows us to write modern JavaScript in a way that is compatible with all relevant browsers. There are other types of resources that we might need in a React application, which we will cover in the following sections.

CSS Loader

When it comes to styling React applications, at the very minimum, we need to be able to include plain CSS files. We are going to do this in Webpack using the following loaders:

css-loader
style-loader
mini-css-extract-plugin

Let’s install the above CSS loaders:

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

Then we’ll add a new rule to the module.rules section of our Webpack config:

@@ -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";
@@ -26,6 +27,13 @@ module.exports = function(_env, argv) {
               envName: isProduction ? "production" : "development"
             }
           }
+        },
+        {
+          test: /\.css$/,
+          use: [
+            isProduction ? MiniCssExtractPlugin.loader : "style-loader",
+            "css-loader"
+          ]
         }
       ]
     }

We’ll also add MiniCssExtractPlugin to the plugins section, which we’ll only enable in production mode:

@@ -36,6 +36,13 @@ module.exports = function(_env, argv) {
           ]
         }
       ]
-    }
+    },
+    plugins: [
+      isProduction &&
+        new MiniCssExtractPlugin({
+          filename: "assets/css/[name].[contenthash:8].css",
+          chunkFilename: "assets/css/[name].[contenthash:8].chunk.css"
+        })
+    ].filter(Boolean)
   };
 };

This configuration works for plain CSS files and can be extended to work with various CSS processors, such as Sass and PostCSS, which we’ll discuss in the next article.

Image Loader

Webpack can also be used to load static resources such as images, videos, and other binary files. The most generic way of handling such types of files is by using file-loader or url-loader , which will provide a URL reference for the required resources to its consumers.

In this section, we will add url-loader to handle common image formats. What sets url-loader apart from file-loader is that if the size of the original file is smaller than a given threshold, it will embed the entire file in the URL as base64-encoded contents, thus removing the need for an additional request.

First we install url-loader :

npm install -D url-loader

Then we add a new rule to the module.rules section of our Webpack config:

@@ -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]"
+            }
+          }
         }
       ]
     },

SVG

For SVG images, we are going to use the @svgr/webpack loader, which transforms imported files into React components.

We install @svgr/webpack :

npm install -D @svgr/webpack

Then we add a new rule to the module.rules section of our Webpack config:

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

SVG images as React components can be convenient, and @svgr/webpack performs optimization using SVGO .

Note: For certain animations or even mouseover effects, you may need to manipulate the SVG using JavaScript. Fortunately, @svgr/webpack embeds SVG contents into the JavaScript bundle by default, allowing you to bypass the security restrictions needed for this.

File-loader

When we need to reference any other kinds of files, the generic file-loader will do the job. It works similarly to url-loader , providing an asset URL to the code that requires it, but it makes no attempt to optimize it.

As always, first we install the Node.js module. In this case, file-loader :

npm install -D file-loader

Then we add a new rule to the module.rules section of our Webpack config. For example:

@@ -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 added file-loader for loading fonts, which you can reference from your CSS files. You can extend this example to load any other kinds of files you need.

Environment Plugin

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

@@ -1,6 +1,7 @@
 const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
+const webpack = require("webpack");

 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -67,6 +68,11 @@ module.exports = function(_env, argv) {
   	new HtmlWebpackPlugin({
     	template: path.resolve(__dirname, "public/index.html"),
     	inject: true
+  	}),
+  	new webpack.DefinePlugin({
+    	"process.env.NODE_ENV": JSON.stringify(
+      	isProduction ? "production" : "development"
+    	)
   	})
 	].filter(Boolean)
   };

Here we substituted process.env.NODE_ENV with a string representing the build mode: "development" or "production" .

HTML Plugin

In the absence of an index.html file, our JavaScript bundle is useless, just sitting there with no one able to find it. In this section, we will introduce html-webpack-plugin to generate an HTML file for us.

We install html-webpack-plugin :

npm install -D html-webpack-plugin

Then we add html-webpack-plugin to the plugins section of our Webpack config:

@@ -1,5 +1,6 @@
 const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const HtmlWebpackPlugin = require("html-webpack-plugin");

 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -63,7 +64,11 @@ module.exports = function(_env, argv) {
         new MiniCssExtractPlugin({
           filename: "assets/css/[name].[contenthash:8].css",
           chunkFilename: "assets/css/[name].[contenthash:8].chunk.css"
-        })
+        }),
+      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.

Optimization

There are several optimization techniques that we can use in our build process. We will begin with code minification, a process by which we can reduce the size of our bundle at no expense in terms of functionality. We’ll use two plugins for minimizing our code: terser-webpack-plugin for JavaScript code, and optimize-css-assets-webpack-plugin for CSS.

Let’s install them:

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

Then we’ll add an optimization section to our config:

@@ -2,6 +2,8 @@ const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const webpack = require("webpack");
+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()
+      ]
+    }
   };
 };

The settings above will ensure code compatibility with all modern browsers.

Code Splitting

Code splitting is another technique that we can use to improve the performance of our application. Code splitting can refer to two different approaches:

  1. Using a dynamic import() statement, we can extract parts of the application that make up a significant portion of our bundle size, and load them on demand.
  2. We can extract code which changes less frequently, in order to take advantage of browser caching and improve performance for repeat-visitors.

We’ll populate the optimization.splitChunks section of our Webpack configuration with settings for extracting third-party dependencies and common chunks into separate files:

@@ -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 take a deeper look at the options we’ve used here:

  • chunks: "all" : By default, common chunk extraction only affects modules loaded with a dynamic import() . This setting enables optimization for entry-point loading as well.
  • minSize: 0 : By default, only chunks above a certain size threshold become eligible for extraction. This setting enables optimization for all common code regardless of its size.
  • maxInitialRequests: 20 and maxAsyncChunks: 20 : These settings increase the maximum number of source files that can be loaded in parallel for entry-point imports and split-point imports, respectively.

Additionally, we specify the following cacheGroups configuration:

  • vendors : Configures extraction for third-party modules.
    test: /[\\/]node_modules[\\/]/
    name(module, chunks, cacheGroupKey)
    
  • common : Configures common chunks extraction from application code.
    • minChunks: 2 : A chunk will be considered common if referenced from at least two modules.
    • priority: -10 : Assigns a negative priority to the common cache group so that chunks for the vendors cache group would be considered first.

We also extract Webpack runtime code in a single chunk that can be shared between multiple entry points, by specifying runtimeChunk: "single" .

Dev Server

So far, we have focused on creating and optimizing the production build of our application, but Webpack also has its own web server with live reloading and error reporting, which will help us in the development process. It’s called webpack-dev-server , and we need to install it separately:

npm install -D webpack-dev-server

In this snippet we introduce a devServer section into our Webpack config:

@@ -120,6 +120,12 @@ module.exports = function(_env, argv) {
         }
       },
       runtimeChunk: "single"
+    },
+    devServer: {
+      compress: true,
+      historyApiFallback: true,
+      open: true,
+      overlay: true
     }
   };
 };

Here we’ve used the following options:

  • compress: true : Enables asset compression for faster reloads.
  • historyApiFallback: true : Enables a fallback to index.html for history-based routing.
  • open: true : Opens the browser after launching the dev server.
  • overlay: true : Displays Webpack errors in the browser window.

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

Webpack and React: Performance-optimized and Ready!

We just learned how to load various resource types with Webpack, how to use Webpack in a development environment, and several techniques for optimizing a production build. If you need to, you can always review the complete configuration file for inspiration for your own React/Webpack setup. Sharpening such skills is standard fare for anyone offering React development services .

In the next part of this series, we’ll expand on this configuration with instructions for more specific use cases, including TypeScript usage, CSS preprocessors, and advanced optimization techniques involving server-side rendering and ServiceWorkers. Stay tuned to learn everything you’ll need to know about Webpack to take your React application to production.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK