Webpack Guide
Webpack Guide
Guides
This section contains guides for understanding and mastering the wide variety of tools and features that webpack offers. The first is a guide
that takes you through getting started.
The guides get more advanced as you go on. Most serve as a starting point, and once completed you should feel more comfortable diving into
the actual documentation.
Warning
The output shown from running webpack in the guides may differ slightly from the output of newer versions. This is to be expected. As
long as the bundles look similar and run correctly, then there shouldn't be any issues. If you do come across an example that seems to be
broken by a new version, please create an issue and we will do our best to resolve the discrepancy.
Getting Started
Webpack is used to compile JavaScript modules. Once installed, you can interact with webpack either from its CLI or API. If you're still new to
webpack, please read through the core concepts and this comparison to learn why you might use it over the other tools that are out in the
community.
Warning
Live Preview
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
Throughout the Guides we will use diff blocks to show you what changes we're making to directories, files, and code. For instance:
Now we'll create the following directory structure, files and their contents:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- index.html
+ |- /src
+ |- index.js
src/index.js
function component() {
const element = document.createElement('div');
// Lodash, currently included via a script, is required for this line to work
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
We also need to adjust our package.json file in order to make sure we mark our package as private , as well as removing the main
entry. This is to prevent an accidental publish of your code.
Tip
If you want to learn more about the inner workings of package.json , then we recommend reading the npm documentation.
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
- "main": "index.js",
+ "private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2",
}
}
In this example, there are implicit dependencies between the <script> tags. Our index.js file depends on lodash being included in
the page before it runs. This is because index.js never explicitly declared a need for lodash ; it assumes that the global variable _ exists.
If a dependency is missing, or included in the wrong order, the application will not function properly.
If a dependency is included but not used, the browser will be forced to download unnecessary code.
Creating a Bundle
First we'll tweak our directory structure slightly, separating the "source" code ( ./src ) from our "distribution" code ( ./dist ). The "source"
code is the code that we'll write and edit. The "distribution" code is the minimized and optimized output of our build process that will
eventually be loaded in the browser. Tweak the directory structure as follows:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- /dist
+ |- index.html
- |- index.html
|- /src
|- index.js
Tip
You may have noticed that index.html was created manually, even though it is now placed in the dist directory. Later on in another
guide, we will generate index.html rather than edit it manually. Once this is done, it should be safe to empty the dist directory and
to regenerate all the files within it.
To bundle the lodash dependency with index.js , we'll need to install the library locally:
Tip
When installing a package that will be bundled into your production bundle, you should use npm install --save . If you're installing
a package for development purposes (e.g. a linter, testing libraries, etc.) then you should use npm install --save-dev . More
information can be found in the npm documentation.
- // Lodash, currently included via a script, is required for this line to work
+ // Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Now, since we'll be bundling our scripts, we have to update our index.html file. Let's remove the lodash <script> , as we now import
it, and modify the other <script> tag to load the bundle, instead of the raw ./src file:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
- <script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
- <script src="./src/index.js"></script>
+ <script src="main.js"></script>
</body>
</html>
In this setup, index.js explicitly requires lodash to be present, and binds it as _ (no global scope pollution). By stating what
dependencies a module needs, webpack can use this information to build a dependency graph. It then uses the graph to generate an
optimized bundle where scripts will be executed in the correct order.
With that said, let's run npx webpack , which will take our script at src/index.js as the entry point, and will generate dist/main.js
as the output. The npx command, which ships with Node 8.2/npm 5.2.0 or higher, runs the webpack binary
( ./node_modules/.bin/webpack ) of the webpack package we installed in the beginning:
$ npx webpack
[webpack-cli] Compilation finished
asset main.js 69.3 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1851 ms
Tip
Your output may vary a bit, but if the build is successful then you are good to go.
Open index.html from the dist directory in your browser and, if everything went right, you should see the following text: 'Hello
webpack' .
Modules
The import and export statements have been standardized in ES2015. They are supported in most of the browsers at this moment,
however there are some browsers that don't recognize the new syntax. But don't worry, webpack does support them out of the box.
Behind the scenes, webpack actually "transpiles" the code so that older browsers can also run it. If you inspect dist/main.js , you might
be able to see how webpack does this, it's quite ingenious! Besides import and export , webpack supports various other module syntaxes
as well, see Module API for more information.
Note that webpack will not alter any code other than import and export statements. If you are using other ES2015 features, make sure to
use a transpiler such as Babel via webpack's loader system.
Using a Configuration
As of version 4, webpack doesn't require any configuration, but most projects will need a more complex setup, which is why webpack supports
a configuration file. This is much more efficient than having to manually type in a lot of commands in the terminal, so let's create one:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
Now, let's run the build again but instead using our new configuration file:
$ npx webpack --config webpack.config.js
[webpack-cli] Compilation finished
asset main.js 69.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1934 ms
Tip
If a webpack.config.js is present, the webpack command picks it up by default. We use the --config option here only to show
that you can pass a configuration of any name. This will be useful for more complex configurations that need to be split into multiple files.
A configuration file allows far more flexibility than CLI usage. We can specify loader rules, plugins, resolve options and many other
enhancements this way. See the configuration documentation to learn more.
NPM Scripts
Given it's not particularly fun to run a local copy of webpack from the CLI, we can set up a little shortcut. Let's adjust our package.json by
adding an npm script:
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
Now the npm run build command can be used in place of the npx command we used earlier. Note that within scripts we can
reference locally installed npm packages by name the same way we did with npx . This convention is the standard in most npm-based
projects because it allows all contributors to use the same set of common scripts.
Now run the following command and see if your script alias works:
...
Tip
Custom parameters can be passed to webpack by adding two dashes between the npm run build command and your parameters,
e.g. npm run build -- --color .
Conclusion
Now that you have a basic build together you should move on to the next guide Asset Management to learn how to manage assets like
images and fonts with webpack. At this point, your project should look like this:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- main.js
|- index.html
|- /src
|- index.js
|- /node_modules
Warning
Do not compile untrusted code with webpack. It could lead to execution of malicious code on your computer, remote servers, or in the
Web browsers of the end users of your application.
If you want to learn more about webpack's design, you can check out the basic concepts and configuration pages. Furthermore, the API section
digs into the various interfaces webpack offers.
Asset Management
If you've been following the guides from the start, you will now have a small project that shows "Hello webpack". Now let's try to incorporate
some other assets, like images, to see how they can be handled.
Prior to webpack, front-end developers would use tools like grunt and gulp to process these assets and move them from their /src folder
into their /dist or /build directory. The same idea was used for JavaScript modules, but tools like webpack will dynamically bundle all
dependencies (creating what's known as a dependency graph). This is great because every module now explicitly states its dependencies and
we'll avoid bundling modules that aren't in use.
One of the coolest webpack features is that you can also include any other type of file, besides JavaScript, for which there is a loader or built-in
Asset Modules support. This means that the same benefits listed above for JavaScript (e.g. explicit dependencies) can be applied to everything
used in building a website or web app. Let's start with CSS, as you may already be familiar with that setup.
Setup
Let's make a minor change to our project before we get started:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
- <title>Getting Started</title>
+ <title>Asset Management</title>
</head>
<body>
- <script src="main.js"></script>
+ <script src="bundle.js"></script>
</body>
</html>
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
- filename: 'main.js',
+ filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
Loading CSS
In order to import a CSS file from within a JavaScript module, you need to install and add the style-loader and css-loader to your module
configuration:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
};
Module loaders can be chained. Each loader in the chain applies transformations to the processed resource. A chain is executed in reverse
order. The first loader passes its result (resource with applied transformations) to the next one, and so forth. Finally, webpack expects
JavaScript to be returned by the last loader in the chain.
The above order of loaders should be maintained: 'style-loader' comes first and followed by 'css-loader' . If this convention is not
followed, webpack is likely to throw errors.
Tip
webpack uses a regular expression to determine which files it should look for and serve to a specific loader. In this case, any file that ends
with .css will be served to the style-loader and the css-loader .
This enables you to import './style.css' into the file that depends on that styling. Now, when that module is run, a <style> tag with
the stringified css will be inserted into the <head> of your html file.
Let's try it out by adding a new style.css file to our project and import it in our index.js :
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- style.css
|- index.js
|- /node_modules
src/style.css
.hello {
color: red;
}
src/index.js
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
modules by path ./node_modules/ 538 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
modules by path ./src/ 965 bytes
./src/index.js + 1 modules 639 bytes [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 326 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 2231 ms
Open up dist/index.html in your browser again and you should see that Hello webpack is now styled in red. To see what webpack did,
inspect the page (don't view the page source, as it won't show you the result, because the <style> tag is dynamically created by JavaScript)
and look at the page's head tags. It should contain the style block that we imported in index.js .
Note that you can, and in most cases should, minimize css for better load times in production. On top of that, loaders exist for pretty much any
flavor of CSS you can think of – postcss, sass, and less to name a few.
Loading Images
So now we're pulling in our CSS, but what about our images like backgrounds and icons? As of webpack 5, using the built-in Asset Modules we
can easily incorporate those in our system as well:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
+ {
+ test: /\.(png|svg|jpg|jpeg|gif)$/i,
+ type: 'asset/resource',
+ },
],
},
};
Now, when you import MyImage from './my-image.png' , that image will be processed and added to your output directory and the
MyImage variable will contain the final url of that image after processing. When using the css-loader, as shown above, a similar process will
occur for url('./my-image.png') within your CSS. The loader will recognize this is a local file, and replace the './my-image.png' path
with the final path to the image in your output directory. The html-loader handles <img src="./my-image.png" /> in the same
manner.
Let's add an image to our project and see how this works, you can use any image you like:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- icon.png
|- style.css
|- index.js
|- /node_modules
src/index.js
import _ from 'lodash';
import './style.css';
+import Icon from './icon.png';
function component() {
const element = document.createElement('div');
document.body.appendChild(component());
src/style.css
.hello {
color: red;
+ background: url('./icon.png');
}
Let's create a new build and open up the index.html file again:
...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
asset bundle.js 73.4 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 540 KiB (javascript) 9.88 KiB (asset)
modules by path ./node_modules/ 539 KiB
modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB
./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
./node_modules/css-loader/dist/runtime/getUrl.js 830 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
modules by path ./src/ 1.45 KiB (javascript) 9.88 KiB (asset)
./src/index.js + 1 modules 794 bytes [built] [code generated]
./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 648 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 1972 ms
If all went well, you should now see your icon as a repeating background, as well as an img element beside our Hello webpack text. If you
inspect this element, you'll see that the actual filename has changed to something like 29822eaa871e8eadeaa4.png . This means webpack
found our file in the src folder and processed it!
Loading Fonts
So what about other assets like fonts? The Asset Modules will take any file you load through them and output it to your build directory. This
means we can use them for any kind of file, including fonts. Let's update our webpack.config.js to handle font files:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/i,
+ type: 'asset/resource',
+ },
],
},
};
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- my-font.woff
+ |- my-font.woff2
|- icon.png
|- style.css
|- index.js
|- /node_modules
With the loader configured and fonts in place, you can incorporate them via an @font-face declaration. The local url(...) directive will
be picked up by webpack, as it was with the image:
src/style.css
+@font-face {
+ font-family: 'MyFont';
+ src: url('./my-font.woff2') format('woff2'),
+ url('./my-font.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
.hello {
color: red;
+ font-family: 'MyFont';
background: url('./icon.png');
}
Now run a new build and let's see if webpack handled our fonts:
...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
assets by info 33.2 KiB [immutable]
asset 55055dbfc7c6a83f60ba.woff 18.8 KiB [emitted] [immutable] [from: src/my-font.woff] (auxiliary name: main)
asset 8f717b802eaab4d7fb94.woff2 14.5 KiB [emitted] [immutable] [from: src/my-font.woff2] (auxiliary name: main)
asset bundle.js 73.7 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 541 KiB (javascript) 43.1 KiB (asset)
javascript modules 541 KiB
modules by path ./node_modules/ 539 KiB
modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB 2 modules
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
modules by path ./src/ 1.98 KiB
./src/index.js + 1 modules 794 bytes [built] [code generated]
./node_modules/css-loader/dist/cjs.js!./src/style.css 1.21 KiB [built] [code generated]
asset modules 126 bytes (javascript) 43.1 KiB (asset)
./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
./src/my-font.woff2 42 bytes (javascript) 14.5 KiB (asset) [built] [code generated]
./src/my-font.woff 42 bytes (javascript) 18.8 KiB (asset) [built] [code generated]
webpack 5.4.0 compiled successfully in 2142 ms
Open up dist/index.html again and see if our Hello webpack text has changed to the new font. If all is well, you should see the
changes.
Loading Data
Another useful asset that can be loaded is data, like JSON files, CSVs, TSVs, and XML. Support for JSON is actually built-in, similar to NodeJS,
meaning import Data from './data.json' will work by default. To import CSVs, TSVs, and XML you could use the csv-loader and xml-
loader. Let's handle loading all three:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
+ {
+ test: /\.(csv|tsv)$/i,
+ use: ['csv-loader'],
+ },
+ {
+ test: /\.xml$/i,
+ use: ['xml-loader'],
+ },
],
},
};
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- data.xml
+ |- data.csv
|- my-font.woff
|- my-font.woff2
|- icon.png
|- style.css
|- index.js
|- /node_modules
src/data.xml
src/data.csv
to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you
Now you can import any one of those four types of data (JSON, CSV, TSV, XML) and the Data variable you import, will contain parsed JSON
for consumption:
src/index.js
function component() {
const element = document.createElement('div');
element.appendChild(myIcon);
+ console.log(Data);
+ console.log(Notes);
+
return element;
}
document.body.appendChild(component());
Re-run the npm run build command and open dist/index.html . If you look at the console in your developer tools, you should be able
to see your imported data being logged to the console!
Tip
This can be especially helpful when implementing some sort of data visualization using a tool like d3. Instead of making an ajax request
and parsing the data at runtime you can load it into your module during the build process so that the parsed data is ready to go as soon as
the module hits the browser.
Warning
Only the default export of JSON modules can be used without warning.
// No warning
import data from './data.json';
Let's say you have a data.toml , a data.yaml and a data.json5 files under src folder:
src/data.toml
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z
src/data.yaml
src/data.json5
{
// comment
title: 'JSON5 Example',
owner: {
name: 'Tom Preston-Werner',
organization: 'GitHub',
bio: 'GitHub Cofounder & CEO\n\
Likes tater tots and beer.',
dob: '1979-05-27T07:32:00.000Z',
},
}
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
+ {
+ test: /\.toml$/i,
+ type: 'json',
+ parser: {
+ parse: toml.parse,
+ },
+ },
+ {
+ test: /\.yaml$/i,
+ type: 'json',
+ parser: {
+ parse: yaml.parse,
+ },
+ },
+ {
+ test: /\.json5$/i,
+ type: 'json',
+ parser: {
+ parse: json5.parse,
+ },
+ },
],
},
};
src/index.js
function component() {
const element = document.createElement('div');
element.appendChild(myIcon);
console.log(Data);
console.log(Notes);
return element;
}
document.body.appendChild(component());
Re-run the npm run build command and open dist/index.html . You should be able to see your imported data being logged to the
console!
Global Assets
The coolest part of everything mentioned above, is that loading assets this way allows you to group modules and assets in a more intuitive
way. Instead of relying on a global /assets directory that contains everything, you can group assets with the code that uses them. For
example, a structure like this can be useful:
- |- /assets
+ |– /components
+ | |– /my-component
+ | | |– index.jsx
+ | | |– index.css
+ | | |– icon.svg
+ | | |– img.png
This setup makes your code a lot more portable as everything that is closely coupled now lives together. Let's say you want to use /my-
component in another project, copy or move it into the /components directory over there. As long as you've installed any external
dependencies and your configuration has the same loaders defined, you should be good to go.
However, let's say you're locked into your old ways or you have some assets that are shared between multiple components (views, templates,
modules, etc.). It's still possible to store these assets in a base directory and even use aliasing to make them easier to import .
Wrapping up
For the next guides we won't be using all the different assets we've used in this guide, so let's do some cleanup so we're prepared for the next
piece of the guides Output Management:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
- |- data.csv
- |- data.json5
- |- data.toml
- |- data.xml
- |- data.yaml
- |- icon.png
- |- my-font.woff
- |- my-font.woff2
- |- style.css
|- index.js
|- /node_modules
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- module: {
- rules: [
- {
- test: /\.css$/i,
- use: ['style-loader', 'css-loader'],
- },
- {
- test: /\.(png|svg|jpg|jpeg|gif)$/i,
- type: 'asset/resource',
- },
- {
- test: /\.(woff|woff2|eot|ttf|otf)$/i,
- type: 'asset/resource',
- },
- {
- test: /\.(csv|tsv)$/i,
- use: ['csv-loader'],
- },
- {
- test: /\.xml$/i,
- use: ['xml-loader'],
- },
- {
- test: /\.toml$/i,
- type: 'json',
- parser: {
- parse: toml.parse,
- },
- },
- {
- test: /\.yaml$/i,
- type: 'json',
- parser: {
- parse: yaml.parse,
- },
- },
- {
- test: /\.json5$/i,
- type: 'json',
- parser: {
- parse: json5.parse,
- },
- },
- ],
- },
};
src/index.js
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
Next guide
Let's move on to Output Management
Further Reading
Loading Fonts on SurviveJS
Output Management
Tip
This guide extends on code examples found in the Asset Management guide.
So far we've manually included all our assets in our index.html file, but as your application grows and once you start using hashes in
filenames and outputting multiple bundles, it will be difficult to keep managing your index.html file manually. However, a few plugins exist
that will make this process much easier to manage.
Preparation
First, let's adjust our project a little bit:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
src/print.js
src/index.js
function component() {
const element = document.createElement('div');
+ const btn = document.createElement('button');
document.body.appendChild(component());
Let's also update our dist/index.html file, in preparation for webpack to split out entries:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
- <title>Asset Management</title>
+ <title>Output Management</title>
+ <script src="./print.bundle.js"></script>
</head>
<body>
- <script src="bundle.js"></script>
+ <script src="./index.bundle.js"></script>
</body>
</html>
Now adjust the config. We'll be adding our src/print.js as a new entry point ( print ) and we'll change the output as well, so that it will
dynamically generate bundle names, based on the entry point names:
webpack.config.js
module.exports = {
- entry: './src/index.js',
+ entry: {
+ index: './src/index.js',
+ print: './src/print.js',
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
Let's run npm run build and see what this generates:
...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [emitted] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [emitted] [minimized] (name: print)
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 1996 ms
We can see that webpack generates our print.bundle.js and index.bundle.js files, which we also specified in our index.html file.
if you open index.html in your browser, you can see what happens when you click the button.
But what would happen if we changed the name of one of our entry points, or even added a new one? The generated bundles would be
renamed on a build, but our index.html file would still reference the old names. Let's fix that with the HtmlWebpackPlugin .
Setting up HtmlWebpackPlugin
First install the plugin and adjust the webpack.config.js file:
webpack.config.js
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Output Management',
+ }),
+ ],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
Before we do a build, you should know that the HtmlWebpackPlugin by default will generate its own index.html file, even though we
already have one in the dist/ folder. This means that it will replace our index.html file with a newly generated one. Let's see what
happens when we do an npm run build :
...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [compared for emit] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [compared for emit] [minimized] (name: print)
asset index.html 253 bytes [emitted]
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 2189 ms
If you open index.html in your code editor, you'll see that the HtmlWebpackPlugin has created an entirely new file for you and that all
the bundles are automatically added.
If you want to learn more about all the features and options that the HtmlWebpackPlugin provides, then you should read up on it on the
HtmlWebpackPlugin repo.
In general it's good practice to clean the /dist folder before each build, so that only used files will be generated. Let's take care of that with
output.clean option.
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ clean: true,
},
};
Now run an npm run build and inspect the /dist folder. If everything went well you should now only see the files generated from the
build and no more old files!
The Manifest
You might be wondering how webpack and its plugins seem to "know" what files are being generated. The answer is in the manifest that
webpack keeps to track how all the modules map to the output bundles. If you're interested in managing webpack's output in other ways,
the manifest would be a good place to start.
The manifest data can be extracted into a json file for consumption using the WebpackManifestPlugin .
We won't go through a full example of how to use this plugin within your projects, but you can read up on the concept page and the caching
guide to find out how this ties into long term caching.
Conclusion
Now that you've learned about dynamically adding bundles to your HTML, let's dive into the development guide. Or, if you want to dig into
more advanced topics, we would recommend heading over to the code splitting guide.
Development
Tip
This guide extends on code examples found in the Output Management guide.
If you've been following the guides, you should have a solid understanding of some of the webpack basics. Before we continue, let's look into
setting up a development environment to make our lives a little easier.
Warning
The tools in this guide are only meant for development, please avoid using them in production!
webpack.config.js
module.exports = {
+ mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
In order to make it easier to track down errors and warnings, JavaScript offers source maps, which map your compiled code back to your
original source code. If an error originates from b.js , the source map will tell you exactly that.
There are a lot of different options available when it comes to source maps. Be sure to check them out so you can configure them to your
needs.
For this guide, let's use the inline-source-map option, which is good for illustrative purposes (though not for production):
webpack.config.js
Now let's make sure we have something to debug, so let's create an error in our print.js file:
src/print.js
...
[webpack-cli] Compilation finished
asset index.bundle.js 1.38 MiB [emitted] (name: index)
asset print.bundle.js 6.25 KiB [emitted] (name: print)
asset index.html 272 bytes [emitted]
runtime modules 1.9 KiB 9 modules
cacheable modules 530 KiB
./src/index.js 406 bytes [built] [code generated]
./src/print.js 83 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 706 ms
Now open the resulting index.html file in your browser. Click the button and look in your console where the error is displayed. The error
should say something like this:
We can see that the error also contains a reference to the file ( print.js ) and line number (2) where the error occurred. This is great because
now we know exactly where to look in order to fix the issue.
Warning
Some text editors have a "safe write" function that might interfere with some of the following tools. Read Adjusting Your Text Editor for a
solution to these issues.
It quickly becomes a hassle to manually run npm run build every time you want to compile your code.
There are a couple of different options available in webpack that help you automatically compile your code whenever it changes:
2. webpack-dev-server
3. webpack-dev-middleware
In most cases, you probably would want to use webpack-dev-server , but let's explore all of the above options.
Let's add an npm script that will start webpack's Watch Mode:
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
Now run npm run watch from the command line and see how webpack compiles your code. You can see that it doesn't exit the command
line because the script is currently watching your files.
Now, while webpack is watching your files, let's remove the error we introduced earlier:
src/print.js
Now save your file and check the terminal window. You should see that webpack automatically recompiles the changed module!
The only downside is that you have to refresh your browser in order to see the changes. It would be much nicer if that would happen
automatically as well, so let's try webpack-dev-server which will do exactly that.
Using webpack-dev-server
The webpack-dev-server provides you with a rudimentary web server and the ability to use live reloading. Let's set it up:
Change your configuration file to tell the dev server where to look for files:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
+ devServer: {
+ static: './dist',
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
This tells webpack-dev-server to serve the files from the dist directory on localhost:8080 .
Tip
webpack-dev-server serves bundled files from the directory defined in output.path , i.e., files will be available under
http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] .
Warning
webpack-dev-server doesn't write any output files after compiling. Instead, it keeps bundle files in memory and serves them as if they
were real files mounted at the server's root path. If your page expects to find the bundle files on a different path, you can change this with
the devMiddleware.publicPath option in the dev server's configuration.
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
+ "start": "webpack serve --open",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
Now we can run npm start from the command line and we will see our browser automatically loading up our page. If you now change any
of the source files and save them, the web server will automatically reload after the code has been compiled. Give it a try!
The webpack-dev-server comes with many configurable options. Head over to the documentation to learn more.
Tip
Now that your server is working, you might want to give Hot Module Replacement a try!
Using webpack-dev-middleware
webpack-dev-middleware is a wrapper that will emit files processed by webpack to a server. This is used in webpack-dev-server
internally, however it's available as a separate package to allow more custom setups if desired. We'll take a look at an example that combines
webpack-dev-middleware with an express server.
Now we need to make some adjustments to our webpack configuration file in order to make sure the middleware will function correctly:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
+ publicPath: '/',
},
};
The publicPath will be used within our server script as well in order to make sure files are served correctly on http://localhost:3000 .
We'll specify the port number later. The next step is setting up our custom express server:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
+ |- server.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
server.js
Now add an npm script to make it a little easier to run the server:
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"start": "webpack serve --open",
+ "server": "node server.js",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"express": "^4.17.1",
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-middleware": "^4.0.2",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
Now in your terminal run npm run server , it should give you an output similar to this:
Now fire up your browser and go to http://localhost:3000 . You should see your webpack app running and functioning!
Tip
If you would like to know more about how Hot Module Replacement works, we recommend you read the Hot Module Replacement guide.
To disable this feature in some common editors, see the list below:
Sublime Text 3: Add atomic_save: 'false' to your user preferences.
JetBrains IDEs (e.g. WebStorm): Uncheck "Use safe write" in Preferences > Appearance & Behavior > System Settings .
Conclusion
Now that you've learned how to automatically compile your code and run a development server, you can check out the next guide, which will
cover Code Splitting.
Code Splitting
Tip
This guide extends the example provided in Getting Started. Please make sure you are at least familiar with the example provided there
and the Output Management chapter.
Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can
then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization which, if used
correctly, can have a major impact on load time.
Prevent Duplication: Use Entry dependencies or SplitChunksPlugin to dedupe and split chunks.
Dynamic Imports: Split code via inline function calls within modules.
Entry Points
This is by far the easiest and most intuitive way to split code. However, it is more manual and has some pitfalls we will go over. Let's take a look
at how we might split another module from the main bundle:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
webpack.config.js
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
If there are any duplicated modules between entry chunks they will be included in both bundles.
It isn't as flexible and can't be used to dynamically split code with the core application logic.
The first of these two points is definitely an issue for our example, as lodash is also imported within ./src/index.js and will thus be
duplicated in both bundles. Let's remove this duplication in next section.
Prevent Duplication
Entry dependencies
The dependOn option allows to share the modules between the chunks:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
If we're going to use multiple entry points on a single HTML page, optimization.runtimeChunk: 'single' is needed too, otherwise we
could get into trouble described here.
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
As you can see there's another runtime.bundle.js file generated besides shared.bundle.js , index.bundle.js and
another.bundle.js .
Although using multiple entry points per page is allowed in webpack, it should be avoided when possible in favor of an entry point with
multiple imports: entry: { page: ['./analytics', './app'] } . This results in a better optimization and consistent execution order
when using async script tags.
SplitChunksPlugin
The SplitChunksPlugin allows us to extract common dependencies into an existing entry chunk or an entirely new chunk. Let's use this to
de-duplicate the lodash dependency from the previous example:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
With the optimization.splitChunks configuration option in place, we should now see the duplicate dependency removed from our
index.bundle.js and another.bundle.js . The plugin should notice that we've separated lodash out to a separate chunk and
remove the dead weight from our main bundle. Let's do an npm run build to see if it worked:
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms
Here are some other useful plugins and loaders provided by the community for splitting code:
mini-css-extract-plugin : Useful for splitting CSS out from the main application.
Dynamic Imports
Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the
import() syntax that conforms to the ECMAScript proposal for dynamic imports. The legacy, webpack-specific approach is to use
require.ensure . Let's try using the first of these two approaches...
Warning
import() calls use promises internally. If you use import() with older browsers (e.g., IE 11), remember to shim Promise using a
polyfill such as es6-promise or promise-polyfill.
Before we start, let's remove the extra entry and optimization.splitChunks from our configuration in the above example as they
won't be needed for this next demonstration:
webpack.config.js
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
- another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- },
- },
};
We'll also update our project to remove the now unused files:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
Now, instead of statically importing lodash , we'll use dynamic importing to separate a chunk:
src/index.js
- return element;
+ return element;
+ })
+ .catch((error) => 'An error occurred while loading the component');
}
-document.body.appendChild(component());
+getComponent().then((component) => {
+ document.body.appendChild(component);
+});
The reason we need default is that since webpack 4, when importing a CommonJS module, the import will no longer resolve to the value of
module.exports , it will instead create an artificial namespace object for the CommonJS module. For more information on the reason
behind this, read webpack 4: import() and CommonJs.
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
./src/index.js 434 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms
As import() returns a promise, it can be used with async functions. Here's how it would simplify the code:
src/index.js
-function getComponent() {
+async function getComponent() {
const element = document.createElement('div');
+ const { default: _ } = await import('lodash');
- return import('lodash')
- .then(({ default: _ }) => {
- const element = document.createElement('div');
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
getComponent().then((component) => {
document.body.appendChild(component);
});
Tip
It is possible to provide a dynamic expression to import() when you might need to import specific module based on a computed
variable later.
Prefetching/Preloading modules
Webpack 4.6.0+ adds support for prefetching and preloading.
Using these inline directives while declaring your imports allows webpack to output “Resource Hint” which tells the browser that for:
An example of this is having a HomePage component, which renders a LoginButton component which then on demand loads a
LoginModal component after being clicked.
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
This will result in <link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page, which will instruct
the browser to prefetch in idle time the login-modal-chunk.js file.
Tip
webpack will add the prefetch hint once the parent chunk has been loaded.
A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finishes loading.
A preloaded chunk has medium priority and is instantly downloaded. A prefetched chunk is downloaded while the browser is idle.
A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
Browser support is different.
An example of this can be having a Component which always depends on a big library that should be in a separate chunk.
Let's imagine a component ChartComponent which needs a huge ChartingLibrary . It displays a LoadingIndicator when rendered
and instantly does an on demand import of ChartingLibrary :
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
When a page which uses the ChartComponent is requested, the charting-library-chunk is also requested via <link rel="preload"> .
Assuming the page-chunk is smaller and finishes faster, the page will be displayed with a LoadingIndicator , until the already requested
charting-library-chunk finishes. This will give a little load time boost since it only needs one round-trip instead of two. Especially in
high-latency environments.
Tip
Using webpackPreload incorrectly can actually hurt performance, so be careful when using it.
Bundle Analysis
Once you start splitting your code, it can be useful to analyze the output to check where modules have ended up. The official analyze tool is a
good place to start. There are some other community-supported options out there as well:
bundle-stats: Generate a bundle report(bundle size, assets, modules) and compare the results between different builds.
Next Steps
See Lazy Loading for a more concrete example of how import() can be used in a real application and Caching to learn how to split code
more effectively.
Caching
Tip
The examples in this guide stem from getting started, output management and code splitting.
So we're using webpack to bundle our modular application which yields a deployable /dist directory. Once the contents of /dist have
been deployed to a server, clients (typically browsers) will hit that server to grab the site and its assets. The last step can be time consuming,
which is why browsers use a technique called caching. This allows sites to load faster with less unnecessary network traffic. However, it can
also cause headaches when you need new code to be picked up.
This guide focuses on the configuration needed to ensure files produced by webpack compilation can remain cached unless their content has
changed.
Output Filenames
We can use the output.filename substitutions setting to define the names of our output files. Webpack provides a method of templating
the filenames using bracketed strings called substitutions. The [contenthash] substitution will add a unique hash based on the content of
an asset. When the asset's content changes, [contenthash] will change as well.
Let's get our project set up using the example from getting started with the plugins from output management, so we don't have to deal with
maintaining our index.html file manually:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- /node_modules
webpack.config.js
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Caching',
}),
],
output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
Running our build script, npm run build , with this configuration should produce the following output:
...
Asset Size Chunks Chunk Names
main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
As you can see the bundle's name now reflects its content (via the hash). If we run another build without making any changes, we'd expect that
filename to stay the same. However, if we were to run it again, we may find that this is not the case:
...
Asset Size Chunks Chunk Names
main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
This is because webpack includes certain boilerplate, specifically the runtime and manifest, in the entry chunk.
Warning
Output may differ depending on your current webpack version. Newer versions may not have all the same issues with hashing as some
older versions, but we still recommend the following steps to be safe.
Extracting Boilerplate
As we learned in code splitting, the SplitChunksPlugin can be used to split modules out into separate bundles. Webpack provides an
optimization feature to split runtime code into a separate chunk using the optimization.runtimeChunk option. Set it to single to
create a single runtime bundle for all chunks:
webpack.config.js
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
index.html 275 bytes [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
+ 1 hidden module
It's also good practice to extract third-party libraries, such as lodash or react , to a separate vendor chunk as they are less likely to
change than our local source code. This step will allow clients to request even less from the server to stay up to date. This can be done by using
the cacheGroups option of the SplitChunksPlugin demonstrated in Example 2 of SplitChunksPlugin. Lets add
optimization.splitChunks with cacheGroups with next params and build:
webpack.config.js
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
runtimeChunk: 'single',
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
},
};
We can now see that our main bundle does not contain vendor code from node_modules directory and is down in size to 240 bytes !
Module Identifiers
Let's add another module, print.js , to our project:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
print.js
src/index.js
import _ from 'lodash';
+ import Print from './print';
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
Running another build, we would expect only our main bundle's hash to change, however...
...
Asset Size Chunks Chunk Names
runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime
vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor
main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main
index.html 352 bytes [emitted]
...
... we can see that all three have. This is because each module.id is incremented based on resolving order by default. Meaning when the
order of resolving is changed, the IDs will be changed as well. To recap:
And, the runtime bundle changed because it now contains a reference to a new module.
The first and last are expected, it's the vendor hash we want to fix. Let's use optimization.moduleIds with 'deterministic' option:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
+ moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
Now, despite any new local dependencies, our vendor hash should stay consistent between builds:
...
Asset Size Chunks Chunk Names
main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...
And let's modify our src/index.js to temporarily remove that extra dependency:
src/index.js
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
...
Asset Size Chunks Chunk Names
main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...
We can see that both builds yielded 55e79e5927a639d21a1b in the vendor bundle's filename.
Conclusion
Caching can be complicated, but the benefit to application or site users makes it worth the effort. See the Further Reading section below to
learn more.
Authoring Libraries
Aside from applications, webpack can also be used to bundle JavaScript libraries. The following guide is meant for library authors looking to
streamline their bundling strategy.
Authoring a Library
Let's assume that we are writing a small library, webpack-numbers , that allows users to convert the numbers 1 through 5 from their numeric
representation to a textual one and vice-versa, e.g. 2 to 'two'.
project
+ |- webpack.config.js
+ |- package.json
+ |- /src
+ |- index.js
+ |- ref.json
Initialize the project with npm, then install webpack , webpack-cli and lodash :
npm init -y
npm install --save-dev webpack webpack-cli lodash
We install lodash as devDependencies instead of dependencies because we don't want to bundle it into our library, or our library
could be easily bloated.
src/ref.json
[
{
"num": 1,
"word": "One"
},
{
"num": 2,
"word": "Two"
},
{
"num": 3,
"word": "Three"
},
{
"num": 4,
"word": "Four"
},
{
"num": 5,
"word": "Five"
},
{
"num": 0,
"word": "Zero"
}
]
src/index.js
import _ from 'lodash';
import numRef from './ref.json';
Webpack Configuration
Let's start with this basic webpack configuration:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
},
};
In the above example, we're telling webpack to bundle src/index.js into dist/webpack-numbers.js .
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
+ library: "webpackNumbers",
},
};
We exposed the entry point as webpackNumbers so users can use it through script tag:
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
However it only works when it's referenced through script tag, it can't be used in other environments like CommonJS, AMD, Node.js, etc.
As a library author, we want it to be compatible in different environments, i.e., users should be able to consume the bundled library in multiple
ways listed below:
script tag:
<!DOCTYPE html>
<html>
...
<script src="https://example.org/webpack-numbers.js"></script>
<script>
// ...
// Global variable
webpackNumbers.wordToNum('Five');
// Property in the window object
window.webpackNumbers.wordToNum('Five');
// ...
</script>
</html>
Let's update the output.library option with its type set to 'umd' :
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
- library: 'webpackNumbers',
+ library: {
+ name: 'webpackNumbers',
+ type: 'umd',
+ },
},
};
Now webpack will bundle a library that can work with CommonJS, AMD, and script tag.
Tip
Note that the library setup is tied to the entry configuration. For most libraries, specifying a single entry point is sufficient. While
multi-part libraries are possible, it is more straightforward to expose partial exports through an index script that serves as a single entry
point. Using an array as an entry point for a library is not recommended.
Externalize Lodash
Now, if you run npx webpack , you will find that a largish bundle is created. If you inspect the file, you'll see that lodash has been bundled
along with your code. In this case, we'd prefer to treat lodash as a peer dependency. Meaning that the consumer should already have
lodash installed. Hence you would want to give up control of this external library to the consumer of your library.
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: {
name: "webpackNumbers",
type: "umd"
},
},
+ externals: {
+ lodash: {
+ commonjs: 'lodash',
+ commonjs2: 'lodash',
+ amd: 'lodash',
+ root: '_',
+ },
+ },
};
This means that your library expects a dependency named lodash to be available in the consumer's environment.
External Limitations
For libraries that use several files from a dependency:
// ...
You won't be able to exclude them from the bundle by specifying library in the externals. You'll either need to exclude them one by one or
by using a regular expression.
module.exports = {
//...
externals: [
'library/one',
'library/two',
// Everything that starts with "library/"
/^library\/.+$/,
],
};
Final Steps
Optimize your output for production by following the steps mentioned in the production guide. Let's also add the path to your generated
bundle as the package's main field in with the package.json
package.json
{
...
"main": "dist/webpack-numbers.js",
...
}
{
...
"module": "src/index.js",
...
}
The key main refers to the standard from package.json , and module to a proposal to allow the JavaScript ecosystem upgrade to use
ES2015 modules without breaking backwards compatibility.
Warning
The module property should point to a script that utilizes ES2015 module syntax but no other syntax features that aren't yet supported
by browsers or node. This enables webpack to parse the module syntax itself, allowing for lighter bundles via tree shaking if users are
only consuming certain parts of the library.
Now you can publish it as an npm package and find it at unpkg.com to distribute it to your users.
Tip
To expose stylesheets associated with your library, the MiniCssExtractPlugin should be used. Users can then consume and load
these as they would any other stylesheet.
Environment Variables
To disambiguate in your webpack.config.js between development and production builds you may use environment variables.
Tip
webpack's environment variables are different from the environment variables of operating system shells like bash and CMD.exe
The webpack command line environment option --env allows you to pass in as many environment variables as you like. Environment
variables will be made accessible in your webpack.config.js . For example, --env production or --env goal=local .
Setting up your env variable without assignment, --env production sets env.production to true by default. There are also
other syntaxes that you can use. See the webpack CLI documentation for more information.
There is one change that you will have to make to your webpack config. Typically, module.exports points to the configuration object. To
use the env variable, you must convert module.exports to a function:
webpack.config.js
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
Tip
Webpack CLI offers some built-in environment variables which you can access inside a webpack configuration.
Build Performance
This guide contains some useful tips for improving build/compilation performance.
General
The following best practices should help, whether you're running build scripts in development or production.
Stay Up to Date
Use the latest webpack version. We are always making performance improvements. The latest recommended version of webpack is:
webpack v5.65.0
Staying up-to-date with Node.js can also help with performance. On top of this, keeping your package manager (e.g. npm or yarn ) up-to-
date can also help. Newer versions create more efficient module trees and increase resolving speed.
Loaders
Apply loaders to the minimal number of modules necessary. Instead of:
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
},
],
},
};
Use the include field to only apply the loader modules that actually need to be transformed by it:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
Bootstrap
Each additional loader/plugin has a bootup time. Try to use as few tools as possible.
Resolving
The following steps can increase resolving speed:
Set resolve.cacheWithContext: false if you use custom resolving plugins, that are not context specific.
Dlls
Use the DllPlugin to move code that is changed less often into a separate compilation. This will improve the application's compilation
speed, although it does increase complexity of the build process.
Smaller = Faster
Decrease the total size of the compilation to increase build performance. Try to keep chunks small.
Worker Pool
The thread-loader can be used to offload expensive loaders to a worker pool.
Warning
Don't use too many workers, as there is a boot overhead for the Node.js runtime and the loader. Minimize the module transfers between
worker and main process. IPC is expensive.
Persistent cache
Use cache option in webpack configuration. Clear cache directory on "postinstall" in package.json .
Tip
We support yarn PnP version 3 yarn 2 berry for persistent caching.
Custom plugins/loaders
Profile them to not introduce a performance problem here.
Progress plugin
It is possible to shorten build times by removing ProgressPlugin from webpack's configuration. Keep in mind, ProgressPlugin might
not provide as much value for fast builds as well, so make sure you are leveraging the benefits of using it.
Development
The following steps are especially useful in development.
Incremental Builds
Use webpack's watch mode. Don't use other tools to watch your files and invoke webpack. The built-in watch mode will keep track of
timestamps and passes this information to the compilation for cache invalidation.
In some setups, watching falls back to polling mode. With many watched files, this can cause a lot of CPU load. In these cases, you can increase
the polling interval with watchOptions.poll .
Compile in Memory
The following utilities improve performance by compiling and serving assets in memory rather than writing to disk:
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
stats.toJson speed
Webpack 4 outputs a large amount of data with its stats.toJson() by default. Avoid retrieving portions of the stats object unless
necessary in the incremental step. webpack-dev-server after v3.1.3 contained a substantial performance fix to minimize the amount of
data retrieved from the stats object per incremental build step.
Devtool
Be aware of the performance differences between the different devtool settings.
"eval" has the best performance, but doesn't assist you for transpiled code.
The cheap-source-map variants are more performant if you can live with the slightly worse mapping quality.
Tip
Make sure the entry chunk is cheap to emit by keeping it small. The following configuration creates an additional chunk for the runtime code,
so it's cheap to generate:
module.exports = {
// ...
optimization: {
runtimeChunk: true,
},
};
module.exports = {
// ...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
};
module.exports = {
// ...
output: {
pathinfo: false,
},
};
TypeScript Loader
To improve the build time when using ts-loader , use the transpileOnly loader option. On its own, this option turns off type checking.
To gain type checking again, use the ForkTsCheckerWebpackPlugin . This speeds up TypeScript type checking and ESLint linting by
moving each to a separate process.
module.exports = {
// ...
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
};
Tip
Production
The following steps are especially useful in production.
Warning
Don't sacrifice the quality of your application for small performance gains! Keep in mind that optimization quality is, in most cases,
more important than build performance.
Source Maps
Source maps are really expensive. Do you really need them?
Babel
Minimize the number of preset/plugins
TypeScript
Use the fork-ts-checker-webpack-plugin for typechecking in a separate process.
Sass
node-sass has a bug which blocks threads from the Node.js thread pool. When using it with the thread-loader set
workerParallelJobs: 2 .
Examples
In the entry file:
// ...
__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';
// ...
Enabling CSP
Please note that CSPs are not enabled by default. A corresponding header Content-Security-Policy or meta tag <meta http-
equiv="Content-Security-Policy" ...> needs to be sent with the document to instruct the browser to enable the CSP. Here's an
example of what a CSP header including a CDN white-listed URL might look like:
For more information on CSP and nonce attribute, please refer to Further Reading section at the bottom of this page.
Trusted Types
Webpack is also capable of using Trusted Types to load dynamically constructed scripts, to adhere to CSP require-trusted-types-for
directive restrictions. See output.trustedTypes configuration option.
Development - Vagrant
If you have a more advanced project and use Vagrant to run your development environment in a Virtual Machine, you'll often want to also run
webpack in the VM.
Vagrant.configure("2") do |config|
config.vm.network :private_network, ip: "10.10.10.61"
end
Make sure to have a webpack.config.js file. If you haven't already, use this as a minimal example to get started:
module.exports = {
context: __dirname,
entry: './app.js',
};
And create an index.html file. The script tag should point to your bundle. If output.filename is not specified in the config, this will be
bundle.js .
<!DOCTYPE html>
<html>
<head>
<script src="/bundle.js" charset="utf-8"></script>
</head>
<body>
<h2>Hey!</h2>
</body>
</html>
By default, the server will only be accessible from localhost. We'll be accessing it from our host PC, so we need to change --host to allow
this.
webpack-dev-server will include a script in your bundle that connects to a WebSocket to reload when a change in any of your files occurs.
The --client-web-socket-url flag makes sure the script knows where to look for the WebSocket. The server will use port 8080 by
default, so we should also specify that here.
--watch-options-poll makes sure that webpack can detect changes in your files. By default, webpack listens to events triggered by the
filesystem, but VirtualBox has many problems with this.
The server should be accessible on http://10.10.10.61:8080 now. If you make a change in app.js , it should live reload.
server {
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
error_page 502 @start-webpack-dev-server;
}
location @start-webpack-dev-server {
default_type text/plain;
return 502 "Please start the webpack-dev-server first.";
}
}
The proxy_set_header lines are important, because they allow the WebSockets to work correctly.
This makes the server only accessible on 127.0.0.1 , which is fine because nginx takes care of making it available on your host PC.
Conclusion
We made the Vagrant box accessible from a static IP, and then made webpack-dev-server publicly accessible so it is reachable from a
browser. We then tackled a common problem that VirtualBox doesn't send out filesystem events, causing the server to not reload on file
changes.
Dependency Management
es6 modules
commonjs
amd
Example, given we have the following folder structure including .ejs files:
example_directory
│
└───template
│ │ table.ejs
│ │ table-row.ejs
│ │
│ └───directory
│ │ another.ejs
Directory: ./template
Regular expression: /^.*\.ejs$/
context module
A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular
expression. The context module contains a map which translates requests to module ids.
Example map:
{
"./table.ejs": 42,
"./table-row.ejs": 43,
"./directory/another.ejs": 44
}
The context module also contains some runtime logic to access the map.
This means dynamic requires are supported but will cause all matching modules to be included in the bundle.
require.context
You can create your own context with the require.context() function.
It allows you to pass in a directory to search, a flag indicating whether subdirectories should be searched too, and a regular expression to
match files against.
require.context(
directory,
(useSubdirectories = true),
(regExp = /^\.\/.*$/),
(mode = 'sync')
);
Examples:
Warning
This can be useful if you want to require all files in a directory or matching a pattern, Example:
function importAll(r) {
r.keys().forEach(r);
}
function importAll(r) {
r.keys().forEach((key) => (cache[key] = r(key)));
}
id is the module id of the context module. This may be useful for module.hot.accept .
Installation
This guide goes through the various methods used to install webpack.
Prerequisites
Before we begin, make sure you have a fresh version of Node.js installed. The current Long Term Support (LTS) release is an ideal starting point.
You may run into a variety of issues with the older versions as they may be missing functionality webpack and/or its related packages require.
Local Installation
The latest webpack release is:
webpack v5.65.0
To install the latest release or a specific version, run one of the following commands:
npm install --save-dev webpack
# or specific version
npm install --save-dev webpack@<version>
Tip
Whether to use --save-dev or not depends on your use cases. Say you're using webpack only for bundling, then it's suggested that
you install it with --save-dev option since you're not going to include webpack in your production build. Otherwise you can ignore --
save-dev .
If you're using webpack v4 or later and want to call webpack from the command line, you'll also need to install the CLI.
Installing locally is what we recommend for most projects. This makes it easier to upgrade projects individually when breaking changes are
introduced. Typically webpack is run via one or more npm scripts which will look for a webpack installation in your local node_modules
directory:
"scripts": {
"build": "webpack --config webpack.config.js"
}
Tip
To run the local installation of webpack you can access its binary version as node_modules/.bin/webpack . Alternatively, if you are
using npm v5.2.0 or greater, you can run npx webpack to do it.
Global Installation
The following NPM installation will make webpack available globally:
Warning
Note that this is not a recommended practice. Installing globally locks you down to a specific version of webpack and could fail in
projects that use a different version.
Bleeding Edge
If you are enthusiastic about using the latest that webpack has to offer, you can install beta versions or even directly from the webpack
repository using the following commands:
Warning
Take caution when installing these bleeding edge releases! They may still contain bugs and therefore should not be used in production.
Warning
HMR is not intended for use in production, meaning it should only be used in development. See the building for production guide for
more information.
Enabling HMR
This feature is great for productivity. All we need to do is update our webpack-dev-server configuration, and use webpack's built-in HMR plugin.
We'll also remove the entry point for print.js as it will now be consumed by the index.js module.
Tip
If you took the route of using webpack-dev-middleware instead of webpack-dev-server , please use the webpack-hot-
middleware package to enable HMR on your custom server or application.
webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
- print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ hot: true,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
- print: './src/print.js',
+ // Runtime code for hot module replacement
+ hot: 'webpack/hot/dev-server.js',
+ // Dev server client for web socket transport, hot and live reload logic
+ client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ // Dev server client for web socket transport, hot and live reload logic
+ hot: false,
+ client: false,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
+ // Plugin for hot module replacement
+ new webpack.HotModuleReplacementPlugin(),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
Tip
You can use the CLI to modify the webpack-dev-server configuration with the following command: webpack serve --hot-only .
Now let's update the index.js file so that when a change inside print.js is detected we tell webpack to accept the updated module.
index.js
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
+
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ })
+ }
Start changing the console.log statement in print.js , and you should see the following output in the browser console (don't worry
about that button.onclick = printMe output for now, we will also update that part later).
print.js
console
To enable HMR, you also need to modify your webpack configuration object to include the HMR entry points. Here's a small example of how
that might look:
dev-server.js
const config = {
mode: 'development',
entry: [
// Runtime code for hot module replacement
'webpack/hot/dev-server.js',
// Dev server client for web socket transport, hot and live reload logic
'webpack-dev-server/client/index.js?hot=true&live-reload=true',
// Your entry
'./src/index.js',
],
devtool: 'inline-source-map',
plugins: [
// Plugin for hot module replacement
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
const compiler = webpack(config);
// `hot` and `client` options are disabled because we added them manually
const server = new webpackDevServer({ hot: false, client: false }, compiler);
(async () => {
await server.start();
console.log('dev server is running');
})();
Tip
If you're using webpack-dev-middleware , check out the webpack-hot-middleware package to enable HMR on your custom dev
server.
Gotchas
Hot Module Replacement can be tricky. To show this, let's go back to our working example. If you go ahead and click the button on the example
page, you will realize the console is printing the old printMe function.
This is happening because the button's onclick event handler is still bound to the original printMe function.
To make this work with HMR we need to update that binding to the new printMe function using module.hot.accept :
index.js
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.appendChild(btn);
return element;
}
- document.body.appendChild(component());
+ let element = component(); // Store the element to re-render on print.js changes
+ document.body.appendChild(element);
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
- printMe();
+ document.body.removeChild(element);
+ element = component(); // Re-render the "component" to update the click handler
+ document.body.appendChild(element);
})
}
This is only one example, but there are many others that can easily trip people up. Luckily, there are a lot of loaders out there (some of which
are mentioned below) that will make hot module replacement much easier.
HMR with Stylesheets
Hot Module Replacement with CSS is actually fairly straightforward with the help of the style-loader . This loader uses
module.hot.accept behind the scenes to patch <style> tags when CSS dependencies are updated.
Now let's update the configuration file to make use of the loader.
webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
hot: true,
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
project
webpack-demo
| - package.json
| - webpack.config.js
| - /dist
| - bundle.js
| - /src
| - index.js
| - print.js
+ | - styles.css
styles.css
body {
background: blue;
}
index.js
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.appendChild(btn);
return element;
}
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
document.body.removeChild(element);
element = component(); // Re-render the "component" to update the click handler
document.body.appendChild(element);
})
}
Change the style on body to background: red; and you should immediately see the page's background color change without a full
refresh.
styles.css
body {
- background: blue;
+ background: red;
}
Other Code and Frameworks
There are many other loaders and examples out in the community to make HMR interact smoothly with a variety of frameworks and libraries...
Vue Loader: This loader supports HMR for vue components out of the box.
Elm Hot webpack Loader: Supports HMR for the Elm programming language.
Angular HMR: No loader necessary! A small change to your main NgModule file is all that's required to have full control over the HMR APIs.
Svelte Loader: This loader supports HMR for Svelte components out of the box.
Tip
If you know of any other loaders or plugins that help with or enhance HMR, please submit a pull request to add them to this list!
Tree Shaking
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module
syntax, i.e. import and export . The name and concept have been popularized by the ES2015 module bundler rollup.
The webpack 2 release came with built-in support for ES2015 modules (alias harmony modules) as well as unused module export detection.
The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the "sideEffects" package.json
property to denote which files in your project are "pure" and therefore safe to prune if unused.
Tip
The remainder of this guide will stem from Getting Started. If you haven't read through that guide already, please do so now.
Add a Utility
Let's add a new utility file to our project, src/math.js , that exports two functions:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- math.js
|- /node_modules
src/math.js
Set the mode configuration option to development to make sure that the bundle is not minified:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
};
With that in place, let's update our entry script to utilize one of these new methods and remove lodash for simplicity:
src/index.js
function component() {
- const element = document.createElement('div');
+ const element = document.createElement('pre');
return element;
}
document.body.appendChild(component());
Note that we did not import the square method from the src/math.js module. That function is what's known as "dead code",
meaning an unused export that should be dropped. Now let's run our npm script, npm run build , and inspect the output bundle:
dist/bundle.js (around lines 90 - 100)
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
Note the unused harmony export square comment above. If you look at the code below it, you'll notice that square is not being
imported, however, it is still included in the bundle. We'll fix that in the next section.
{
"name": "your-project",
"sideEffects": false
}
All the code noted above does not contain side effects, so we can mark the property as false to inform webpack that it can safely prune
unused exports.
Tip
A "side effect" is defined as code that performs a special behavior when imported, other than exposing one or more exports. An example
of this are polyfills, which affect the global scope and usually do not provide an export.
If your code did have some side effects though, an array can be provided instead:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
The array accepts simple glob patterns to the relevant files. It uses glob-to-regexp under the hood (Supports: * , ** , {a,b} , [a-z] ).
Patterns like *.css , which do not include a / , will be treated like **/*.css .
Tip
Note that any imported file is subject to tree shaking. This means if you use something like css-loader in your project and import a
CSS file, it needs to be added to the side effect list so it will not be unintentionally dropped in production mode:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
Finally, "sideEffects" can also be set from the module.rules configuration option.
usedExports relies on terser to detect side effects in statements. It is a difficult task in JavaScript and not as effective as straightforward
sideEffects flag. It also can't skip subtree/dependencies since the spec says that side effects need to be evaluated. While exporting
function works fine, React's Higher Order Components (HOC) are problematic in this regard.
function Button(_ref) {
// ...
}
function merge() {
var _final = {};
for (
var _len = arguments.length, objs = new Array(_len), _key = 0;
_key < _len;
_key++
) {
objs[_key] = arguments[_key];
}
return _final;
}
function withAppProvider() {
return function addProvider(WrappedComponent) {
var WithProvider =
/*#__PURE__*/
(function (_React$Component) {
// ...
return WithProvider;
})(Component);
WithProvider.contextTypes = WrappedComponent.contextTypes
? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
: polarisAppProviderContextTypes;
var FinalComponent = hoistStatics(WithProvider, WrappedComponent);
return FinalComponent;
};
}
export {
// ...,
Button$1,
};
When Button is unused you can effectively remove the export { Button$1 }; which leaves all the remaining code. So the question is
"Does this code have any side effects or can it be safely removed?". Difficult to say, especially because of this line withAppProvider()
(Button) . withAppProvider is called and the return value is also called. Are there any side effects when calling merge or
hoistStatics ? Are there side effects when assigning WithProvider.contextTypes (Setter?) or when reading
WrappedComponent.contextTypes (Getter?).
Terser actually tries to figure it out, but it doesn't know for sure in many cases. This doesn't mean that terser is not doing its job well because it
can't figure it out. It's too difficult to determine it reliably in a dynamic language like JavaScript.
But we can help terser by using the /*#__PURE__*/ annotation. It flags a statement as side effect free. So a small change would make it
possible to tree-shake the code:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
This would allow to remove this piece of code. But there are still questions with the imports which need to be included/evaluated because they
could contain side effects.
It's similar to /*#__PURE__*/ but on a module level instead of a statement level. It says ( "sideEffects" property): "If no direct export
from a module flagged with no-sideEffects is used, the bundler can skip evaluating the module for side effects.".
index.js
import './configure';
export * from './types';
export * from './components';
components/index.js
// ...
export { default as Breadcrumbs } from './Breadcrumbs';
export { default as Button, buttonFrom, buttonsFrom } from './Button';
export { default as ButtonGroup } from './ButtonGroup';
// ...
package.json
// ...
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
// ...
For import { Button } from "@shopify/polaris"; this has the following implications:
include it: include the module, evaluate it and continue analysing dependencies
skip over: don't include it, don't evaluate it but continue analysing dependencies
exclude it: don't include it, don't evaluate it and don't analyse dependencies
index.js : No direct export is used, but flagged with sideEffects -> include it
configure.js : No export is used, but flagged with sideEffects -> include it
types/index.js : No export is used, not flagged with sideEffects -> exclude it
components/index.js : No direct export is used, not flagged with sideEffects, but reexported exports are used -> skip over
components/Breadcrumbs.js : No export is used, not flagged with sideEffects -> exclude it. This also excluded all dependencies like
components/Breadcrumbs.css even if they are flagged with sideEffects.
components/Button.js : Direct export is used, not flagged with sideEffects -> include it
components/Button.css : No export is used, but flagged with sideEffects -> include it
After this optimization, other optimizations can still apply. For example: buttonFrom and buttonsFrom exports from Button.js are
unused too. usedExports optimization will pick it up and terser may be able to drop some statements from the module.
Module Concatenation also applies. So that these 4 modules plus the entry module (and probably more dependencies) can be concatenated.
index.js has no code generated in the end.
file.js
/*#__PURE__*/ double(55);
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
};
Tip
Note that the --optimize-minimize flag can be used to enable TerserPlugin as well.
With that squared away, we can run another npm run build and see if anything has changed.
Notice anything different about dist/bundle.js ? The whole bundle is now minified and mangled, but, if you look carefully, you won't see
the square function included but will see a mangled version of the cube function ( function r(e){return e*e*e}n.a=r ). With
minification and tree shaking, our bundle is now a few bytes smaller! While that may not seem like much in this contrived example, tree
shaking can yield a significant decrease in bundle size when working on larger applications with complex dependency trees.
Tip
ModuleConcatenationPlugin is needed for the tree shaking to work. It is added by mode: 'production' . If you are not using it,
remember to add the ModuleConcatenationPlugin manually.
Conclusion
What we've learned is that in order to take advantage of tree shaking, you must...
Use the production mode configuration option to enable various optimizations including minification and tree shaking.
You can imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead
code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, you have to shake the
tree, causing them to fall.
If you are interested in more ways to optimize your output, please jump to the next guide for details on building for production.
Production
In this guide, we'll dive into some of the best practices and utilities for building a production site or application.
Tip
This walkthrough stems from Tree Shaking and Development. Please ensure you are familiar with the concepts/setup introduced in those
guides before continuing on.
Setup
The goals of development and production builds differ greatly. In development, we want strong source mapping and a localhost server with live
reloading or hot module replacement. In production, our goals shift to a focus on minified bundles, lighter weight source maps, and optimized
assets to improve load time. With this logical separation at hand, we typically recommend writing separate webpack configurations for each
environment.
While we will separate the production and development specific bits out, note that we'll still maintain a "common" configuration to keep things
DRY. In order to merge these configurations together, we'll use a utility called webpack-merge . With the "common" configuration in place,
we won't have to duplicate code within the environment-specific configurations.
Let's start by installing webpack-merge and splitting out the bits we've already worked on in previous guides:
project
webpack-demo
|- package.json
|- package-lock.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
|- /dist
|- /src
|- index.js
|- math.js
|- /node_modules
webpack.common.js
webpack.dev.js
webpack.prod.js
In webpack.common.js , we now have setup our entry and output configuration and we've included any plugins that are required for
both environments. In webpack.dev.js , we've set mode to development . Also, we've added the recommended devtool for that
environment (strong source mapping), as well as our devServer configuration. Finally, in webpack.prod.js , mode is set to
production which loads TerserPlugin , which was first introduced by the tree shaking guide.
Note the use of merge() calls in the environment-specific configurations to include our common configuration in webpack.dev.js and
webpack.prod.js . The webpack-merge tool offers a variety of advanced features for merging but for our use case we won't need any of
that.
NPM Scripts
Now, let's modify our npm scripts to use the new configuration files. For the start script, which runs webpack-dev-server , we will use
webpack.dev.js , and for the build script, which runs webpack to create a production build, we will use webpack.prod.js :
package.json
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
- "start": "webpack serve --open",
+ "start": "webpack serve --open --config webpack.dev.js",
- "build": "webpack"
+ "build": "webpack --config webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^0.28.4",
"csv-loader": "^2.1.1",
"express": "^4.15.3",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.29.0",
"style-loader": "^0.18.2",
"webpack": "^4.30.0",
"webpack-dev-middleware": "^1.12.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0",
"xml-loader": "^1.2.1"
}
}
Feel free to run those scripts and see how the output changes as we continue adding to our production configuration.
webpack.prod.js
module.exports = merge(common, {
mode: 'production',
});
Tip
Technically, NODE_ENV is a system environment variable that Node.js exposes into running scripts. It is used by convention to determine
dev-vs-prod behavior by server tools, build scripts, and client-side libraries. Contrary to expectations, process.env.NODE_ENV is not
set to 'production' within the build script webpack.config.js , see #2537. Thus, conditionals like process.env.NODE_ENV
=== 'production' ? '[name].[contenthash].bundle.js' : '[name].bundle.js' within webpack configurations do not
work as expected.
If you're using a library like react , you should actually see a significant drop in bundle size after adding DefinePlugin . Also, note that any
of our local /src code can key off of this as well, so the following check would be valid:
src/index.js
function component() {
const element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
Minification
Webpack v4+ will minify your code by default in production mode .
Note that while the TerserPlugin is a great place to start for minification and being used by default, there are other options out there:
ClosureWebpackPlugin
If you decide to try another minification plugin, make sure your new choice also drops dead code as described in the tree shaking guide and
provide it as the optimization.minimizer .
Source Mapping
We encourage you to have source maps enabled in production, as they are useful for debugging as well as running benchmark tests. That said,
you should choose one with a fairly quick build speed that's recommended for production use (see devtool ). For this guide, we'll use the
source-map option in the production as opposed to the inline-source-map we used in the development:
webpack.prod.js
module.exports = merge(common, {
mode: 'production',
+ devtool: 'source-map',
});
Tip
Avoid inline-*** and eval-*** use in production as they can increase bundle size and reduce the overall performance.
Minimize CSS
It is crucial to minimize your CSS for production. Please see the Minimizing for Production section.
CLI Alternatives
Many of the options described above can be set as command line arguments. For example, optimization.minimize can be set with --
optimization-minimize , and mode can be set with --mode . Run npx webpack --help=verbose for a full list of CLI arguments.
While these shorthand methods are useful, we recommend setting these options in a webpack configuration file for more configurability.
Lazy Loading
Tip
This guide is a small follow-up to Code Splitting. If you have not yet read through that guide, please do so now.
Lazy, or "on demand", loading is a great way to optimize your site or application. This practice essentially involves splitting your code at logical
breakpoints, and then loading it once the user has done something that requires, or will require, a new block of code. This speeds up the initial
load of the application and lightens its overall weight as some blocks may never even be loaded.
Example
Let's take the example from Code Splitting and tweak it a bit to demonstrate this concept even more. The code there does cause a separate
chunk, lodash.bundle.js , to be generated and technically "lazy-loads" it as soon as the script is run. The trouble is that no user
interaction is required to load the bundle – meaning that every time the page is loaded, the request will fire. This doesn't help us too much and
will impact performance negatively.
Let's try something different. We'll add an interaction to log some text to the console when the user clicks a button. However, we'll wait to load
that code ( print.js ) until the interaction occurs for the first time. To do this we'll go back and rework the final Dynamic Imports example
from Code Splitting and leave lodash in the main chunk.
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- print.js
|- /node_modules
src/print.js
console.log(
'The print.js module has loaded! See the network tab in dev tools...'
);
src/index.js
return element;
}
- getComponent().then(component => {
- document.body.appendChild(component);
- });
+ document.body.appendChild(component());
Warning
Note that when using import() on ES6 modules you must reference the .default property as it's the actual module object that
will be returned when the promise is resolved.
Now let's run webpack and check out our new lazy-loading functionality:
...
Asset Size Chunks Chunk Names
print.bundle.js 417 bytes 0 [emitted] print
index.bundle.js 548 kB 1 [emitted] [big] index
index.html 189 bytes [emitted]
...
Frameworks
Many frameworks and libraries have their own recommendations on how this should be accomplished within their methodologies. Here are a
few examples:
ECMAScript Modules
ECMAScript Modules (ESM) is a specification for using Modules in the Web. It's supported by all modern browsers and the recommended way of
writing modular code for the Web.
Exporting
The export keyword allows to expose things from an ESM to other modules:
let a, b, other;
export { a, b, other as c };
Importing
The import keyword allows to get references to things from other modules into an ESM:
Node.js established a way of explicitly setting the module type of files by using a property in the package.json . Setting "type":
"module" in a package.json does force all files below this package.json to be ECMAScript Modules. Setting "type": "commonjs" will
instead force them to be CommonJS Modules.
{
"type": "module"
}
In addition to that, files can set the module type by using .mjs or .cjs extension. .mjs will force them to be ESM, .cjs force them to
be CommonJs.
In DataURIs using the text/javascript or application/javascript mime type will also force module type to ESM.
In addition to the module format, flagging modules as ESM also affect the resolving logic, interop logic and the available symbols in modules.
Imports in ESM are resolved more strictly. Relative requests must include a filename and file extension.
Tip
Only the "default" export can be imported from non-ESM. Named exports are not available.
Tip
Shimming
The webpack compiler can understand modules written as ES2015 modules, CommonJS or AMD. However, some third party libraries may
expect global dependencies (e.g. $ for jQuery ). The libraries might also create globals which need to be exported. These "broken modules"
are one instance where shimming comes into play.
Warning
We don't recommend using globals! The whole concept behind webpack is to allow more modular front-end development. This means
writing isolated modules that are well contained and do not rely on hidden dependencies (e.g. globals). Please use these features only
when necessary.
Another instance where shimming can be useful is when you want to polyfill browser functionality to support more users. In this case, you may
only want to deliver those polyfills to the browsers that need patching (i.e. load them on demand).
The following article will walk through both of these use cases.
Tip
For simplicity, this guide stems from the examples in Getting Started. Please make sure you are familiar with the setup there before
moving on.
Shimming Globals
Let's start with the first use case of shimming global variables. Before we do anything let's take another look at our project:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- index.html
|- /src
|- index.js
|- /node_modules
Remember that lodash package we were using? For demonstration purposes, let's say we wanted to instead provide this as a global
throughout our application. To do this, we can use ProvidePlugin .
The ProvidePlugin makes a package available as a variable in every module compiled through webpack. If webpack sees that variable
used, it will include the given package in the final bundle. Let's go ahead by removing the import statement for lodash and instead
provide it via the plugin:
src/index.js
return element;
}
document.body.appendChild(component());
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
+ }),
+ ],
};
If you encounter at least one instance of the variable _ , include the lodash package and provide it to the modules that need it.
..
We can also use the ProvidePlugin to expose a single export of a module by configuring it with an "array path" (e.g. [module, child,
...children?] ). So let's imagine we only wanted to provide the join method from lodash wherever it's invoked:
src/index.js
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.ProvidePlugin({
- _: 'lodash',
+ join: ['lodash', 'join'],
}),
],
};
This would go nicely with Tree Shaking as the rest of the lodash library should get dropped.
Granular Shimming
Some legacy modules rely on this being the window object. Let's update our index.js so this is the case:
function component() {
const element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
document.body.appendChild(component());
This becomes a problem when the module is executed in a CommonJS context where this is equal to module.exports . In this case you
can override this using the imports-loader :
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: require.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
+ },
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
Global Exports
Let's say a library creates a global variable that it expects its consumers to use. We can add a small module to our setup to demonstrate this:
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- globals.js
|- /node_modules
src/globals.js
Now, while you'd likely never do this in your own source code, you may encounter a dated library you'd like to use that contains similar code to
what's shown above. In this case, we can use exports-loader , to export that global variable as a normal module export. For instance, in
order to export file as file and helpers.parse as parse :
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: require.resolve('./src/globals.js'),
+ use:
+ 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+ },
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
Now from within our entry script (i.e. src/index.js ), we could use const { file, parse } = require('./globals.js'); and all
should work smoothly.
Loading Polyfills
Almost everything we've discussed thus far has been in relation to handling legacy packages. Let's move on to our second topic: polyfills.
There's a lot of ways to load polyfills. For example, to include the babel-polyfill we might:
src/index.js
+import 'babel-polyfill';
+
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
Tip
Note that we aren't binding the import to a variable. This is because polyfills simply run on their own, prior to the rest of the code base,
allowing us to then assume certain native functionality exists.
Note that this approach prioritizes correctness over bundle size. To be safe and robust, polyfills/shims must run before all other code, and thus
either need to load synchronously, or, all app code needs to load after all polyfills/shims load. There are many misconceptions in the
community, as well, that modern browsers "don't need" polyfills, or that polyfills/shims merely serve to add missing features - in fact, they
often repair broken implementations, even in the most modern of browsers. The best practice thus remains to unconditionally and
synchronously load all polyfills/shims, despite the bundle size cost this incurs.
If you feel that you have mitigated these concerns and wish to incur the risk of brokenness, here's one way you might do it: Let's move our
import to a new file and add the whatwg-fetch polyfill:
src/index.js
-import 'babel-polyfill';
-
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- globals.js
+ |- polyfills.js
|- /node_modules
src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';
webpack.config.js
module.exports = {
- entry: './src/index.js',
+ entry: {
+ polyfills: './src/polyfills',
+ index: './src/index.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
{
test: require.resolve('./src/globals.js'),
use:
'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
},
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
With that in place, we can add the logic to conditionally load our new polyfills.bundle.js file. How you make this decision depends on
the technologies and browsers you need to support. We'll do some testing to determine whether our polyfills are needed:
dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
+ <script>
+ const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+ if (!modernBrowser) {
+ const scriptElement = document.createElement('script');
+
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
- <script src="main.js"></script>
+ <script src="index.bundle.js"></script>
</body>
</html>
Now we can fetch some data within our entry script:
src/index.js
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+ .then((response) => response.json())
+ .then((json) => {
+ console.log(
+ "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+ );
+ console.log(json);
+ })
+ .catch((error) =>
+ console.error('Something went wrong when fetching this data: ', error)
+ );
If we run our build, another polyfills.bundle.js file will be emitted and everything should still run smoothly in the browser. Note that
this set up could likely be improved upon but it should give you a good idea of how you can provide polyfills only to the users that actually
need them.
Further Optimizations
The babel-preset-env package uses browserslist to transpile only what is not supported in your browsers matrix. This preset comes with
the useBuiltIns option, false by default, which converts your global babel-polyfill import to a more granular feature by feature
import pattern:
import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';
Node Built-Ins
Node built-ins, like process , can be polyfilled right directly from your configuration file without the use of any special loaders or plugins. See
the node configuration page for more information and examples.
Other Utilities
There are a few other tools that can help when dealing with legacy modules.
When there is no AMD/CommonJS version of the module and you want to include the dist , you can flag this module in noParse . This will
cause webpack to include the module without parsing it or resolving require() and import statements. This practice is also used to
improve the build performance.
Warning
Any feature requiring the AST, like the ProvidePlugin , will not work.
Lastly, there are some modules that support multiple module styles; e.g. a combination of AMD, CommonJS, and legacy. In most of these
cases, they first check for define and then use some quirky code to export properties. In these cases, it could help to force the CommonJS
path by setting additionalCode=var%20define%20=%20false; via the imports-loader .
TypeScript
Tip
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. In this guide we will learn how to integrate TypeScript with
webpack.
Basic Setup
First install the TypeScript compiler and loader by running:
Now we'll modify the directory structure & the configuration files:
project
webpack-demo
|- package.json
|- package-lock.json
+ |- tsconfig.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
+ |- index.ts
|- /node_modules
tsconfig.json
Let's set up a configuration to support JSX and compile TypeScript down to ES5...
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
webpack.config.js
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
This will direct webpack to enter through ./index.ts , load all .ts and .tsx files through the ts-loader , and output a bundle.js
file in our current directory.
Now lets change the import of lodash in our ./index.ts due to the fact that there is no default export present in lodash definitions.
./index.ts
function component() {
const element = document.createElement('div');
return element;
}
document.body.appendChild(component());
Tip
To make imports do this by default and keep import _ from 'lodash'; syntax in TypeScript, set
"allowSyntheticDefaultImports" : true and "esModuleInterop" : true in your tsconfig.json file. This is related to
TypeScript configuration and mentioned in our guide only for your information.
Loader
ts-loader
We use ts-loader in this guide as it makes enabling additional webpack features, such as importing other web assets, a bit easier.
Warning
ts-loader uses tsc , the TypeScript compiler, and relies on your tsconfig.json configuration. Make sure to avoid setting
module to "CommonJS", or webpack won't be able to tree-shake your code.
Note that if you're already using babel-loader to transpile your code, you can use @babel/preset-typescript and let Babel handle
both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary to ts-loader , the underlying
@babel/plugin-transform-typescript plugin does not perform any type checking.
Source Maps
To learn more about source maps, see the development guide.
To enable source maps, we must configure TypeScript to output inline source maps to our compiled JavaScript files. The following line must be
added to our TypeScript configuration:
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
}
}
Now we need to tell webpack to extract these source maps and include in our final bundle:
webpack.config.js
module.exports = {
entry: './src/index.ts',
+ devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
Client types
It's possible to use webpack specific features in your TypeScript code, such as import.meta.webpack . And webpack provides types for
them as well, just add a TypeScript reference directive to declare it:
For example if we want to install lodash we can run the following command to get the typings for it:
custom.d.ts
Here we declare a new module for SVGs by specifying any import that ends in .svg and defining the module's content as any . We could
be more explicit about it being a url by defining the type as string. The same concept applies to other assets including CSS, SCSS, JSON and
more.
Build Performance
Warning
Web Workers
As of webpack 5, you can use Web Workers without worker-loader .
Syntax
new Worker(new URL('./worker.js', import.meta.url));
The syntax was chosen to allow running code without bundler, it is also available in native ECMAScript modules in the browser.
Example
src/index.js
src/deep-thought.js
Node.js
Similar syntax is supported in Node.js (>= 12.17.0):
import { Worker } from 'worker_threads';
Note that this is only available in ESM. Worker in CommonJS syntax is not supported by either webpack or Node.js.
This guide extends on code examples found in the Output Management guide.
Progressive Web Applications (or PWAs) are web apps that deliver an experience similar to native applications. There are many things that can
contribute to that. Of these, the most significant is the ability for an app to be able to function when offline. This is achieved through the use of
a web technology called Service Workers.
This section will focus on adding an offline experience to our app. We'll achieve this using a Google project called Workbox which provides
tools that help make offline support for web apps easier to setup.
So let's test what the current experience is like using a server with more basic features. Let's use the http-server package: npm install
http-server --save-dev . We'll also amend the scripts section of our package.json to add in a start script:
package.json
{
...
"scripts": {
- "build": "webpack"
+ "build": "webpack",
+ "start": "http-server dist"
},
...
}
Note: webpack DevServer writes in-memory by default. We'll need to enable devserverdevmiddleware.writeToDisk option in order for http-
server to be able to serve files from ./dist directory.
If you haven't previously done so, run the command npm run build to build your project. Then run the command npm start . This
should produce the following output:
If you open your browser to http://localhost:8080 (i.e. http://127.0.0.1 ) you should see your webpack application being served
from the dist directory. If you stop the server and refresh, the webpack application is no longer available.
This is what we aim to change. Once we reach the end of this module we should be able to stop the server, hit refresh and still see our
application.
Adding Workbox
Let's add the Workbox webpack plugin and adjust the webpack.config.js file:
webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Progressive Web Application',
}),
+ new WorkboxPlugin.GenerateSW({
+ // these options encourage the ServiceWorkers to get in there fast
+ // and not allow any straggling "old" SWs to hang around
+ clientsClaim: true,
+ skipWaiting: true,
+ }),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
With that in place, let's see what happens when we do an npm run build :
...
Asset Size Chunks Chunk Names
app.bundle.js 545 kB 0, 1 [emitted] [big] app
print.bundle.js 2.74 kB 1 [emitted] print
index.html 254 bytes [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js 268 bytes [emitted]
service-worker.js 1 kB [emitted]
...
As you can see, we now have 2 extra files being generated; service-worker.js and the more verbose precache-
manifest.b5ca1c555e832d6fbf9462efd29d27eb.js . service-worker.js is the Service Worker file and precache-
manifest.b5ca1c555e832d6fbf9462efd29d27eb.js is a file that service-worker.js requires so it can run. Your own generated files
will likely be different; but you should have a service-worker.js file there.
So we're now at the happy point of having produced a Service Worker. What's next?
index.js
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }
Once more npm run build to build a version of the app including the registration code. Then serve it with npm start . Navigate to
http://localhost:8080 and take a look at the console. Somewhere in there you should see:
SW registered
Now to test it. Stop your server and refresh your page. If your browser supports Service Workers then you should still be looking at your
application. However, it has been served up by your Service Worker and not by the server.
Conclusion
You have built an offline app using the Workbox project. You've started the journey of turning your web app into a PWA. You may now want to
think about taking things further. A good resource to help you with that can be found here.
Public Path
The publicPath configuration option can be quite useful in a variety of scenarios. It allows you to specify the base path for all the assets
within your application.
Use Cases
There are a few use cases in real applications where this feature becomes especially neat. Essentially, every file emitted to your
output.path directory will be referenced from the output.publicPath location. This includes child chunks (created via code splitting)
and any other assets (e.g. images, fonts, etc.) that are a part of your dependency graph.
Environment Based
In development for example, we might have an assets/ folder that lives on the same level of our index page. This is fine, but what if we
wanted to host all these static assets on a CDN in production?
To approach this problem you can easily use a good old environment variable. Let's say we have a variable ASSET_PATH :
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// This makes it possible for us to safely use env vars on our code
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
On The Fly
Another possible use case is to set the publicPath on the fly. Webpack exposes a global variable called __webpack_public_path__ that
allows you to do that. In your application's entry point, you can do this:
__webpack_public_path__ = process.env.ASSET_PATH;
That's all you need. Since we're already using the DefinePlugin on our configuration, process.env.ASSET_PATH will always be defined
so we can safely do that.
Warning
Be aware that if you use ES6 module imports in your entry file the __webpack_public_path__ assignment will be done after the
imports. In such cases, you'll have to move the public path assignment to its own dedicated module and then import it on top of your
entry.js:
// entry.js
import './public-path';
import './app';
Automatic publicPath
There are chances that you don't know what the publicPath will be in advance, and webpack can handle it automatically for you by
determining the public path from variables like import.meta.url , document.currentScript , script.src or self.location .
What you need is to set output.publicPath to 'auto' :
webpack.config.js
module.exports = {
output: {
publicPath: 'auto',
},
};
Note that in cases where document.currentScript is not supported, e.g., IE browser, you will have to include a polyfill like
currentScript Polyfill .
Integrations
Let's start by clearing up a common misconception. Webpack is a module bundler like Browserify or Brunch. It is not a task runner like Make,
Grunt, or Gulp. Task runners handle automation of common development tasks such as linting, building, or testing your project. Compared to
bundlers, task runners have a higher level focus. You can still benefit from their higher level tooling while leaving the problem of bundling to
webpack.
Bundlers help you get your JavaScript and stylesheets ready for deployment, transforming them into a format that's suitable for the browser.
For example, JavaScript can be minified or split into chunks and lazy-loaded to improve performance. Bundling is one of the most important
challenges in web development, and solving it well can remove a lot of pain from the process.
The good news is that, while there is some overlap, task runners and bundlers can play well together if approached in the right way. This guide
provides a high-level overview of how webpack can be integrated into some of the more popular task runners.
NPM Scripts
Often webpack users use npm scripts as their task runner. This is a good starting point. Cross-platform support can become a problem, but
there are several workarounds for that. Many, if not most users, get by with npm scripts and various levels of webpack configuration and
tooling.
So while webpack's core focus is bundling, there are a variety of extensions that can enable you to use it for jobs typical of a task runner.
Integrating a separate tool adds complexity, so be sure to weigh the pros and cons before going forward.
Grunt
For those using Grunt, we recommend the grunt-webpack package. With grunt-webpack you can run webpack or webpack-dev-server
as a task, get access to stats within template tags, split development and production configurations and more. Start by installing grunt-
webpack as well as webpack itself if you haven't already:
Gruntfile.js
grunt.loadNpmTasks('grunt-webpack');
};
Gulp
Gulp is also a fairly straightforward integration with the help of the webpack-stream package (a.k.a. gulp-webpack ). In this case, it is
unnecessary to install webpack separately as it is a direct dependency of webpack-stream :
gulpfile.js
const gulp = require('gulp');
const webpack = require('webpack-stream');
gulp.task('default', function () {
return gulp
.src('src/entry.js')
.pipe(
webpack({
// Any configuration options...
})
)
.pipe(gulp.dest('dist/'));
});
Mocha
The mocha-webpack utility can be used for a clean integration with Mocha. The repository offers more details on the pros and cons but
essentially mocha-webpack is a simple wrapper that provides almost the same CLI as Mocha itself and provides various webpack
functionality like an improved watch mode and improved path resolution. Here is a small example of how you would install it and use it to run
a test suite (found within ./test ):
Karma
The karma-webpack package allows you to use webpack to pre-process files in Karma.
npm install --save-dev webpack karma karma-webpack
karma.conf.js
Advanced entry
home.js
home.scss
account.js
account.scss
webpack.config.js
module.exports = {
mode: process.env.NODE_ENV,
entry: {
home: ['./home.js', './home.scss'],
account: ['./account.js', './account.scss'],
},
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.scss$/,
use: [
// fallback to style-loader in development
process.env.NODE_ENV !== 'production'
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
};
Running webpack with above configuration will output into ./dist as we did not specify different output path. ./dist directory will now
contain four files:
home.js
home.css
account.js
account.css
Asset Modules
Asset Modules is a type of module that allows one to use asset files (fonts, icons, etc) without configuring additional loaders.
Prior to webpack 5 it was common to use:
Asset Modules type replaces all of these loaders by adding 4 new module types:
asset/resource emits a separate file and exports the URL. Previously achievable by using file-loader .
asset/inline exports a data URI of the asset. Previously achievable by using url-loader .
asset/source exports the source code of the asset. Previously achievable by using raw-loader .
asset automatically chooses between exporting a data URI and emitting a separate file. Previously achievable by using url-loader
with asset size limit.
When using the old assets loaders (i.e. file-loader / url-loader / raw-loader ) along with Asset Module in webpack 5, you might want
to stop Asset Module from processing your assets again as that would result in asset duplication. This can be done by setting asset's module
type to 'javascript/auto' .
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
},
],
+ type: 'javascript/auto'
},
]
},
}
To exclude assets that came from new URL calls from the asset loaders add dependency: { not: ['url'] } to the loader configuration.
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
+ dependency: { not: ['url'] },
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
],
}
}
Resource assets
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.png/,
+ type: 'asset/resource'
+ }
+ ]
+ },
};
src/index.js
All .png files will be emitted to the output directory and their paths will be injected into the bundles.
You can modify this template by setting output.assetModuleFilename in your webpack configuration:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
};
Another case to customize output filename is to emit some kind of assets to a specified directory:
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
- }
+ },
+ {
+ test: /\.html/,
+ type: 'asset/resource',
+ generator: {
+ filename: 'static/[hash][ext][query]'
+ }
+ }
]
},
};
With this configuration all the html files will be emitted into a static directory within the output directory.
Rule.generator.filename is the same as output.assetModuleFilename and works only with asset and asset/resource
module types.
Inlining assets
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
- assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
- test: /\.png/,
- type: 'asset/resource'
+ test: /\.svg/,
+ type: 'asset/inline'
- },
+ }
- {
- test: /\.html/,
- type: 'asset/resource',
- generator: {
- filename: 'static/[hash][ext][query]'
- }
- }
]
}
};
src/index.js
All .svg files will be injected into the bundles as data URI.
If you want to use a custom encoding algorithm, you may specify a custom function to encode a file content:
webpack.config.js
Source assets
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
- test: /\.svg/,
- type: 'asset/inline',
- generator: {
- dataUrl: content => {
- content = content.toString();
- return svgToMiniDataURI(content);
- }
- }
+ test: /\.txt/,
+ type: 'asset/source',
}
]
},
};
src/example.txt
Hello world
src/index.js
src/index.js
Depending on the target in your configuration, webpack would compile the above code into a different result:
// target: web
new URL(
__webpack_public_path__ + 'logo.svg',
document.baseURI || self.location.href
);
// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);
src/index.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
+ test: /\.txt/,
+ type: 'asset',
}
]
},
};
Now webpack will automatically choose between resource and inline by following a default condition: a file with size less than 8kb will
be treated as a inline module type and resource module type otherwise.
You can change this condition by setting a Rule.parser.dataUrlCondition.maxSize option on the module rule level of your webpack
configuration:
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
+ parser: {
+ dataUrlCondition: {
+ maxSize: 4 * 1024 // 4kb
+ }
+ }
}
]
},
};
It is now recommended to remove all inline loader syntax and use a resourceQuery condition to mimic the functionality of the inline syntax.
and if you'd like to exclude raw assets from being processed by other loaders, use a negative condition:
module: {
rules: [
// ...
+ {
+ test: /\.m?js$/,
+ resourceQuery: { not: [/raw/] },
+ use: [ ... ]
+ },
{
resourceQuery: /raw/,
type: 'asset/source',
}
]
},
or a oneOf list of rules. Here only the first matching rule will be applied:
module: {
rules: [
// ...
+ { oneOf: [
{
resourceQuery: /raw/,
type: 'asset/source',
},
+ {
+ test: /\.m?js$/,
+ use: [ ... ]
+ },
+ ] }
]
},
Package exports
The exports field in the package.json of a package allows to declare which module should be used when using module requests like
import "package" or import "package/sub/path" . It replaces the default implementation that returns main field resp. index.js
files for "package" and the file system lookup for "package/sub/path" .
When the exports field is specified, only these module requests are available. Any other requests will lead to a ModuleNotFound Error.
General syntax
In general the exports field should contain an object where each properties specifies a sub path of the module request. For the examples
above the following properties could be used: "." for import "package" and "./sub/path" for import "package/sub/path" .
Properties ending with a / will forward a request with this prefix to the old file system lookup algorithm. For properties ending with * , *
may take any value and any * in the property value is replaced with the taken value.
An example:
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
Alternatives
Instead of providing a single result, the package author may provide a list of results. In such a scenario this list is tried in order and the first
valid result will be used.
Note: Only the first valid result will be used, not all valid results.
Example:
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
Conditional syntax
Instead of providing results directly in the exports field, the package author may let the module system choose one based on conditions
about the environment.
In this case an object mapping conditions to results should be used. Conditions are tried in object order. Conditions that contain invalid results
are skipped. Conditions might be nested to create a logical AND. The last condition in the object might be the special "default" condition,
which is always matched.
Example:
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
The available conditions vary depending on the module system and tool used.
Abbreviation
When only a single entry ( "." ) into the package should be supported the { ".": ... } object nesting can be omitted:
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
Notes about ordering
In an object where each key is a condition, order of properties is significant. Conditions are handled in the order they are specified.
Example: { "red": "./stop.js", "green": "./drive.js" } != { "green": "./drive.js", "red": "./stop.js" } (when
both red and green conditions are set, first property will be used)
In an object where each key is a subpath, order of properties (subpaths) is not significant. More specific paths are preferred over less specific
ones.
Example: { "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" } == { "./a/b/c": "./z", "./a/b/": "./y/",
"./a/": "./x/" } (order will always be: ./a/b/c > ./a/b/ > ./a/ )
exports field is preferred over other package entry fields like main , module , browser or custom ones.
Support
Feature "." property
Supported by Node.js, webpack, rollup, esinstall, wmr
(3) The property value is ignored and property key is used as target. Effectively only allowing mappings with key and value are identical.
(4) The syntax is supported, but always the first entry is used, which makes it unusable for any practical use case.
(6) For the require condition object order is handled incorrectly. This is intentionally as wmr doesn't differ between referencing syntax.
(7) When using "exports": "./file.js" abbreviation, any request e. g. package/not-existing will resolve to that. When not using
the abbreviation, direct file access e. g. package/file.js will not lead to an error.
Conditions
Reference syntax
One of these conditions is set depending on the syntax used to reference the module:
Condition import
Description Request is issued from ESM syntax or similar.
Condition require
Description Request is issued from CommonJs/AMD syntax or similar.
Supported by Node.js, webpack, rollup, esinstall(1), wmr(1)
Condition style
Description Request is issued from a stylesheet reference.
Condition sass
Description Request is issued from a sass stylesheet reference.
Condition asset
Description Request is issued from a asset reference.
Condition script
Description Request is issued from a normal script tag without module system.
Condition module
Description All module syntax that allows to reference javascript supports ESM.
(only combined with import or require )
Condition esmodules
Description Always set by supported tools.
Supported by wmr
Condition types
Description Request is issued from typescript that is interested in type
declarations.
(1) import and require are both set independent of referencing syntax. require has always lower priority.
import
JS import() expression
JS Worklet.addModule
require
CommonJs require(...)
AMD define()
AMD require([...])
CommonJs require.resolve()
CommonJs (webpack) require.ensure([...])
style
CSS @import
asset
CSS url()
script
script should only be set when no module system is supported. When the script is preprocessed by a system supporting CommonJs it
should set require instead.
This condition should be used when looking for a javascript file that can be injected as script tag in a HTML page without additional
preprocessing.
Optimizations
The following conditions are set for various optimizations:
Condition production
Description In a production environment.
No devtooling should be included.
Supported by webpack
Condition development
Description In a development environment.
Devtooling should be included.
Supported by webpack
Note: Since production and development is not supported by everyone, no assumption should be made when none of these is set.
Target environment
The following conditions are set depending on the target environment:
Condition browser
Description Code will run in a browser.
Condition electron
Description Code will run in electron.(1)
Supported by webpack
Condition worker
Description Code will run in a (Web)Worker.(1)
Supported by webpack
Condition worklet
Description Code will run in a Worklet.(1)
Supported by -
Condition node
Description Code will run in Node.js.
Condition deno
Description Code will run in Deno.
Supported by -
Condition react-native
Description Code will run in react-native.
Supported by -
(1) electron , worker and worklet comes combined with either node or browser , depending on the context.
Since there are multiple versions of each environment the following guidelines apply:
node : See engines field for compatibility.
browser : Compatible with current Spec and stage 4 proposals at time of publishing the package. Polyfilling resp. transpiling must be
handled on consumer side.
Features that are not possible to polyfill or transpile should be used carefully as it limits the possible usage.
deno : TBD
react-native : TBD
Condition webpack
Description Processed by webpack.
Supported by webpack
Sadly there is no node-js condition for Node.js as runtime. This would simplify creating exceptions for Node.js.
Conditions: Custom
The following tools support custom conditions:
Tool Node.js
Supported yes
Tool webpack
Supported yes
Tool rollup
Supported yes
Tool esinstall
Supported no
Tool wmr
Supported no
<company-name>:<condition-name>
Common patterns
All patterns are explained with a single "." entry into the package, but they can be extended from multiple entries too, by repeating the
pattern for each entry.
These pattern should be used as guide not as strict ruleset. They can be adapted to the individual packages.
exports should be written to use fallbacks for unknown future cases. default condition can be used for that.
As the future is unknown we assume an environment similar to browsers and module system similar to ESM.
Not all conditions are supported by every tool.
Depending on the package intention maybe something else makes sense and in this case the patterns should be adopted to that. Example: For
a command line tool a browser-like future and fallback doesn't make a lot of sense, and in this case node.js-like environments and fallbacks
should be used instead.
For complex use cases multiple patterns need to be combined by nesting these conditions.
{
"type": "module",
"exports": "./index.js"
}
Note: Providing only a ESM comes with restrictions for node.js. Such a package would only work in Node.js >= 14 and only when using
import . It won't work with require() .
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
Most tools get the ESM version. Node.js is an exception here. It gets a CommonJs version when using require() . This will lead to two
instances of these package when referencing it with require() and import , but that doesn't hurt as the package doesn't have state.
The module condition is used as optimization when preprocessing node-targeted code with a tool that supports ESM for require() (like a
bundler, when bundling for Node.js). For such a tool the exception is skipped. This is technically optional, but bundlers would include the
package source code twice otherwise.
You can also use the stateless pattern if you are able to isolate your package state in JSON files. JSON is consumable from CommonJs and ESM
without polluting the graph with the other module system.
Note that here stateless also means class instances are not tested with instanceof as there can be two different classes because of the
double module instantiation.
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
// wrapper.js
import cjs from './index.cjs';
In a stateful package we must ensure that the package is never instantiated twice.
This isn't a problem for most tools, but Node.js is again an exception here. For Node.js we always use the CommonJs version and expose
named exports in the ESM with a ESM wrapper.
{
"type": "commonjs",
"exports": "./index.js"
}
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
Note that despite using "type": "module" and .js for dist-bundle.js this file is not in ESM format. It should use globals to allow
direct consumption as script tag.
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
When the development condition is supported we use the version enhanced for development. Otherwise, in production or when mode is
unknown, we use the optimized version.
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"node": "./wrapper-process-env.cjs",
"default": "./index-optimized.js"
}
}
// wrapper-process-env.cjs
if (process.env.NODE_ENV !== 'development') {
module.exports = require('./index-optimized.cjs');
} else {
module.exports = require('./index-with-devtools.cjs');
}
We prefer static detection of production/development mode via the production or development condition.
Node.js allows to detection production/development mode at runtime via process.env.NODE_ENV , so we use that as fallback in Node.js.
Sync conditional importing ESM is not possible and we don't want to load the package twice, so we have to use CommonJs for the runtime
detection.
When it's not possible to detect mode we fallback to the production version.
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
Providing Node.js, browser and electron versions
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
Combining patterns
Example 1
This is an example for a package that has optimizations for production and development usage with runtime detection for process.env and
also ships a CommonJs and ESM version
{
"type": "module",
"exports": {
"node": {
"development": {
"module": "./index-with-devtools.js",
"import": "./wrapper-with-devtools.js",
"require": "./index-with-devtools.cjs"
},
"production": {
"module": "./index-optimized.js",
"import": "./wrapper-optimized.js",
"require": "./index-optimized.cjs"
},
"default": "./wrapper-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
Example 2
This is an example for a package that supports Node.js, browser and electron, has optimizations for production and development usage with
runtime detection for process.env and also ships a CommonJs and ESM version.
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
Looks complex, yes. We were already able to reduce some complexity due to a assumption we can make: Only node need a CommonJs
version and can detect production/development with process.env .
Guidelines
Avoid the default export. It's handled differently between tooling. Only use named exports.
Either use .cjs or type: "commonjs" in package.json to clearly mark source code as CommonJs. This makes it statically detectable
for tools if CommonJs or ESM is used. This is important for tools that only support ESM and no CommonJs.