Maintain Control: A Guide to Webpack and React, Pt. 1
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 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 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:
-
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[\\/]/ 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 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!
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK