Maintain Control: A Webpack/React Tutorial, Pt. 1
When starting a new React project, you have many templates to choose from. These templates 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.
When starting a new React project, you have many templates to choose from. These templates 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.
Michael is a senior full-stack developer specializing in front-end development with React and TypeScript.
Expertise
When starting a new React 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 React 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.
React Webpack 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
: Enables source-map generation in development mode. -
entry
: The main file of our React application. -
output.path
: The root directory to store output files in. -
output.filename
: The filename pattern to use for generated files. -
output.publicPath
: The path to the root directory where the files will be deployed on the web server.
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 in React projects, 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:
@@ -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"]
}
};
};
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 dynamicimport()
syntax in browsers lacking nativePromise
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:
-
babel-plugin-transform-react-remove-prop-types
removes unnecessary prop-types from production code. -
@babel/plugin-transform-react-inline-elements
evaluatesReact.createElement
during compilation and inlines the result. -
@babel/plugin-transform-react-constant-elements
extracts static React elements as constants.
The command below will install all the necessary dependencies:
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
: Parses CSS files, resolving external resources, such as images, fonts, and additional style imports. -
style-loader
: During development, injects loaded styles into the document at runtime. -
mini-css-extract-plugin
: Extracts loaded styles into separate files for production use to take advantage of browser caching.
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";
@@ -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 add MiniCssExtractPlugin
to the plugins
section, which we’ll only enable in production mode:
@@ -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 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,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)
};
};
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,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.
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 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()
+ ]
+ }
};
};
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:
- 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. - 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 dynamicimport()
. 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
andmaxAsyncChunks: 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[\\/]/
: Filename pattern for matching third-party dependencies. -
name(module, chunks, cacheGroupKey)
: Groups separate chunks from the same module together by giving them a common name.
-
-
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 thecommon
cache group so that chunks for thevendors
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 toindex.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!
The first part of our React/Webpack tutorial covered loading various resource types with Webpack, using Webpack with React 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.
Further Reading on the Toptal Blog:
Understanding the basics
Does React use Webpack?
No, but they can be used together. Webpack and React accomplish different tasks in the development process. React can be used without Webpack.
Can you use React without Webpack?
Yes, you can use a different module bundler for writing React applications, or skip the bundling step altogether.
What is the purpose of Webpack?
Webpack ties source files together in a way that can be understood by the browser.
What is Webpack and how does it work?
Webpack is a module bundler for JavaScript. Webpack resolves external module dependencies and publishes them in a way that can be understood by the browser.
What is the Webpack bundler?
Webpack is an npm module used for bundling JavaScript. It is responsible for collecting application dependencies and merging them for consumption by web browsers.
Is Webpack a framework?
No, it is not. A framework is something that lets you write your code in a different, more convenient way, or introduces new functionality. Webpack does not do any of that and instead comes after the fact to optimize and adapt your code to different environments.
Porto, Portugal
Member since December 5, 2018
About the author
Michael is a senior full-stack developer specializing in front-end development with React and TypeScript.