The intent of this project is to familiarize myself with how webpack works by diving into the documentation and setting up the best webpack boilerplate I can. This is also an exercise in autonomy and self-study.
The content I've found online about webpack has been difficult to use or poorly written, and it seems most tutorials and blogs revolve around create-react-app
, so I'm using this readme to create my own lightweight documentation for future reference.
If it doesn't exist, make it! If it exists, but you don't like it or it doesn't work, make something better! That's my philosophy.
- None
- Development
- Production
- asset/resource
- asset/inline
- asset/source
- asset
- CSS
- Babel
- terser-webpack-plugin
- mini-css-extract-plugin
- clean-webpack-plugin
- html-webpack-plugin
Follow these steps in order to use this project:
- Fork this repository.
- Run
npm install
- To use webpack:
- For development mode:
npm run dev
- For production mode:
npm run build
- For development mode:
Webpack allows us to set the mode. There are several reasons to change the mode:
-
Defining
mode
setsprocess.env.NODE_ENV
toproduction
ordevelopment
if set to the corresponding option. If none are selected, webpack defaults toproduction
. -
Errors are handled differently in
production
compared todevelopment
mode. It's incredibly difficult to parse the bundled, minified,production
file, but it's easy to read errors indevelopment
mode. -
Several plugins are included in
production
mode by default and don't need to be included in the config file.terser-plugin
is one of these. Production mode is designed to make the codebase streamlined, lightweight and small. -
In
development
mode, certain plugins are unnecessary. Code minification and hashing of filenames is unnecessary because users will never be accessing or caching development builds. Development mode is designed to make the build process quick and cater to developer needs rather than end user needs.
There are 3 options:
none
development
production
module.exports = {
mode: 'none',
}
I have built two separate webpack config files:
webpack.development.config.js
for development bundles.webpack.production.config.js
for production bundles.
Each is designed to cater to it's corresponding environment better and will have a separate npm
script in package.json
.
Link to webpack-dev-server documentation.
Link to webpack-dev-server github repo.
While developing new features, build time is costly. Quick build changes are valuable.
port
: Port where dev-server will be running.static
: What will be served on that port.devMiddleware
:index
: file that will be used as index file.writeToDisk
: By default,webpack-dev-server
generates files in memory and doesn't save them to the disk. Dist will be empty, even though application is available in the browser. If set totrue
,webpack-dev-server
writes generated files to dist directory.
module.exports = {
devServer: {
port: 3000,
static: {
directory: path.resolve(__dirname, './dist'),
},
devMiddleware: {
index: 'index.html',
writeToDisk: true,
}
}
}
Inside package.json
, both serve
and --hot
need to be added to the dev
script.
serve
is the command to startwebpack-dev-server
--hot
is the command for hot module replacement.HMR
exchanges, adds, or removes modules while an application is running, without a full reload.
"scripts": {
"dev": "webpack serve --config webpack.development.config.js --hot"
}
This is great for development because:
- Webpack retains application state which is lost during a full reload.
- Instantly update the browser when modifications are made to CSS/JS in the source code, which is almost comparable to changing styles directly in the browser's dev tools.
Rules is an array of rules within the module
object. Individual rules are anonymous object and have three parts: conditions
, results
, and nested rules
.
Rule documentation here.
module.exports = {
module: {
rules: [
{
// Rules will live here
},
{
// Another rule can live here
}
]
}
}
Asset modules enable asset files to be used without configuring additional loaders. Asset modules come built in with webpack 5 and things like raw-loader
, url-loader
and file-loader
are unnecessary.
Asset Modules documentation here.
Type asset/resource
emits a separate file and exports the URL. Previously done with file-loader
.
- This is great for importing large files like a JPG because it allows the browser to download it separately. This keeps the bundle size from bloating.
rules: [
{
test: /\.(png|jpg|gif)$/,
type: 'asset/resource'
}
]
Type asset/inline
exports a data URI of the asset. Previously done with url-loader
.
-
This is great for SVG's because since they're inline, the browser will not make separate requests for each SVG file.
-
This is bad for JPG's because it will turn them to base64 in order to include them inline. This enlarges the size of the bundle dramatically.
rules: [
{
test: /\.(svg)$/,
type: 'asset/inline'
}
]
Type asset/source
exports content of file as a JavaScript string
and injects it into the bundle as is. Previously done with raw-loader
.
- This is useful for
.txt
files.
rules: [
{
tests: /\.txt$/,
type: 'asset/source'
}
]
Type asset
will allow webpack to make it's own decision based on file size.
-
asset/inline
is chosen if file size is less than 8kb. -
asset/resource
is chosen if file size is greater than 8kb. -
8kb is the default setting and can be changed by using the
parser
setting as demonstrated below.
rules: [
{
test: /\.svg$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 3 * 1024 // 3kb
}
}
}
]
While Webpack includes asset loaders out of the box, any additional loaders must be installed as dependencies to the application. Multiple loaders can be included in a single rule.
Loader documentation here
css-loader
only reads and returns contents of css file.style-loader
injects css into page using style guides. Bundles it with JavaScript inbundle.js
. This is recommended fordevelopment
mode.mini-css-extract-plugin
is used to create a separate file and is used in place ofstyle-loader
inproduction
mode. Refer to the plugin section below for more details.
rules: [
{
test: /\.css$/,
use: [
'style-loader', 'css-loader'
]
}
]
Babel allows newer ECMAScript features to be transpiled into older versions. This allows new ECMAScript features to be used during development without compromising browser compatability during production.
- Important to exclude
node_modules
@babel/env
compiles ECMAScript 6 and newer to ECMAScript 5.- Babel plugins handle new features not covered by
@babel/env
Documentation for babel-loader
here.
Babel config documentation here. (Choose the webpack
option.)
Hooking Babel up to the webpack config file looks like this:
module.exports = {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [ '@babel/preset-env' ],
plugins: [ '@babel/plugin-proposal-class-properties' ]
}
}
}
]
}
Plugin/Preset order matters!
- Plugins run before presets
- Plugin ordering is first to last
- Preset ordering is reversed (last to first)
Babel's documentation for configuration here.
Babel's documentation for configuring babel.config.json
here.
Plugins serve the purpose of doing anything else that loaders can't do. Webpack provides many plugins out of the box. Plugins are added inside the plugins
array in the config file.
Webpack 5 comes with terser-webpack-plugin
out of the box, but it must be installed in order to customize the options.
terser-webpack-plugin
documentation here
- Webpack 4 and below does not come with the plugin.
- uses
terser
to minify JavaScript.
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
plugins: [
new TerserPlugin()
]
}
Create a separate CSS file rather than bundling it with the JS file like style-loader
does.
mini-css-extract-plugin
documentation here
- Recommended for
production
mode. - Builds on top of a Webpack 5 feature, thus Webpack 5 is required for this plugin to work.
- Recommended to combine
mini-css-extract-plugin
withcss-loader
. - Needs to be added to the CSS rule:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
],
},
],
}
- In addition to adding the rule, it also needs to be addded to plugins.
- Output filename can be set like so:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
],
},
],
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.css',
})
]
}
In order to keep the dist
directory clean, it's essential to remove files upon each build. This is especially important when using hashed names, because by default, files from previous builds are not removed or overwritten.
clean-webpack-plugin
solves this problem, however you can also use the clean
option in output
.
output.clean
documentation here
As of writing this, there is currently a bug with
output.clean
when usingwebpack-dev-server
ifdevServer.writeToDisk
is set totrue
.I'm using
webpack
v5.53.0 andwebpack-dev-server
v4.2.1 while writing this.There's a discussion on github regarding this issue. To check it's current status go here.
As a workaround, I am currently using
clean-webpack-plugin
in development mode for the sake of usingwebpack-dev-server
, but I'm usingoutput.clean
in my production configuration.
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin()
],
}
- Other directories can be cleaned with additional options.
- File paths are relative to the directory specified in the
webpack.config.js
path variable. - In this case, that directory is
dist
'**/*'
specifies that everything in dist should be cleaned.- To clean a directory called
build
that is outside of thedist
directory, you need the exact path like so:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
'**/*',
path.join(process.cwd(), 'build/**/*')
]
})
]
}
When [contenthash]
(see caching below) is used to generated hashed filenames, the filenames become dynamic and filepaths can no longer be hardcoded in the HTML document. To solve this problem, html-webpack-plugin
can be used to generate an HTML file that includes the correct filepaths every time a new hash is generated.
html-webpack-plugin
documentation here.
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin()
]
}
Additional options can be specified for this plugin to enable more control of the generated file.
Here's a list of options included in the plugin.
And a link to the documentation for creating and using an HTML template for even more control over the generated file
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
title: 'Hello World',
filename: html/generatedHtml.html,
meta: {
description: 'Description'
}
})
]
Browsers download assets before loading websites. Each time a user reloads a page, the browser downloads all of the files again. This is inefficient.
Browsers can cache files to save time, which solves that problem. Great! However, this introduces another problem: If you change a file without changing the name, the user's browser won't download that new file because it thinks it's the same file it currently has cached.
To solve this, use [contenthash]
, which creates a hash based on the contents of the file. Every time the file is changed, it's parsed by the hashing function and a new hash is generated. This means every time you change a file, it will have a new name, and our little caching problem is solved!
Browser Caching documentation here
HOW COOL IS THAT!?
- This is a feature included in webpack and nothing needs to be installed.
- add
[contenthash]
wherever a filename is specified, such as theoutput
object:
module.exports = {
output: {
filename: 'bundle.[contenthash].js',
}
}
- Or the minified CSS file:
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.[contenthash].css',
})
],
}
There's one problem with this: when a file is dynamically renamed with this hash, the static index.html
isn't updated with the new name. Using html-webpack-plugin
to generate a new html template that includes the correct hashed filenames solves this problem.