SurviveJS - Webpack 5. From Apprentice To Master
SurviveJS - Webpack 5. From Apprentice To Master
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook
using lightweight tools and many iterations to get reader feedback, pivot until you
have the right book and build traction once you do.
Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii
What is webpack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii
How webpack changes the situation . . . . . . . . . . . . . . . . . . . . . . . iv
What will you learn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv
How is the book organized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv
Who is the book for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi
What are the book conventions . . . . . . . . . . . . . . . . . . . . . . . . . . vi
How is the book versioned . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
How to get support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii
Where to find additional material . . . . . . . . . . . . . . . . . . . . . . . . . viii
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
What is Webpack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . x
Webpack relies on modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . x
Webpack’s execution process . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
Webpack is configuration driven . . . . . . . . . . . . . . . . . . . . . . . . . xiv
Hot Module Replacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv
Asset hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv
Code splitting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi
Webpack 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii
CONTENTS
I Developing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1. Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1 Setting up the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Installing webpack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Running webpack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Setting up assets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Configuring mini-html-webpack-plugin . . . . . . . . . . . . . . . 6
1.6 Examining the output . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.7 Adding a build shortcut . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2. Development Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1 Webpack watch mode . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 webpack-dev-server . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 webpack-plugin-serve . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.4 Accessing development server from the network . . . . . . . . . . . 15
2.5 Polling instead of watching files . . . . . . . . . . . . . . . . . . . . . 15
2.6 Making it faster to develop webpack configuration . . . . . . . . . 16
2.7 Watching files outside of webpack’s module graph . . . . . . . . . . 17
2.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3. Composing Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.1 Possible ways to manage configuration . . . . . . . . . . . . . . . . . 18
3.2 Composing configuration by merging . . . . . . . . . . . . . . . . . 19
3.3 Setting up webpack-merge . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4 Benefits of composing configuration . . . . . . . . . . . . . . . . . . 22
3.5 Configuration layouts . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
II Styling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4. Loading Styles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.1 Loading CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.2 Setting up initial CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.3 PostCSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
CONTENTS
5. Separating CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.1 Setting up MiniCssExtractPlugin . . . . . . . . . . . . . . . . . . . . 34
5.2 Managing styles outside of JavaScript . . . . . . . . . . . . . . . . . . 36
5.3 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
7. Autoprefixing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
7.1 Setting up autoprefixing . . . . . . . . . . . . . . . . . . . . . . . . . . 45
7.2 Defining a browserslist . . . . . . . . . . . . . . . . . . . . . . . . . . 46
7.3 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
9. Loading Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
9.1 Integrating images to the project . . . . . . . . . . . . . . . . . . . . . 59
9.2 Using srcsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
CONTENTS
IV Building . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
12. Source Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
12.1 Inline source maps and separate source maps . . . . . . . . . . . . . 83
12.2 Enabling source maps . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
12.3 Source map types supported by webpack . . . . . . . . . . . . . . . . 85
12.4 Inline source map types . . . . . . . . . . . . . . . . . . . . . . . . . . 85
12.5 Separate source map types . . . . . . . . . . . . . . . . . . . . . . . . 88
12.6 Other source map options . . . . . . . . . . . . . . . . . . . . . . . . . 91
CONTENTS
V Optimizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
VI Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
23. Build Targets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
23.1 Web targets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
23.2 Node targets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
23.3 Desktop targets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
23.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
General checklist . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
Development checklist . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
Production checklist . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Appendices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Module related errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
DeprecationWarning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Glossary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Foreword
It’s a funny story how I started with webpack. Before getting addicted to JavaScript,
I also developed in Java. I tried GWT (Google Web Toolkit) in that time. GWT is a
Java-to-JavaScript Compiler, which has a great feature: code splitting¹. I liked this
feature and missed it in existing JavaScript tooling. I opened an issue² to an existing
module bundler, but it did not go forward. Webpack was born.
Somehow the Instagram frontend team discovered an early version of webpack and
started to use it for instagram.com. Pete Hunt, Facebook employee managing the
Instagram web team, gave the first significant talk about webpack³ at OSCON 2014.
The talk boosted the popularity of webpack. One of the reasons for adoption of
webpack by Instagram was code splitting.
I have been following this book since its early stages. It was once a combined React
and webpack book. It has grown since then and become a book of its own filled with
content.
Juho is an important part of the webpack documentation team for the webpack
documentation, so he knows best what complements the official documentation. He
has used this knowledge to create a book that supplies you with a deep understanding
of webpack and teaches you to use the tool to its full potential.
Tobias Koppers
¹http://www.gwtproject.org/doc/latest/DevGuideCodeSplitting.html
²https://github.com/medikoo/modules-webmake/issues/7
³https://www.youtube.com/watch?v=VkTCL6Nqm6Y
Preface
The book you are reading right now goes years back. It all started with a comment
I made on Christian Alfoni’s blog in 2014. It was when I discovered webpack and
React, and I felt there was a need for a cookbook about the topics. The work began
with a GitHub wiki in early 2015.
After a while, I realized this should become an actual book and tried pitching it
to a known publisher. As they weren’t interested yet and I felt the book needed to
happen, I started the SurviveJS effort. The book warped into “SurviveJS - Webpack
and React”, my first self-published book. It combined the two topics into one single
book.
Because a book focusing only on a single technology can stand taller, I split the book
into two separate ones in 2016. The current edition represents the webpack portion of
it, and it has grown significantly due to this greater focus. The journey has not been
a short one, but it has been possible thanks to community support and continued
interest in the topic.
During these years, webpack has transformed. Instead of relying on a single prolific
author, a core team has grown around the project and has attracted more people to
support the effort.
There is an open collective campaign⁴ to help the project to succeed financially. I am
donating 30% of the book earnings to Tobias to support the project. By supporting
the book, you help webpack development as well.
Juho Vepsäläinen
⁴https://opencollective.com/webpack
Introduction
Webpack⁵ simplifies web development by solving a fundamental problem: bundling.
It takes in various assets, such as JavaScript, CSS, and HTML, and transforms them
into a format that’s convenient to consume through a browser. Doing this well takes
a significant amount of pain away from web development.
It’s not the most accessible tool to learn due to its configuration-driven approach,
but it’s incredibly powerful. The purpose of this guide is to help you get started with
webpack and go beyond the basics.
What is webpack
Web browsers consume HTML, CSS, JavaScript, and multimedia files. As a project
grows, tracking all of these files and adapting them to different targets (e.g. browsers)
becomes too complicated to manage without help. Webpack addresses these prob-
lems. Managing complexity is one of the fundamental issues of web development,
and solving this problem well helps significantly.
Webpack isn’t the only available bundler, and a collection of different tools have
emerged. Task runners, such as Grunt and Gulp, are good examples of higher-level
tools. Often the problem is that you need to write the workflows by hand. Pushing
that issue to a bundler, such as webpack, is a step forward.
Framework specific abstractions, such as create-react-app⁶, rockpack⁷, or @angular/-
cli⁸, use webpack underneath. That said, there’s still value in understanding the tool
if you have to customize the setup.
• Developing gets you up and running with webpack. This part goes through
features such as automatic browser refresh and explains how to compose your
configuration so that it remains maintainable.
Introduction v
• Styling puts heavy emphasis on styling related topics. You will learn how to
load styles with webpack and introduce techniques such as autoprefixing into
your setup.
• Loading explains webpack’s loader definitions in detail and shows you how to
load assets such as images, fonts, and JavaScript.
• Building introduces source maps and the ideas of bundle and code splitting.
You will learn to tidy up your build.
• Optimizing pushes your build to production quality level and introduces
many smaller tweaks to make it smaller. You will learn to tune webpack for
performance.
• Output discusses webpack’s output related techniques. Despite its name, it’s
not only for the web. You see how to manage multiple page setups with
webpack, pick up the basic idea of Server-Side Rendering, and learn about
Module Federation.
• Techniques discusses several specific ideas, including dynamic loading, web
workers, internationalization, deploying your applications, and consuming npm
packages through webpack.
• Extending shows how to extend webpack with loaders and plugins.
Finally, there is a short conclusion chapter that recaps the main points of the book. It
contains checklists of techniques from this book that allow you to go through your
projects methodically.
The appendices at the end of the book cover secondary topics and sometimes dig
deeper into the main ones. You can approach them in any order you want, depending
on your interest.
The Troubleshooting appendix at the end covers what to do when webpack gives you
an error. It includes a process, so you know what to do and how to debug the problem.
When in doubt, study the appendix. If you are unsure of a term and its meaning, see
the Glossary at the end of the book.
Introduction vi
This is a tip. Often you can find auxiliary information and further refer-
ences in tips.
Especially in the early part of the book, the code is written in a tutorial form. For this
reason, the following syntax is used:
// Or combinations of both
const { MiniHtmlWebpackPlugin } = require("mini-html-webpack-plugin");
const webpack = require("webpack");
Sometimes the code assumes addition without the highlighting for insertion and
many examples of the book work without by themselves and I’ve crosslinked to
prerequisites where possible.
You’ll also see code within sentences and occasionally important terms have been
highlighted. You can find the definition of these terms at the Glossary.
The page shows you the individual commits that went to the project between the
given version range. You can also see the lines that have changed in the book.
The current version of the book is 3.0.11.
¹⁰https://survivejs.com/blog/
Introduction viii
If you post questions to Stack Overflow, tag them using survivejs. You can use the
hashtag #survivejs on Twitter for the same result.
I am available for commercial consulting. In my past work, I have helped companies
to optimize their usage of webpack. The work has an impact on both developer
experience and the end-users in the form of a more performant and optimized build.
Acknowledgments
Big thanks to Christian Alfoni²⁰ for helping me craft the first version of this book as
this inspired the entire SurviveJS effort. The text you see now is a complete rewrite.
This book wouldn’t be half as good as it is without patient editing and feedback by
my editors Jesús Rodríguez²¹, Artem Sapegin²², and Pedr Browne²³. Thank you.
This book wouldn’t have been possible without the original “SurviveJS - Webpack
and React” effort. Anyone who contributed to it deserves my thanks. You can check
that book for more accurate attributions.
Thanks to Mike “Pomax” Kamermans, Cesar Andreu, Dan Palmer, Viktor Jančík, Tom
Byrer, Christian Hettlage, David A. Lee, Alexandar Castaneda, Marcel Olszewski,
Steve Schwartz, Chris Sanders, Charles Ju, Aditya Bhardwaj, Rasheed Bustamam,
José Menor, Ben Gale, Jake Goulding, Andrew Ferk, gabo, Giang Nguyen, @Coaxial,
@khronic, Henrik Raitasola, Gavin Orland, David Riccitelli, Stephen Wright, Majky
Bašista, Gunnari Auvinen, Jón Levy, Alexander Zaytsev, Richard Muller, Ava Mallory
(Fiverr), Sun Zheng’ an, Nancy (Fiverr), Aluan Haddad, Steve Mao, Craig McKenna,
Tobias Koppers, Stefan Frede, Vladimir Grenaderov, Scott Thompson, Rafael De Leon,
Gil Forcada Codinachs, Jason Aller, @pikeshawn, Stephan Klinger, Daniel Carral,
Nick Yianilos, Stephen Bolton, Felipe Reis, Rodolfo Rodriguez, Vicky Koblinski, Pyotr
Ermishkin, Ken Gregory, Dmitry Kaminski, John Darryl Pelingo, Brian Cui, @st-
sloth, Nathan Klatt, Muhamadamin Ibragimov, Kema Akpala, Roberto Fuentes, Eric
Johnson, Luca Poldelmengo, Giovanni Iembo, Dmitry Anderson , Douglas Cerna,
Chris Blossom, Bill Fienberg, Andrey Bushman, Andrew Staroscik, Cezar Neaga, Eric
Hill, Jay Somedon, Luca Fagioli, @cdoublev, Boas Mollig, Shahin Sheidaei, Stefan
Frede, Dennis Weiershäuser, Tommy-Pepsi Gaudreau, Andrea Maschio, Kusal KC,
PrinceRajRoy, Cody Casey, Kahlil Hodgson, Fahad Amin Shovon, Justin Wen, Rajiv
Seelam, Steve Higham, and many others who have contributed direct feedback for
this book!
²⁰http://www.christianalfoni.com/
²¹https://github.com/Foxandxss
²²https://github.com/sapegin
²³https://github.com/Undistraction
What is Webpack
Webpack is a module bundler. Webpack can take care of bundling alongside a
separate task runner. However, the line between bundler and task runner has become
blurred thanks to community-developed webpack plugins. Sometimes these plugins
are used to perform tasks that are usually done outside of webpack, such as cleaning
the build directory or deploying the build although you can defer these tasks outside
of webpack.
React, and Hot Module Replacement (HMR) helped to popularize webpack and led
to its usage in other environments, such as Ruby on Rails²⁴. Despite its name, webpack
is not limited to the web alone. It can bundle with other targets as well, as discussed
in the Build Targets chapter.
If you want to understand build tools and their history in better detail,
check out the Comparison of Build Tools appendix.
Webpack supports ES2015, CommonJS, MJS, and AMD module formats out of the
box. There’s also support for WebAssembly²⁵, a new way of running low-level code
in the browser. The loader mechanism works for CSS as well, with @import and
url() support through css-loader. You can find plugins for specific tasks, such as
minification, internationalization, HMR, and so on.
Webpack begins its work from entries. Often these are JavaScript modules where
webpack begins its traversal process. During this process, webpack evaluates entry
matches against loader configurations that tell webpack how to transform each
match.
²⁵https://developer.mozilla.org/en-US/docs/WebAssembly
What is Webpack xii
Resolution process
An entry itself is a module and when webpack encounters one, it tries to match the
module against the file system using the resolve configuration. For example, you
can tell webpack to perform the lookup against specific directories in addition to
node_modules.
It’s possible to adjust the way webpack matches against file extensions, and
you can define specific aliases for directories. The Consuming Packages
chapter covers these ideas in greater detail.
If the resolution pass failed, webpack will raise a runtime error. If webpack managed
to resolve a file, webpack performs processing over the matched file based on the
loader definition. Each loader applies a specific transformation against the module
contents.
The way a loader gets matched against a resolved file can be configured in multiple
ways, including by file type and by location within the file system. Webpack’s
flexibility even allows you to apply a specific transformation to a file based on where
it was imported into the project.
The same resolution process is performed against webpack’s loaders. Webpack allows
you to apply similar logic when determining which loader it should use. Loaders
have resolve configurations of their own for this reason. If webpack fails to perform
a loader lookup, it will raise a runtime error.
²⁶https://webpack.js.org/configuration/experiments/#experiments
²⁷https://www.npmjs.com/package/enhanced-resolve
What is Webpack xiii
Evaluation process
Assuming all loaders were found, webpack evaluates the matched loaders from
bottom to top and right to left (styleLoader(cssLoader('./main.css'))) while
running the module through each loader in turn. As a result, you get output which
webpack will inject in the resulting bundle. The Loader Definitions chapter covers
the topic in detail.
If loader evaluation completed without a runtime error, webpack includes the source
in the bundle. Although loaders can do a lot, they don’t provide enough power for
advanced tasks. Plugins can intercept runtime events supplied by webpack.
A good example is bundle extraction performed by the MiniCssExtractPlugin which,
when used with a loader, extracts CSS files out of the bundle and into a separate file.
Without this step, CSS would be inlined in the resulting JavaScript, as webpack treats
all code as JavaScript by default. The extraction idea is discussed in the Separating
CSS chapter.
What is Webpack xiv
Finishing
After every module has been evaluated, webpack writes output. The output is a small
runtime that executes the result in a browser and a manifest listing bundles to load.
The runtime can be extracted to a file of its own, as discussed later in the book.
That’s not all there is to the bundling process. For example, you can define specific
split points where webpack generates separate bundles that are loaded based on
application logic. This idea is discussed in the Code Splitting chapter.
module.exports = {
entry: { app: "./entry.js" }, // Start bundling
output: {
path: path.join(__dirname, "dist"), // Output to dist directory
filename: "[name].js", // Emit app.js by capturing entry name
},
// Resolve encountered imports
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
{ test: /\.js$/, use: "swc-loader", exclude: /node_modules/ },
],
},
// Perform additional processing
plugins: [new webpack.DefinePlugin({ HELLO: "hello" })],
// Adjust module resolution algorithm
resolve: { alias: { react: "preact-compat" } },
};
What is Webpack xv
Webpack’s configuration model can feel a bit opaque at times as the configuration
file can appear monolithic and it can be difficult to understand what webpack is
doing unless you know the ideas behind it. The book exists to make the concepts and
ideas to address this problem.
Often webpack’s property definitions are flexible and it’s the best to look at
either the documentation or TypeScript definitions to see what’s allowed.
For example, entry can be a function and an asynchronous one even. At
times, there are multiple ways to achieve the same, especially with loaders.
Webpack’s plugins are registered from top to bottom but loaders follow the
opposite rule. That means if you add a loader definition after the existing
ones and it matches the same test, it will be evaluated first. See the Loader
Definitions chapter to understand the different possibilities better.
Asset hashing
With webpack, you can inject a hash to each bundle name (e.g., app.d587bbd6.js) to
invalidate bundles on the client side as changes are made. Bundle splitting allows
the client to reload only a small part of the data in the ideal case.
²⁸http://livereload.com/
²⁹http://www.browsersync.io/
What is Webpack xvi
Code splitting
In addition to HMR, webpack’s bundling capabilities are extensive. Webpack allows
you to split code in various ways. You can even load code dynamically as your
application gets executed. This sort of lazy loading comes in handy, especially for
broader applications, as dependencies can be loaded on the fly as needed.
Even small applications can benefit from code splitting, as it allows the users to get
something usable in their hands faster. Performance is a feature, after all. Knowing
the basic techniques is worthwhile.
Webpack 5
Webpack 5 is a new version of the tool that promises the following changes:
Webpack 5 release post³⁰ lists all the major changes. Apart from the caching
improvements and Module Federation, it can be considered a clean up release.
There’s an official migration guide³¹ that lists all of the changes that have to be done
to port a project from webpack 4 to 5.
It’s possible that a project will run without any changes to the configuration but that
you’ll receive deprecation warnings. To find out where they are coming from, use
node --trace-deprecation node_modules/webpack/bin/webpack.js when running
webpack.
³⁰https://webpack.js.org/blog/2020-10-10-webpack-5-release/
³¹https://webpack.js.org/migrate/5/
What is Webpack xvii
Conclusion
Webpack comes with a significant learning curve. However, it’s a tool worth learning,
given how much time and effort it can save over the long term. To get a better idea
how it compares to others, check out the Comparison of Build Tools appendix.
Webpack won’t solve everything. However, it does solve the problem of bundling.
That’s one less worry during development.
To summarize:
• Webpack is a module bundler, but you can also use it running tasks as well.
• Webpack relies on a dependency graph underneath. Webpack traverses through
the source to construct the graph, and it uses this information and configuration
to generate bundles.
• Webpack relies on loaders and plugins. Loaders operate on a module level,
while plugins rely on hooks provided by webpack and have the best access to
its execution process.
• Webpack’s configuration describes how to transform assets of the graphs and
what kind of output it should generate. Part of this information can be included
in the source itself if features like code splitting are used.
• Hot Module Replacement (HMR) helped to popularize webpack. It’s a feature
that can enhance the development experience by updating code in the browser
without needing a full page refresh.
• Webpack can generate hashes for filenames allowing you to invalidate past
bundles as their contents change.
In the next part of the book, you’ll learn to construct a development configuration
using webpack while learning more about its basic concepts.
I Developing
In this part, you get up and running with webpack. You will learn to configure
webpack-plugin-serve. Finally, you compose the configuration so that it’s possible
to expand in the following parts of the book.
1. Getting Started
Before getting started, make sure you are using a recent version of Node¹. You should
use at least the most current LTS (long-term support) version as the configuration of
the book has been written with modern Node features in mind.
You should have node and npm (or yarn) commands available at your terminal. To get
a more controlled environment, use Docker², nvm³, or a similar tool.
mkdir webpack-demo
cd webpack-demo
# -y generates a `package.json` with default values
# Set the defaults at ~/.npmrc
npm init -y
You can tweak the generated package.json manually to make further changes to
it even though a part of the operations modify the file automatically for you. The
official documentation explains package.json options⁵ in more detail.
¹http://nodejs.org/
²https://www.docker.com/
³https://www.npmjs.com/package/nvm
⁴https://github.com/survivejs-demos/webpack-demo
⁵https://docs.npmjs.com/files/package.json
Getting Started 3
This is an excellent chance to set up version control using Git⁶. You can
create a commit per step and tag per chapter, so it’s easier to move back
and forth if you want.
npm add is an alias for npm install. It’s used in the book as it aligns well
with Yarn and yarn add. You can use which one you prefer.
⁶https://git-scm.com/
⁷https://www.npmjs.com/package/webpack-nano
⁸https://www.npmjs.com/package/webpack-cli
Getting Started 4
$ node_modules/.bin/wp
� webpack: Build Finished
� webpack: assets by status 0 bytes [cached] 1 asset
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'product\
ion' for this value. Set 'mode' option to 'development' or 'production'\
to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn \
more: https://webpack.js.org/configuration/mode/
ERROR in main
Module not found: Error: Can't resolve './src' in 'webpack-demo'
The output tells that webpack cannot find the source to compile. Ideally we would
pass mode parameter to it as well to define which defaults we want.
To make webpack compile, do the following:
You can display the exact path of the executables using npm bin. Most likely
it points to ./node_modules/.bin.
We also have to modify the original file to import the new file and render the
application to the DOM:
src/index.js
document.body.appendChild(component());
module.exports = {
mode,
plugins: [
new MiniHtmlWebpackPlugin({ context: { title: "Demo" } }),
],
};
⁹https://www.npmjs.com/package/mini-html-webpack-plugin
¹⁰https://www.npmjs.com/package/html-webpack-plugin
Getting Started 7
1. Build the project using node_modules/.bin/wp --mode production. You can try
the development and none modes too.
2. Run a static file server using npx serve dist or a similar command you are
familiar with.
The none mode doesn’t apply any defaults. Use it for debugging.
npx is installed with npm and could be used to run npm packages without
installation, as well as to run locally installed packages.
Hello world
Webpack has default configuration for its entries and output. It looks for
source from ./src by default and it emits output to ./dist. You can
control these through entry and output respectively as seen in the What is
Webpack chapter.
Getting Started 8
Starting from webpack 5, the output has been simplified and it’s largely self-
explanatory. The default output has improved as well as you can see by studying
dist/main.js. Earlier it contained an entire webpack runtime but starting from
webpack 5, the tool is able to optimize the result to a minimum required.
{
"scripts": {
"build": "wp --mode production"
}
}
Run npm run build to see the same output as before. npm adds node_modules/.bin
temporarily to the path enabling this. As a result, rather than having to write "build":
"node_modules/.bin/wp", you can do "build": "wp".
You can execute this kind of scripts through npm run and you can use the command
anywhere within your project. If you run the command without any parameters (npm
run), it gives you the listing of available scripts.
Getting Started 9
1.8 Conclusion
Even though you have managed to get webpack up and running, it does not do that
much yet. Developing against it would be painful as we would have to recompile
all the time. That’s where webpack’s more advanced features we explore in the next
chapters come in.
To recap:
• It’s a good idea to use a locally installed version of webpack over a globally
installed one. This way you can be sure of what version you are using. The
local dependency also works in a Continuous Integration environment.
• Webpack provides a command line interface through the webpack-cli package.
You can use it even without configuration, but any advanced usage requires a
config file. webpack-nano is a good alternative for basic usage.
• To write more complicated setups, you most likely have to write a separate
webpack.config.js file.
• mini-html-webpack-plugin and html-webpack-plugin can be used to gener-
ate an HTML entry point to your application. In the Multiple Pages chapter you
will see how to generate multiple separate pages using these plugins.
• It’s handy to use npm scripts in package.json to manage webpack. You can use
them as a light task runner and use system features outside of webpack.
In the next chapter, you will learn how to improve the developer experience by
enabling automatic browser refresh.
¹¹https://www.npmjs.com/package/concurrently
2. Development Server
When developing a frontend without any special tooling, you often end up having
to refresh the browser to see changes. Given this gets annoying fast, there’s tooling
to remedy the problem.
The first tools on the market were LiveReload¹ and Browsersync². The point of either
is to allow refreshing the browser automatically as you develop. They also pick up
CSS changes and apply the new style without a hard refresh that loses the state of
the browser.
It’s possible to setup Browsersync to work with webpack through browser-sync-
webpack-plugin³, but webpack has more tricks in store in the form of a watch mode,
and a development server.
¹http://livereload.com/
²http://www.browsersync.io/
³https://www.npmjs.com/package/browser-sync-webpack-plugin
Development Server 11
2.2 webpack-dev-server
webpack-dev-server⁴ (WDS) is the officially maintained development server running
in-memory, meaning the bundle contents aren’t written out to files but stored in
memory. The distinction is vital when trying to debug code and styles.
If you go with WDS, there are a couple of relevant fields that you should be aware
of:
To integrate with another server, it’s possible to emit files from WDS to the
file system by setting devServer.writeToDisk property to true.
You should use WDS strictly for development. If you want to host your
application, consider other solutions, such as Apache or Nginx.
⁴https://www.npmjs.com/package/webpack-dev-server
Development Server 12
2.3 webpack-plugin-serve
webpack-plugin-serve⁵ (WPS) is a third-party plugin that wraps the logic required to
update the browser into a webpack plugin. Underneath it relies on webpack’s watch
mode, and it builds on top of that while implementing Hot Module Replacement
(HMR) and other features seen in WDS.
WPS also supports webpack’s multi-compiler mode (i.e., when you give an array of
configurations to it) and a status overlay.
Given webpack’s watch mode emits to the file system by default, WPS provides an
option for webpack-plugin-ramdisk⁶ to write to the RAM instead. Using the option
improves performance while avoiding excessive writes to the file system.
To integrate WPS to the project, define an npm script for launching it:
package.json
{
"scripts": {
"start": "wp --mode development",
}
}
⁵https://www.npmjs.com/package/webpack-plugin-serve
⁶https://www.npmjs.com/package/webpack-plugin-ramdisk
Development Server 13
In addition, WPS has to be connected to webpack configuration. In this case we’ll run
it in liveReload mode and refresh the browser on changes. We’ll make it possible to
change the port by passing an environmental variable, like PORT=3000 npm start:
webpack.config.js
module.exports = {
watch: mode === "development",
entry: ["./src", "webpack-plugin-serve/client"],
mode,
plugins: [
new MiniHtmlWebpackPlugin({ context: { title: "Demo" } }),
new WebpackPluginServe({
port: parseInt(process.env.PORT, 10) || 8080,
static: "./dist",
liveReload: true,
waitForBuild: true,
}),
],
};
If you use Safari, you may have to set host: "127.0.0.1", for
WebpackPluginServe forlive reloading to work.
Development Server 14
If you execute either npm run start or npm start now, you should see something
similar to this in the terminal:
Hello world
If you try modifying the code, you should see the output in your terminal. The
browser should also perform a hard refresh so that you can see the change.
Enable the historyFallback flag if you are using HTML5 History API
based routing.
Development Server 15
module.exports = {
watchOptions: {
aggregateTimeout: 300, // Delay the first rebuild (in ms)
poll: 1000, // Poll using interval (in ms or a boolean)
ignored: /node_modules/, // Ignore to decrease CPU usage
},
};
The setup is more resource-intensive than the file watching, but it’s worth trying out
if the file watching doesn’t work for you.
⁷https://github.com/mhallin/vagrant-notify-forwarder
Development Server 16
{
"scripts": {
"watch": "nodemon --watch \"./webpack.*.*\" --exec \"npm start\"",
"start": "wp --mode development"
}
}
## Development plugins
The webpack ecosystem contains many development plugins:
2.8 Conclusion
WPS and WDS complement webpack and make it more developer-friendly. To recap:
• Webpack’s watch mode is the first step towards a better development experience.
You can have webpack compile bundles as you edit your source.
• WPS and WDS refresh the browser on change. They also implement Hot Module
Replacement.
• The default webpack watching setup can be problematic on specific systems,
where more resource-intensive polling is an alternative.
• WDS can be integrated into an existing Node server using a middleware, giving
you more control than relying on the command line interface.
• WPS and WDS do far more than refreshing and HMR. For example, proxying
allows you to connect it to other servers.
In the next chapter, you’ll learn to compose configuration so that it can be developed
further later in the book.
¹³https://www.npmjs.com/package/webpack-add-dependency-plugin
3. Composing Configuration
Even though not a lot has been done with webpack yet, the amount of configuration is
starting to feel substantial. Now you have to be careful about the way you compose it
as you have separate production and development targets in the project. The situation
can only get worse as you want to add more functionality for testing and other
purposes.
Using a single monolithic configuration file impacts comprehension and removes
any potential for reusability. As the needs of your project grow, you have to figure
out the means to manage webpack configuration more effectively.
• Maintain configuration within multiple files for each environment and point
webpack to each through the --config parameter, sharing configuration through
module imports.
• Push configuration to a library, which you then consume. Examples: webpack-
config-plugins¹, Neutrino², webpack-blocks³.
• Push configuration to a tool. Examples: create-react-app⁴, kyt⁵, nwb⁶.
• Maintain all configuration within a single file and branch there and rely on the
--mode parameter. The approach is explained in detail later in this chapter.
webpack-merge provides even more control through strategies that enable you to
control its behavior per field. They allow you to force it to append, prepend, or replace
content.
Even though webpack-merge was designed for this book, it has proven to be an
invaluable tool beyond it. You can consider it as a learning tool and pick it up in
your work if you find it handy.
⁷https://www.npmjs.org/package/webpack-merge
⁸https://www.npmjs.com/package/webpack-chain
Composing Configuration 20
To give a degree of abstraction, you can define webpack.config.js for higher level
configuration and webpack.parts.js for configuration parts to consume. Here is the
development server as a function:
webpack.parts.js
exports.devServer = () => ({
watch: true,
plugins: [
new WebpackPluginServe({
port: parseInt(process.env.PORT, 10) || 8080,
static: "./dist", // Expose if output.path changes
liveReload: true,
waitForBuild: true,
}),
],
});
For the sake of simplicity, we’ll develop all of the configuration using
JavaScript. It would be possible to use TypeScript here as well. If you
want to go that route, see the Loading JavaScript chapter for the required
TypeScript setup.
Composing Configuration 21
module.exports = getConfig(mode);
After these changes, the build should behave the same way as before. This time,
however, you have room to expand, and you don’t have to worry about how to
combine different parts of the configuration.
Composing Configuration 22
You can add more targets by expanding the package.json definition and branching at
webpack.config.js based on the need. webpack.parts.js grows to contain specific
techniques you can then use to compose the configuration.
Webpack does not set global NODE_ENV⁹ based on mode by default. If you
have any external tooling, such as Babel, relying on it, make sure to set it
explicitly. To do this, set process.env.NODE_ENV = mode; within getConfig.
• Splitting configuration into smaller functions lets you keep on expanding the
setup.
• You can type the functions assuming you are using a language such as
TypeScript.
• If you consume the configuration across multiple projects, you can publish
the configuration as a package and then have only one place to optimize and
upgrade as the underlying configuration changes. SurviveJS - Maintenance¹⁰
covers practices related to the approach.
• Treating configuration as a package allows you to version it as any other and
deliver change logs to document the changes to the consumers.
• Taken far enough, you can end up with your own create-react-app that can be
used to bootstrap projects quickly with your preferred setup.
.
└── config
├── webpack.common.js
├── webpack.development.js
├── webpack.parts.js
└── webpack.production.js
In this case, you would point to the targets through webpack --config parameter and
merge common configuration through module.exports = merge(common, config);.
.
└── config
├── parts
│ ├── devserver.js
...
│ ├── index.js
│ └── javascript.js
└── ...
• It can make sense to develop the package using TypeScript to document the
interface well. It’s particularly useful if you are authoring your configuration
in TypeScript as discussed in the Loading JavaScript chapter.
• Expose functions that cover only one piece of functionality at a time as it lets
you to replace a Hot Module Replacement implementation easily for example.
• Provide enough customization options through function parameters. It can be
a good idea to expose an object as that lets you mimic named parameters in
JavaScript. You can then destructure the parameters from that while combining
this with good defaults and TypeScript types.
• Include all related dependencies within the configuration package. In specific
cases you could use peerDependencies if you want that the consumer is able
to control specific versions. Doing this means you’ll likely download more
dependencies that you would need but it’s a good compromise.
• For parameters that have a loader string within them, use require.resolve to
resolve against a loader within the configuration package. Otherwise the build
can fail as it’s looking into the wrong place for the loaders.
• When wrapping loaders, use the associated TypeScript type in parameters.
• Consider testing the package by using snapshots (expect().toMatchSnapshot()
in Jest) to assert output changes. See the Extending with Plugins chapters for an
example of a test harness.
Composing Configuration 25
3.6 Conclusion
Even though the configuration is technically the same as before, now you have room
to grow it through composition.
To recap:
The next parts of this book cover different techniques, and webpack.parts.js sees
a lot of action as a result. The changes to webpack.config.js, fortunately, remain
minimal.
¹¹https://www.npmjs.com/package/webpack-merge
II Styling
In this part, you will learn about styling-related concerns in detail including loading
styles, separating CSS, eliminating unused CSS, and autoprefixing.
4. Loading Styles
Webpack doesn’t handle styling out of the box, and you will have to use loaders
and plugins to allow loading style files. In this chapter, you will set up CSS with
the project and see how it works out with automatic browser refreshing. When you
make a change to the CSS webpack doesn’t have to force a full refresh. Instead, it
can patch the CSS without one.
¹https://www.npmjs.com/package/css-loader
²https://www.npmjs.com/package/style-loader
Loading Styles 28
exports.loadCSS = () => ({
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
});
Above means that files ending with .css should invoke the given loaders. Loaders
return the new source files with transformations applied on them. They can be
chained together like a pipe in Unix, and are evaluated from right to left:
styleLoader(cssLoader(input))
body {
background: cornsilk;
}
To make webpack aware of the CSS, we have to refer to it from our source code:
src/index.js
import "./main.css";
...
Execute npm start and browse to http://localhost:8080 if you are using the default
port and open up main.css and change the background color to something like lime
(background: lime).
4.3 PostCSS
PostCSS³ allows you to perform transformations over CSS through JavaScript plugins.
PostCSS is the equivalent of Babel for styling and you can find plugins for many
purposes. It can even fix browser bugs like 100vh behavior on Safari postcss-100vh-
fix⁴. PostCSS is discussed in the next chapters.
³http://postcss.org/
⁴https://www.npmjs.com/package/postcss-100vh-fix
Loading Styles 30
For anything css-in-js related, please refer to the documentation of the specific
solution. Often webpack is well supported by the options.
The CSS Modules appendix discusses an approach that allows you to treat
local to files by default. It avoids the scoping problem of CSS.
const config = {
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 1 },
},
"sass-loader",
],
};
If you added more loaders, such as postcss-loader, to the chain, you would have to
adjust the importLoaders option accordingly.
4.6 Conclusion
Webpack can load a variety of style formats. The approaches covered here write the
styling to JavaScript bundles by default.
To recap:
Although the loading approach covered here is enough for development purposes, it’s
not ideal for production. You’ll learn why and how to solve this in the next chapter
by separating CSS from the source.
¹²https://github.com/postcss/postcss-loader/issues/166
5. Separating CSS
Even though there is a nice build set up now, where did all the CSS go? As per
configuration, it has been inlined to JavaScript! Although this can be convenient
during development, it doesn’t sound ideal.
The current solution doesn’t allow caching CSS. You can also get a Flash of
Unstyled Content (FOUC). FOUC happens because the browser takes a while to
load JavaScript, and the styles would be applied only then. Separating CSS to a file
of its own avoids the problem by letting the browser to manage it separately.
Webpack provides a means to generate a separate CSS bundles using mini-css-extract-
plugin¹ (MCEP). It can aggregate multiple CSS files into one. For this reason, it comes
with a loader that handles the extraction process. The plugin then picks up the result
aggregated by the loader and emits a separate file with the styling.
¹https://www.npmjs.com/package/mini-css-extract-plugin
²https://www.npmjs.com/package/extract-css-chunks-webpack-plugin
Separating CSS 34
That [name] placeholder uses the name of the entry where the CSS is referred.
Placeholders and hashing are discussed in detail in the Adding Hashes to Filenames
chapter.
If you wanted to output the resulting file to a specific directory, you could
do it by passing a path. Example: filename: "styles/[name].css".
If you are using CSS Modules, remember to tweak use as discussed in the
Loading Styles chapter. You can maintain separate setups for standard CSS
and CSS Modules so that they get loaded through discrete logic.
Separating CSS 36
After running npm run build, you should see output similar to the following:
Now styling has been pushed to a separate CSS file. Thus, the JavaScript bundle has
become slightly smaller, and you avoid the FOUC problem. The browser doesn’t have
to wait for JavaScript to load to get styling information. Instead, it can process the
CSS separately, avoiding the flash.
After this change, you don’t have to refer to styling from your application code
anymore. In this approach, you have to be careful with CSS ordering, though.
³https://www.npmjs.com/package/glob
Separating CSS 37
If you want strict control over the ordering, you can set up a single CSS entry and
then use @import to bring the rest to the project through it. Another option would be
to set up a JavaScript entry and go through import to get the same effect.
5.3 Conclusion
The current setup separates styling from JavaScript neatly. Even though the tech-
nique is most valuable with CSS, it can be used to extract any type of modules to a
separate file. The hard part of MiniCssExtractPlugin has to do with its setup, but
the complexity can be hidden behind an abstraction.
To recap:
In the next chapter, you’ll learn to eliminate unused CSS from the project.
⁴https://www.npmjs.com/package/webpack-watched-glob-entries-plugin
6. Eliminating Unused CSS
Frameworks like Bootstrap or Tailwind tend to come with a lot of CSS. Often you
use only a small part of it and if you aren’t careful, you will bundle the unused CSS.
PurgeCSS¹ is a tool that can achieve this by analyzing files. It walks through your
code and figures out which CSS classes are being used as often there is enough
information for it to strip unused CSS from your project. It also works with single
page applications to an extent.
Given PurgeCSS works well with webpack, we’ll demonstrate it in this chapter.
Generate a starter configuration using npx tailwindcss init. After this you’ll end
up with a tailwind.config.js file at the project root.
¹https://www.npmjs.com/package/purgecss
Eliminating Unused CSS 39
To make sure the tooling can find files containing Tailwind classes, adjust it as
follows:
tailwind.config.js
module.exports = {
content: ["./src/**/*.{js}"],
theme: {
extend: {},
},
plugins: [],
};
exports.tailwind = () => ({
loader: "postcss-loader",
options: {
postcssOptions: { plugins: [require("tailwindcss")()] },
},
});
@tailwind base;
@tailwind components;
/* Write your utility classes here */
@tailwind utilities;
body {
background: cornsilk;
}
You should also make the demo component use Tailwind classes:
src/component.js
If you run the application (npm start), the “Hello world” should look like a button.
Styled hello
Eliminating Unused CSS 41
As you can see, the size of the CSS file grew, and this is something to fix with
PurgeCSS.
²https://www.npmjs.com/package/purgecss-webpack-plugin
Eliminating Unused CSS 42
exports.eliminateUnusedCSS = () => ({
plugins: [
new PurgeCSSPlugin({
paths: ALL_FILES, // Consider extracting as a parameter
extractors: [
{
extractor: (content) =>
content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [],
extensions: ["html"],
},
],
}),
],
});
³https://github.com/FullHuman/purgecss/releases/tag/v3.0.0
Eliminating Unused CSS 43
If you execute npm run build now, you should see something:
The size of the style has decreased noticeably. Instead of 1.99 MiB, we have roughly
7 KiB now.
⁴https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css
⁵https://www.npmjs.com/package/uncss
Eliminating Unused CSS 44
6.3 Conclusion
Using PurgeCSS can lead to a significant decrease in file size. It’s mainly valuable
for static sites that rely on a massive CSS framework. The more dynamic a site or an
application becomes, the harder it becomes to analyze reliably.
To recap:
In the next chapter, you’ll learn to autoprefix. Enabling the feature makes it more
convenient to develop complicated CSS setups that work with older browsers as well.
⁶https://developers.google.com/web/fundamentals/performance/critical-rendering-path/
⁷https://github.com/addyosmani/critical-path-css-tools
7. Autoprefixing
It can be challenging to remember which vendor prefixes you have to use for specific
CSS rules to support a large variety of users. Autoprefixing solves this problem. It
can be enabled through PostCSS and the autoprefixer¹ plugin. autoprefixer uses Can
I Use² service to figure out which rules should be prefixed and its behavior can be
tuned further.
exports.autoprefix = () => ({
loader: "postcss-loader",
options: {
postcssOptions: { plugins: [require("autoprefixer")()] },
},
});
¹https://www.npmjs.com/package/autoprefixer
²http://caniuse.com/
Autoprefixing 46
³https://www.npmjs.com/package/cosmiconfig
⁴https://www.npmjs.com/package/parcel-css-loader
⁵https://www.npmjs.com/package/browserslist
Autoprefixing 47
If you build the application now (npm run build) and examine the built CSS, you
should see that CSS was added to support older browsers. Try adjusting the definition
to see what difference it makes on the build output.
You can lint CSS through Stylelint⁶. It can be set up the same way through
postcss-loader as autoprefixing above.
⁶http://stylelint.io/
⁷https://www.npmjs.com/package/browserslist#configuring-for-different-environments
⁸https://www.npmjs.com/package/postcss-preset-env
Autoprefixing 48
7.3 Conclusion
Autoprefixing is a convenient technique as it decreases the amount of work needed
while crafting CSS. You can maintain minimum browser requirements within a
.browserslistrc file. The tooling can then use that information to generate optimal
output.
To recap:
const config = {
module: {
rules: [
{
// **Conditions** to match files using RegExp, function.
test: /\.js$/,
// **Restrict** matching to a directory.
include: path.join(__dirname, "app"),
exclude: (path) => path.match(/node_modules/);
// **Actions** to apply loaders to the matched files.
use: "babel-loader",
},
],
},
};
Loader Definitions 51
const config = {
test: /\.css$/,
use: ["style-loader", "css-loader"],
};
Based on the right to left rule, the example can be split up while keeping it equivalent:
const config = [
{ test: /\.css$/, use: "style-loader" },
{ test: /\.css$/, use: "css-loader" },
];
If you are not sure how a particular RegExp matches, consider using an
online tool, such as regex101¹, RegExr², or ExtendsClass RegEx Tester³.
Enforcing order
Even though it would be possible to develop an arbitrary configuration using the rule
above, it can be convenient to be able to force specific rules to be applied before or
after regular ones. The enforce field can come in handy here. It can be set to either
pre or post to push processing either before or after other loaders.
¹https://regex101.com/
²http://regexr.com/
³https://extendsclass.com/regex-tester.html
Loader Definitions 52
Linting is a good example because the build should fail before it does anything else.
Using enforce: "post" is rarer and it would imply you want to perform a check
against the built source. Performing analysis against the built source is one potential
example.
const config = {
test: /\.js$/,
enforce: "pre", // "post" too
use: "eslint-loader",
};
It would be possible to write the same configuration without enforce if you chained
the declaration with other loaders related to the test carefully. Using enforce
removes the necessity for that and allows you to split loader execution into separate
stages that are easier to compose.
This style of configuration works in entries and source imports too as webpack picks
it up. The format comes in handy in certain individual cases, but often you are better
off using more readable alternatives. In this case, it’s preferable to go through use:
const config = {
test: /\.js$/,
use: { loader: "babel-loader", options: { presets: ["env"] } },
};
Loader Definitions 53
The problem with this approach is that it couples your source with webpack.
Nonetheless, it’s still an excellent form to know.
Since configuration entries go through the same mechanism, the same forms work
there as well:
const config = {
test: /\.css$/,
// `resource` refers to the resource path matched.
// `resourceQuery` contains possible query passed to it
// `issuer` tells about match context path
// You have to return something falsy, object, or a string
use: ({ resource, resourceQuery, issuer }) =>
env === "development" && ["css-loader", "style-loader"],
};
const config = {
rules: [
{
test: /\.js$/,
use: [
(info) => ({
loader: "babel-loader",
options: { presets: ["env"] },
}),
],
},
],
};
If you execute code like this, you’ll see a print in the console:
{
resource: '/webpack-demo/src/main.css',
realResource: '/webpack-demo/src/main.css',
resourceQuery: '',
issuer: '',
compiler: 'mini-css-extract-plugin /webpack-demo/node_modules/css-loa\
der/dist/cjs.js!/webpack-demo/node_modules/postcss-loader/src/index.js?\
?ref--4-2!/webpack-demo/node_modules/postcss-loader/src/index.js??ref--\
4-3!/webpack-demo/src/main.css'
}
Loader Definitions 55
const config = {
test: /\.png$/,
oneOf: [
{ resourceQuery: /inline/, use: "url-loader" },
{ resourceQuery: /external/, use: "file-loader" },
],
};
If you wanted to embed the context information to the filename, the rule could use
resourcePath over resourceQuery.
const config = {
test: /\.css$/,
rules: [
{ issuer: /\.js$/, use: "style-loader" },
{ use: "css-loader" },
],
};
Loader Definitions 56
const config = {
test: /\.css$/,
rules: [
// Add CSS imported from other modules to the DOM
{ issuer: { not: /\.css$/ }, use: "style-loader" },
{ use: "css-loader" }, // Apply against CSS imports
],
};
• not - Do not match against a condition (see test for accepted values).
• and - Match against an array of conditions. All must match.
• or - Match against an array while any must match.
Loader Definitions 57
8.11 Conclusion
Webpack provides multiple ways to setup loaders but sticking with use is enough
starting from webpack 4. Be careful with loader ordering, as it’s a common source of
problems.
To recap:
• Loaders allow you determine what should happen when webpack’s module
resolution mechanism encounters a file.
• A loader definition consists of conditions based on which to match and actions
that should be performed when a match happens.
• Webpack provides multiple ways to match and alter loader behavior. You can,
for example, match based on a resource query after a loader has been matched
and route the loader to specific actions.
¹https://webpack.js.org/guides/asset-modules/
²https://www.npmjs.com/package/url-loader
³https://www.npmjs.com/package/file-loader
⁴https://www.npmjs.com/package/raw-loader
Loading Images 59
To test that the setup works, download an image or generate it (convert -size
100x100 gradient:blue logo.png) and refer to it from the project:
src/main.css
body {
background: cornsilk;
background-image: url("./logo.png");
background-repeat: no-repeat;
background-position: center;
}
The behavior changes depending on the limit you set. Below the limit, it should
inline the image while above it should emit a separate asset and a path to it.
Assuming you have set up your styling correctly, you can refer to your SVG files as
below. The example SVG path below is relative to the CSS file:
.icon {
background-image: url("../assets/icon.svg");
}
@import "~sprite.sass";
.close-button {
sprite($close);
}
.open-button {
sprite($open);
}
¹⁶https://www.npmjs.com/package/webpack-spritesmith
¹⁷https://www.npmjs.com/package/image-trace-loader
¹⁸https://www.npmjs.com/package/lqip-loader
Loading Images 63
The URL interface¹⁹ is standard and technically it would work even without a bundler
assuming the image was in the correct location. If webpack is used, it will let you
process the image.
It’s also possible to set up dynamic imports as discussed in the Code Splitting chapter.
Here’s a small example:
¹⁹https://developer.mozilla.org/en-US/docs/Web/API/URL
Loading Images 64
9.9 Conclusion
Webpack allows you to inline images within your bundles when needed. Figuring out
proper inlining limits for your images requires experimentation. You have to balance
between bundle sizes and the number of requests.
To recap:
• Use loader type field to set asset loading behavior. It replaces file-loader and
url-loader used before webpack 5.
• You can find image optimization related loaders and plugins that allow you to
tune their size further.
• It’s possible to generate sprite sheets out of smaller images to combine them
into a single request.
• Webpack allows you to load images dynamically based on a given condition.
const config = {
test: /\.woff2?(\?v=\d+\.\d+\.\d+)?$/, // Match .woff?v=1.1.1.
type: "asset",
parser: { dataUrlCondition: { maxSize: 50000 } },
};
In case you want to make sure the site looks good on a maximum amount of browsers,
you can use type: "asset/resource" field at a loader definition and forget about
inlining. Again, it’s a trade-off as you get extra requests, but perhaps it’s the right
move.
¹https://caniuse.com/woff
Loading Fonts 66
const config = {
test: /\.(ttf|eot|woff|woff2)$/,
type: "asset/resource",
};
The way you write your CSS definition² matters. To make sure you are getting the
benefit from the newer formats, they should become first in the definition.
@font-face {
font-family: "Demo Font";
src: url("./fonts/df.woff2") format("woff2"), url("./fonts/df.woff")
format("woff"),
url("./fonts/df.eot") format("embedded-opentype"), url("./fonts/df.\
ttf")
format("truetype");
}
To have more control over font output, one option is to use url-loader and file-loader
as they still work. Furthermore, it’s possible to manipulate publicPath and override
the default per loader definition. The following example illustrates these techniques
together:
{
// Match woff2 and patterns like .woff?v=1.1.1.
test: /\.woff2?(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: "url-loader",
options: {
limit: 50000,
mimetype: "application/font-woff",
name: "./fonts/[name].[ext]", // Output below ./fonts
publicPath: "../", // Take the directory into account
},
},
},
Take care with SVG images if you have SVG specific image setup in place
already. If you want to process font SVGs differently, set their definitions
carefully. The Loader Definitions chapter covers alternatives.
10.7 Conclusion
Loading fonts is similar to loading other assets. You have to consider the browsers
you want to support and choose the loading strategy based on that.
To recap:
• When loading fonts, the same techniques as for images apply. You can choose
to inline small fonts while bigger ones are served as separate assets.
• If you decide to provide first class support to only modern browsers, you can
select only a font format or two and let the older browsers to use system level
fonts.
In the next chapter, you’ll learn to load JavaScript using Babel and webpack. Webpack
loads JavaScript by default, but there’s more to the topic as you have to consider what
browsers you want to support.
⁷https://www.npmjs.com/package/webfonts-loader
11. Loading JavaScript
Webpack processes ES2015 module definitions by default and transforms them into
code. It does not transform specific syntax, such as const, though. The resulting code
can be problematic especially in the older browsers.
To get a better idea of the default transform, we can generate a build while setting
webpack’s mode to none to avoid any transformation. Change the build target to use
none temporarily ({ mode: "none" } in webpack configuration) and run npm run
build:
dist/main.js
...
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require\
__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => __WEBPACK_DEFAULT_EXPORT__
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ((text \
= "Hello world") => {
const element = document.createElement("div");
element.className = "rounded bg-red-100 border max-w-md m-4 p-4";
element.innerHTML = text;
return element;
});
...
The problem can be worked around by processing the code through Babel¹, a
JavaScript transpiler that supports ES2015+ features and more. It resembles ESLint
¹https://babeljs.io/
Loading JavaScript 70
in that it’s built on top of presets and plugins. Presets are collections of plugins, and
you can define your own as well.
Babel isn’t the only option, although it’s the most popular one. esbuild-
loader² and swc-loader³ are worth checking out if you don’t need any
specific Babel presets or plugins and want more performance.
Given that Node supports the ES2015 specification well⁶ these days, you
can use a lot of ES2015 features without having to process configuration
through Babel.
Setting up babel-loader
The first step towards configuring Babel to work with webpack is to set up babel-
loader⁷. It takes the code and turns it into a format older browsers can understand.
Install babel-loader and include its peer dependency @babel/core:
exports.loadJavaScript = () => ({
module: {
rules: [
// Consider extracting include as a parameter
{ test: /\.js$/, include: APP_SOURCE, use: "babel-loader" },
],
},
});
Next, you need to connect this to the main configuration. If you are using a modern
browser for development, you can consider processing only the production code
through Babel. It’s used for both production and development environments in this
case. Also, only application code is processed through Babel.
⁷https://www.npmjs.com/package/babel-loader
Loading JavaScript 72
Adjust as below:
webpack.config.js
Even though you have Babel installed and set up, you are still missing one bit: Babel
configuration. The configuration can be set up using a .babelrc dotfile as then other
tooling can use the same.
Setting up .babelrc
At a minimum, you need @babel/preset-env⁸. It’s a Babel preset that enables the
required plugins based on browserslist⁹ definition.
Install the preset first:
To make Babel aware of the preset, you need to write a .babelrc. Given webpack
supports ES2015 modules out of the box, you should tell Babel to skip processing
them.
⁸https://www.npmjs.com/package/@babel/preset-env
⁹https://www.npmjs.com/package/browserslist
Loading JavaScript 73
{
"presets": [["@babel/preset-env", { "modules": false }]]
}
If you execute npm run build -- --mode none and examine dist/main.js, you will
see something different based on your .browserslistrc file. Try to include only a
definition like IE 8 there, and the code should change accordingly:
dist/main.js
...
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require\
__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => __WEBPACK_DEFAULT_EXPORT__
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (functi\
on () {
var text = arguments.length > 0 && arguments[0] !== undefined ? argum\
ents[0] : "Hello world";
var element = document.createElement("div");
element.className = "rounded bg-red-100 border max-w-md m-4 p-4";
element.innerHTML = text;
return element;
});
...
Note especially how the function was transformed. You can try out different browser
definitions and language features to see how the output changes based on the
selection.
Loading JavaScript 74
core-js pollutes the global scope with objects like Promise. Given this
can be problematic for library authors, there’s @babel/plugin-transform-
runtime¹⁴ option. It can be enabled as a Babel plugin, and it avoids the
problem of globals by rewriting the code in a such way that they aren’t be
needed.
Certain webpack features, such as Code Splitting, write Promise based code
to webpack’s bootstrap after webpack has processed loaders. The problem
can be solved by applying a shim before your application code is executed.
Example: entry: { app: ["core-js/es/promise", "./src"] }.
¹⁰https://www.npmjs.com/package/core-js
¹¹https://www.npmjs.com/package/regenerator-runtime
¹²https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
¹³https://www.npmjs.com/package/corejs-upgrade-webpack-plugin
¹⁴https://babeljs.io/docs/plugins/transform-runtime/
Loading JavaScript 75
• babel-plugin-import¹⁷ rewrites module imports so that you can use a form such
as import { Button } from "antd"; instead of pointing to the module through
an exact path.
• babel-plugin-transform-react-remove-prop-types¹⁸ removes propType related
code from your production build. It also allows component authors to generate
code that’s wrapped so that setting environment at DefinePlugin can kick in
as discussed in the Environment Variables chapter.
• babel-plugin-macros¹⁹ provides a runtime environment for small Babel modifi-
cations without requiring additional plugin setup.
¹⁵https://babeljs.io/docs/en/options
¹⁶https://www.npmjs.com/package/json5
¹⁷https://www.npmjs.com/package/babel-plugin-import
¹⁸https://www.npmjs.com/package/babel-plugin-transform-react-remove-prop-types
¹⁹https://www.npmjs.com/package/babel-plugin-macros
²⁰https://www.npmjs.com/package/babel-register
²¹https://www.npmjs.com/package/babel-cli
Loading JavaScript 76
The fallback isn’t without problems as in the worst case, it can force the browser to
load the module twice. Therefore relying on a user agent may be a better option as
highlighted by John Stewart in his example²³. To solve the issue, Andrea Giammarchi
has developed a universal bundle loader²⁴.
On webpack side, you will have to take care to generate two builds with differing
browserslist definitions and names. In addition, you have to make sure the HTML
template receives the script tags as above so it’s able to load them.
²²https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
²³https://github.com/johnstew/differential-serving
²⁴https://medium.com/@WebReflection/a-universal-bundle-loader-6d7f3e628f93
Loading JavaScript 77
To give you a better idea on how to implement the technique, consider the following
and set up a browserslist as below:
.browserslistrc
The idea is to then write webpack configuration to control which target is chosen
like this:
webpack.config.js
{
"scripts": {
"build": "wp --mode prod:legacy && wp --mode prod:modern"
}
}
To complete the setup, you have to write a script reference to your HTML using one
of the techniques outlined above. The webpack builds can run parallel and you could
use for example use the concurrently²⁵ package to speed up the execution.
These days it’s possible to go one step further and use native JavaScript
modules directly in the browser²⁶.
11.6 TypeScript
Microsoft’s TypeScript²⁷ is a compiled language that follows a similar setup as Babel.
The neat thing is that in addition to JavaScript, it can emit type definitions. A good
editor can pick those up and provide enhanced editing experience. Stronger typing
is valuable for development as it becomes easier to state your type contracts.
Compared to Facebook’s type checker Flow, TypeScript is a safer option in terms of
ecosystem. As a result, you find more premade type definitions for it, and overall,
the quality of support should be better.
ts-loader²⁸ is the recommended option for TypeScript. One option is to leave only
compilation to it and then handle type checking either outside of webpack or to use
fork-ts-checker-webpack-plugin²⁹ to handle checking in a separate process.
²⁵https://www.npmjs.com/package/concurrently
²⁶https://philipwalton.com/articles/using-native-javascript-modules-in-production-today/
²⁷http://www.typescriptlang.org/
²⁸https://www.npmjs.com/package/ts-loader
²⁹https://www.npmjs.com/package/fork-ts-checker-webpack-plugin
Loading JavaScript 79
Webpack 5 includes TypeScript support out of the box. Make sure you
don’t have @types/webpack installed in your project as it will conflict.
@types/webpack-env³² contains webpack types related to the environment.
If you use features like require.context, then you should install this one.
Especially the logError portion is important as without this ts-node would crash the
build on error. transpileOnly is useful to set if you want to handle type-checking
³⁰https://www.npmjs.com/package/@babel/plugin-transform-typescript
³¹https://babeljs.io/docs/en/next/babel-plugin-transform-typescript.html#caveats
³²https://www.npmjs.com/package/@types/webpack-env
³³https://www.npmjs.com/package/ts-node
³⁴https://www.npmjs.com/package/ts-node-dev
Loading JavaScript 80
outside of the process. For example, you could run tsc using a separate script. Often
editor tooling can catch type issues as you are developing as well eliminating the
need to check through ts-node.
11.7 WebAssembly
WebAssembly³⁵ allows developers to compile to a low-level representation of code
that runs within the browser. It complements JavaScript and provides one path of
potential optimization. The technology can also be useful when you want to run an
old application without porting it entirely to JavaScript.
Starting from webpack 5, the tool supports new style asynchronous WebAssembly.
The official examples, wasm-simple³⁶ and wasm-complex³⁷, illustrate the experimen-
tal functionality well. wasmpack’s webpack tutorial³⁸ shows how to package Rust
code using WebAssembly to be consumed through webpack.
11.8 Conclusion
Webpack loads JavaScript out of the box. Tools like Babel let you target specific
browsers and have more control over the output.
To recap:
• Babel gives you control over what browsers to support. It can compile ES2015+
features to a form the older browser understand. @babel/preset-env is valu-
able as it can choose which features to compile and which polyfills to enable
based on your browser definition.
• Babel allows you to use experimental language features. Babel ecosystem has
numerous presets and plugins to customize it.
• Babel functionality can be enabled per development target. This way you can
be sure you are using the correct plugins at the right place.
³⁵https://developer.mozilla.org/en-US/docs/WebAssembly
³⁶https://github.com/webpack/webpack/tree/master/examples/wasm-simple
³⁷https://github.com/webpack/webpack/tree/master/examples/wasm-complex
³⁸https://rustwasm.github.io/docs/wasm-pack/tutorials/hybrid-applications-with-webpack/index.html
IV Building
In this part, you enable source maps on the build, discuss how to split it into separate
bundles in various ways, and learn to tidy up the result.
12. Source Maps
When your source code has gone through transformations, debugging in the browser
becomes a problem. Source maps solve this problem by providing a mapping be-
tween the original and the transformed source code. In addition to source compiling
to JavaScript, this works for styling as well.
One approach is to skip source maps during development and rely on browser support
of language features. If you use ES2015 without any extensions and develop using a
modern browser, this can work. The advantage of doing this is that you avoid all the
problems related to source maps while gaining better performance.
If you are using webpack 4 or newer and the mode option, the tool will generate
source maps automatically for you in development mode. Production usage requires
attention, though.
If you want to understand the ideas behind source maps in greater detail,
see the source map specification¹.
¹https://sourcemaps.info/spec.html
Source Maps 83
Webpack supports a wide variety of source map types. These vary based on quality
and build speed. For now, you enable source-map for production and let webpack use
the default for development. Set it up as follows:
webpack.config.js
source-map is the slowest and highest quality option of them all, but that’s fine for
a production build.
If you build the project now (npm run build), you should see source maps in the
project output at the dist directory. Take a good look at those .map files. That’s where
the mapping between the generated and the source happens. During development, it
writes the mapping information in the bundle.
• Chrome³
• Firefox⁴
³https://developers.google.com/web/tools/chrome-devtools
⁴https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map
Source Maps 85
• IE Edge⁵
• Safari⁶
• Inline source maps add the mapping data directly to the generated files.
• Separate source maps emit the mapping data to separate source map files
and link the source to them using a comment. Hidden source maps omit the
comment on purpose.
Thanks to their speed, inline source maps are ideal for development. Given they
make the bundles big, separate source maps are the preferred solution for production.
Separate source maps work during development as well if the performance overhead
is acceptable.
devtool: "eval"
eval generates code in which each module is wrapped within an eval function:
/***/ "./src/index.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */\
var _main_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./s\
rc/main.css\");\n/* harmony import */ var _main_css__WEBPACK_IMPORTED_M\
ODULE_0___default = /*#__PURE__*/__webpack_require__.n(_main_css__WEBPA\
CK_IMPORTED_MODULE_0__);\n/* harmony import */ var _component__WEBPACK_\
IMPORTED_MODULE_1__ = __webpack_require__(\"./src/component.js\");\n\n\\
ndocument.body.appendChild(Object(_component__WEBPACK_IMPORTED_MODULE_1\
__[\"default\"])());\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ }),
devtool: "cheap-eval-source-map"
{
"version": 3,
"file": "./src/index.js.js",
"sources": ["webpack:///./src/index.js?3700"],
"sourcesContent": [
"import './main.css';\nimport component from \"./component\";\ndocu\
ment.body.appendChild(component());"
],
"mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA",
"sourceRoot": ""
}
Source Maps 87
devtool: "cheap-module-eval-source-map"
{
"version": 3,
"file": "./src/index.js.js",
"sources": ["webpack:///./src/index.js?b635"],
"sourcesContent": ["import './main.css';\nimport component ..."],
"mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AAEA",
"sourceRoot": ""
}
devtool: "eval-source-map"
eval-source-map is the highest quality option of the inline options. It’s also the
slowest one as it emits the most data:
{
"version": 3,
"sources": ["webpack:///./src/index.js?b635"],
"names": ["document", "body", "appendChild", "component"],
"mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AAEAA,QAAQ,CAACC,IAAT,CAAc\
C,WAAd,CAA0BC,0DAAS,EAAnC",
"file": "./src/index.js.js",
"sourcesContent": ["import './main.css';\nimport component ..."],
"sourceRoot": ""
}
Source Maps 88
devtool: "cheap-source-map"
cheap-source-map is similar to the cheap options above. The result is going to miss
column mappings. Also, source maps from loaders, such as css-loader, are not going
to be used.
Examining the .map file reveals the following output in this case:
{
"version": 3,
"file": "main.js",
"sources": [
"webpack:///webpack/bootstrap",
"webpack:///./src/component.js",
"webpack:///./src/index.js",
"webpack:///./src/main.css"
],
"sourcesContent": [
"...",
"// extracted by mini-css-extract-plugin"
],
"mappings": ";AAAA;...;;ACFA;;;;A",
"sourceRoot": ""
}
Source Maps 89
devtool: "cheap-module-source-map"
{
"version": 3,
"file": "main.js",
"sources": [
"webpack:///webpack/bootstrap",
"webpack:///./src/component.js",
"webpack:///./src/index.js",
"webpack:///./src/main.css"
],
"sourcesContent": [
"...",
"// extracted by mini-css-extract-plugin"
],
"mappings": ";AAAA;...;;ACFA;;;;A",
"sourceRoot": ""
}
⁸https://github.com/webpack/webpack/issues/4176
Source Maps 90
devtool: "source-map"
source-map provides the best quality with the complete result, but it’s also the slowest
option. The output reflects this:
{
"version": 3,
"sources": [
"webpack:///webpack/bootstrap",
"webpack:///./src/component.js",
"webpack:///./src/index.js",
"webpack:///./src/main.css"
],
"names": [
"text",
"element",
"document",
"createElement",
"className",
"innerHTML",
"body",
"appendChild",
"component"
],
"mappings": ";AAAA;...;;ACFA;;;;A",
"file": "main.js",
"sourcesContent": [
"...",
"// extracted by mini-css-extract-plugin"
],
"sourceRoot": ""
}
devtool: "hidden-source-map"
devtool: "nosources-source-map"
const config = {
output: {
// Modify the name of the generated source map file.
// You can use [file], [id], [fullhash], and [chunkhash]
// replacements here. The default option is often enough.
sourceMapFilename: "[file].map", // Default
If you want more control over source map generation, it’s possible to use the
SourceMapDevToolPlugin¹⁰ or EvalSourceMapDevToolPlugin instead. The latter is
a more limited alternative, and as stated by its name, it’s handy for generating eval
based source maps.
Both plugins can allow more granular control over which portions of the code you
want to generate source maps for, while also having strict control over the result with
SourceMapDevToolPlugin. Using either plugin allows you to skip the devtool option.
Given webpack matches only .js and .css files by default for source maps, you can
use SourceMapDevToolPlugin to overcome this issue. This can be achieved by passing
a test pattern like /\.(js|jsx|css)($|\?)/i.
EvalSourceMapDevToolPlugin accepts only module field. Therefore it can be consid-
ered as an alias to devtool: "eval" while allowing a notch more flexibility.
const config = {
output: {
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
},
plugins: [webpack.SourceMapDevToolPlugin({})],
};
const config = {
stats: {
ignoreWarnings: { message: /Failed to parse source map/ },
},
};
12.13 Conclusion
Source maps can be convenient during development. They provide better means to
debug applications as you can still examine the original code over a generated one.
They can be valuable even for production usage and allow you to debug issues while
serving a client-friendly version of your application.
To recap:
• Source maps can be helpful both during development and production. They
provide information about what’s going on and speed up debugging.
• Webpack supports many source map variants in inline and separate categories.
Inline source maps are handy during development due to their speed. Separate
source maps work for production as then loading them becomes optional.
• devtool: "source-map" is the highest quality option valuable for production.
• inline-module-source-map is a good starting point for development.
• Use devtool: "hidden-source-map" to get only stack traces during production
and to send it to a third-party service for you to examine later and fix.
• SourceMapDevToolPlugin and EvalSourceMapDevToolPlugin provide more con-
trol over the result than the devtool shortcut.
• You should use source-map-loader with third-party dependencies.
• Enabling source maps for styling requires additional effort. You have to enable
sourceMap option per styling related loader you are using.
The goal is to end up with a split point that gets loaded on demand. There can be
splits inside splits, and you can structure an entire application based on splits. The
advantage of doing this is that then the initial payload of your site can be smaller
than it would be otherwise.
Code splitting
Dynamic import
Dynamic imports are defined as Promises:
Webpack provides extra control through a comment. In the example, we’ve renamed
the resulting chunk. Giving multiple chunks the same name will group them to the
same bundle. In addition webpackMode, webpackPrefetch, and webpackPreload are
good to know options as they let you define when the import will get triggered and
how the browser should treat it.
Code Splitting 97
Mode lets you define what happens on import(). Out of the available options, weak
is suitable for server-side rendering (SSR) as using it means the Promise will reject
unless the module was loaded another way. In the SSR case, that would be ideal.
Prefetching tells the browser that the resource will be needed in the future while
preloading means the browser will need the resource within the current page. Based
on these tips the browser can then choose to load the data optimistically. Webpack
documentation explains the available options in greater detail³.
The interface allows composition, and you could load multiple resources in parallel:
Promise.all([import("lunr"), import("../search_index.json")]).then(
([lunr, search]) => {
return {
index: lunr.Index.load(search.index),
lines: search.lines,
};
}
);
The code above creates separate bundles to a request. If you wanted only one, you
would have to use naming or define an intermediate module to import.
The syntax works only with JavaScript after configuring it the right way.
If you use another environment, you may have to use alternatives covered
in the following sections.
³https://webpack.js.org/api/module-methods/#magic-comments
⁴https://webpack.js.org/plugins/prefetch-plugin/
Code Splitting 98
You also need to point the application to this file, so the application knows to load
it by binding the loading process to click. Whenever the user happens to click the
button, you trigger the loading process and replace the content:
src/component.js
return element;
};
If you open up the application (npm start) and click the button, you should see the
new text in it.
Code Splitting 99
That 34.js is your split point. Examining the file reveals webpack has processed the
code.
const config = {
plugins: [
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
],
};
13.6 Conclusion
Code splitting is a feature that allows you to push your application a notch further.
You can load code when you need it to gain faster initial load times and improved
user experience especially in a mobile context where bandwidth is limited.
To recap:
• Code splitting comes with extra effort as you have to decide what to split
and where. Often, you find good split points within a router. Or you notice
that specific functionality is required only when a particular feature is used.
Charting is an excellent example of this.
• Use naming to pull separate split points into the same bundles.
• The techniques can be used within modern frameworks and libraries like React.
You can wrap related logic to a specific component that handles the loading
process in a user-friendly manner.
In the next chapter, you’ll learn how to split a vendor bundle without through
webpack configuration.
¹https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693
Bundle Splitting 103
import "react";
import "react-dom";
...
Execute npm run build to get a baseline build. You should end up with something as
below:
To extract a vendor bundle from the node_modules directory, adjust the code as
follows:
webpack.config.js
If you try to generate a build now (npm run build), you should see something along
this:
},
},
},
},
]);
Starting from webpack 5, there’s more control over chunking based on asset type:
const config = {
optimization: {
splitChunks: {
// css/mini-extra is injected by mini-css-extract-plugin
minSize: { javascript: 20000, "css/mini-extra": 10000 },
},
},
};
const config = {
plugins: [
new webpack.optimize.AggressiveSplittingPlugin({
minSize: 10000,
maxSize: 30000,
}),
],
};
There’s a trade-off as you lose out in caching if you split to multiple small bundles.
You also get request overhead in HTTP/1 environment.
The aggressive merging plugin works the opposite way and allows you to combine
small bundles into bigger ones:
const config = {
plugins: [
new AggressiveMergingPlugin({
minSizeReduce: 2,
moveToParents: true,
}),
],
};
It’s possible to get good caching behavior with these plugins if a webpack records
are used. The idea is discussed in detail in the Adding Hashes to Filenames chapter.
webpack.optimize contains LimitChunkCountPlugin and MinChunkSizePlugin which
give further control over chunk size.
²https://medium.com/webpack/webpack-http-2-7083ec3f3ce6
Bundle Splitting 108
const config = {
entry: {
app: {
import: path.join(__dirname, "src", "index.js"),
dependOn: "vendor",
},
vendor: ["react", "react-dom"],
},
};
If you have this configuration in place, you can drop optimization.splitChunks and
the output should still be the same.
14.7 Conclusion
The situation is better now compared to the earlier. Note how small main bundle
compared to the vendor bundle. To benefit from this split, you set up caching in the
next part of this book in the Adding Hashes to Filenames chapter.
To recap:
• Webpack allows you to split bundles from configuration entries through the
optimization.splitChunks.cacheGroups field. It performs bundle splitting by
default in production mode as well.
• A vendor bundle contains the third-party code of your project. The vendor
dependencies can be detected by inspecting where the modules are imported.
• Webpack offers more control over chunking through specific plugins, such as
AggressiveSplittingPlugin and AggressiveMergingPlugin. Mainly the split-
ting plugin can be handy in HTTP/2 oriented setups.
• Internally webpack relies on three chunk types: entry, normal, and initial
chunks.
const config = {
output: {
clean: true,
},
};
For earlier versions, you can either use clean-webpack-plugin¹ or solve the problem
outside of webpack. You could for example trigger rm -rf ./build && webpack or
rimraf ./build && webpack in an npm script to keep it cross-platform.
¹https://www.npmjs.com/package/clean-webpack-plugin
Tidying Up 111
Setting up output.clean
To wrap the syntax into a function, add a function as follows.
webpack.parts.js
exports.clean = () => ({
output: {
clean: true,
},
});
After this change, the build directory should remain tidy while building and
developing. You can verify this by building the project and making sure no old files
remained in the output directory.
exports.attachRevision = () => ({
plugins: [
new webpack.BannerPlugin({
banner: new GitRevisionPlugin().version(),
}),
],
});
If you build the project (npm run build), you should notice the files ending with
.LICENSE.txt containing comments like /*! 0b5bb05 */ or /*! v1.7.0-9-g5f82fe8
*/ in the beginning.
The output can be customized further by adjusting the banner. You can also pass
revision information to the application using webpack.DefinePlugin. This technique
is discussed in detail in the Environment Variables chapter.
Tidying Up 113
The code expects you run it within a Git repository! Otherwise, you get a
fatal: Not a git repository (or any of the parent directories):
.git error. If you are not using Git, you can replace the banner with other
data.
15.4 Conclusion
Often, you work with webpack by identifying a problem and then discovering a
plugin to tackle it. It’s entirely acceptable to solve these types of issues outside of
webpack, but webpack can often handle them as well.
To recap:
• You can find many small plugins that work as tasks and push webpack closer
to a task runner.
• These tasks include cleaning the build and deployment. The Deploying Appli-
cations chapter discusses the latter topic in detail.
• It can be a good idea to add small comments to the production build to tell what
version has been deployed. This way you can debug potential issues faster.
• Secondary tasks, like these, can be performed outside of webpack. If you
are using a multi-page setup as discussed in the Multiple Pages chapter, this
becomes a necessity.
⁴https://www.npmjs.com/package/copy-webpack-plugin
⁵https://www.npmjs.com/package/cpy-cli
V Optimizing
In this part, you will learn about code minification, setting environment variables,
adding hashing to filenames, webpack runtime, analyzing build statistics, and
improving webpack performance.
16. Minifying
Since webpack 4, the production output gets minified using terser¹ by default. Terser
is an ES2015+ compatible JavaScript-minifier. Compared to UglifyJS, the earlier
standard for many projects, it’s a future-oriented option.
Although webpack minifies the output by default, it’s good to understand how to
customize the behavior should you want to adjust it further or replace the minifier.
¹https://www.npmjs.com/package/terser
²https://www.npmjs.com/package/terser-webpack-plugin
Minifying 116
exports.minifyJavaScript = () => ({
optimization: { minimizer: [new TerserPlugin()] },
});
If you execute npm run build now, you should see result close to the same as before.
Source maps are disabled by default. You can enable them through the
sourceMap flag. You should check terser-webpack-plugin documentation
for further options.
Scope hoisting
Since webpack 4, it applies scope hoisting in production mode by default. It hoists all
modules to a single scope instead of writing a separate closure for each. Doing this
slows down the build but gives you bundles that are faster to execute. Read more
about scope hoisting³ at the webpack blog.
³https://medium.com/webpack/brief-introduction-to-scope-hoisting-in-webpack-8435084c171f
⁴https://www.npmjs.com/package/html-loader
⁵https://www.npmjs.com/package/posthtml
⁶https://www.npmjs.com/package/posthtml-loader
⁷https://www.npmjs.com/package/posthtml-minifier
⁸https://www.npmjs.com/package/posthtml-minify-classnames
Minifying 118
Like for JavaScript, you can wrap the idea in a configuration part:
webpack.parts.js
To override cssnano with another option, use the minify option. It accepts
a function with the signature (data, inputMap, minimizerOptions) =>
<string>.
⁹https://www.npmjs.com/package/css-minimizer-webpack-plugin
¹⁰http://cssnano.co/
Minifying 119
If you build the project now (npm run build), you should notice that CSS has become
smaller as it’s missing comments and has been concatenated:
¹¹https://www.npmjs.com/package/last-call-webpack-plugin
Minifying 120
16.7 Conclusion
Minification is the most comfortable step you can take to make your build smaller.
To recap:
• Minification process analyzes your source code and turns it into a smaller
form with the same meaning if you use safe transformations. Specific unsafe
transformations allow you to reach even smaller results while potentially
breaking code that relies, for example, on exact parameter naming.
• Webpack performs minification in production mode using Terser by default.
• Besides JavaScript, it’s possible to minify other assets, such as CSS and HTML
too. Minifying these requires specific technologies that have to be applied
through loaders and plugins of their own.
You’ll learn to apply tree shaking against code in the next chapter.
¹²https://www.npmjs.com/package/compression-webpack-plugin
¹³https://www.npmjs.com/package/webpack-obfuscator
17. Tree Shaking
Tree shaking is a feature enabled by the ES2015 module definition. The idea is that
given it’s possible to analyze the module definition statically without running it,
webpack can tell which parts of the code are being used and which are not. It’s
possible to verify this behavior by expanding the application and adding code there
that should be eliminated.
Starting from webpack 5, tree shaking has been improved and it works in cases where
it didn’t work before, including nesting and CommonJS.
To make sure you use a part of the code, alter the application entry point:
src/index.js
...
import { bake } from "./shake";
bake();
Tree Shaking 122
If you build the project again (npm run build) and examine the build (dist/main.js),
it should contain console.log("bake"), but miss console.log("shake"). That’s tree
shaking in action.
SurviveJS - Maintenance⁴ delves deeper to the topic from the package point
of view.
17.4 Conclusion
Tree shaking is a potentially powerful technique. For the source to benefit from tree
shaking, npm packages have to be implemented using the ES2015 module syntax,
and they have to expose the ES2015 version through package.json module field tools
like webpack can pick up.
To recap:
• Tree shaking drops unused pieces of code based on static code analysis.
Webpack performs this process for you as it traverses the dependency graph.
• To benefit from tree shaking, you have to use ES2015 module definition.
• As a package author, you can provide a version of your package that contains
ES2015 modules, while the rest has been transpiled to ES5. It’s important to set
"sideEffects": false as after that webpack knows it’s safe to tree shake the
package.
You’ll learn how to manage environment variables using webpack in the next chapter.
²https://www.npmjs.com/package/babel-plugin-transform-imports
³https://github.com/webpack/webpack/issues/2867
⁴https://survivejs.com/maintenance/packaging/building/
18. Environment Variables
Sometimes a part of your code should execute only during development. Or you
could have experimental features in your build that are not ready for production yet.
Controlling environment variables becomes valuable as you can toggle functional-
ity using them.
Since JavaScript minifiers can remove dead code (if (false)), you can build on
top of this idea and write code that gets transformed into this form. Webpack’s
DefinePlugin enables replacing free variables so that you can convert
¹https://www.npmjs.com/package/dotenv-webpack
Environment Variables 125
var foo;
if (foo === "bar") console.log("bar"); // Not free
if (bar === "bar") console.log("bar"); // Free
If you replaced bar with a string like "foobar", then you would end up with the code
as below:
var foo;
if (foo === "bar") console.log("bar"); // Not free
if ("foobar" === "bar") console.log("bar");
Further analysis shows that "foobar" === "bar" equals false so a minifier gives the
following:
var foo;
if (foo === "bar") console.log("bar"); // Not free
if (false) console.log("bar");
var foo;
if (foo === "bar") console.log("bar"); // Not free
// if (false) means the block can be dropped entirely
²https://www.npmjs.com/package/babel-plugin-transform-define
Environment Variables 126
return {
plugins: [new webpack.DefinePlugin(env)],
};
};
If you run the application, you should see a new message on the button.
Environment Variables 127
.
└── store
├── index.js
├── store.dev.js
└── store.prod.js
The idea is that you choose either dev or prod version of the store depending on the
environment. It’s that index.js which does the hard work:
Webpack can pick the right code based on the DefinePlugin declaration and this
code. You have to use CommonJS module definition style here as ES2015 imports
don’t allow dynamic behavior by design.
³https://blog.johnnyreilly.com/2018/03/its-not-dead-webpack-and-dead-code.html
Environment Variables 128
18.4 Conclusion
Setting environment variables is a technique that allows you to control which paths
of the source are included in the build.
To recap:
To ensure the build has good cache invalidation behavior, you’ll learn to include
hashes to the generated filenames in the next chapter. This way the client notices if
assets have changed and can fetch the updated versions.
19. Adding Hashes to Filenames
Even though the generated build works, the file names it uses is problematic. It
doesn’t allow to leverage client level cache efficiently as there’s no way tell whether
or not a file has changed. Cache invalidation can be achieved by including a hash to
the filenames.
19.1 Placeholders
Webpack provides placeholders for this purpose. These strings are used to attach
specific information to webpack output. The most valuable ones are:
It’s preferable to use particularly hash and contenthash only for production purposes
as hashing doesn’t do much good during development.
There are more options available, and you can even modify the hashing
and digest type as discussed at loader-utils² documentation.
If you are using webpack 4, be careful with contenthash as it’s not fully
reliable³. There chunkhash may be the preferable option.
Example placeholders
Assume you have the following configuration:
const config = {
output: {
path: PATHS.build,
filename: "[name].[contenthash].js",
},
};
²https://www.npmjs.com/package/loader-utils
³https://github.com/webpack/webpack/issues/11146
Adding Hashes to Filenames 131
main.d587bbd6e38337f5accd.js
vendor.dc746a5db4ed650296e1.js
If the file contents related to a chunk are different, the hash changes as well, thus the
cache gets invalidated. More accurately, the browser sends a new request for the file.
If only main bundle gets updated, only that file needs to be requested again.
The same result can be achieved by generating static filenames and invalidating the
cache through a querystring (i.e., main.js?d587bbd6e38337f5accd). The part behind
the question mark invalidates the cache. According to Steve Souders⁴, attaching the
hash to the filename is the most performant option.
⁴http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
Adding Hashes to Filenames 132
If you generate a build now (npm run build), you should see something:
The files have neat hashes now. To prove that it works for styling, you could try
altering src/main.css and see what happens to the hashes when you rebuild.
Adding Hashes to Filenames 133
19.3 Conclusion
Including hashes related to the file contents to their names allows to invalidate them
on the client-side. If a hash has changed, the client is forced to download the asset
again.
To recap:
The next chapter discusses the topic of webpack runtime. To make sure changes
made to it won’t invalidate more code than it should, it’s a good practice to separate
it.
Philip Walton has written about caching in detail⁵ and his article is a great
read if you want to know more about the topic on a more general level.
⁵https://philipwalton.com/articles/cascading-cache-invalidation/
20. Separating a Runtime
When webpack writes bundles, it maintains a runtime as well. The runtime includes
a manifest of the files to be loaded initially. If the names of the files change, then the
manifest changes and the change invalidates the file in which it is contained. For this
reason, it can be a good idea to write the runtime to a file of its own or inline the
manifest information to the index.html file of the project.
The name runtime is used by convention. You can use any other name, and it will
still work.
Separating a Runtime 135
If you build the project now (npm run build), you should see something:
This change gave a separate file that contains the runtime. In the output above it has
been marked with runtime chunk name. As the setup is using MiniHtmlWebpackPlugin,
there is no need to worry about loading the runtime ourselves as the plugin adds a
reference to index.html. Try adjusting src/index.js and see how the hashes change.
Starting from webpack 5, the tool will take your browserslist definition into account
when generating the runtime. See the Autoprefixing chapter for an expanded
discussion. In webpack 5, it’s possible to use target to define in which format the
runtime is written. Setting it to es5 would emit ECMAScript 5 compatible code while
setting to es2015 would generate shorter code for the newer target. The setting also
affects the Minifying process.
To get a better idea of the runtime contents, run the build in development
mode or pass none to mode through configuration. You should see some-
thing familiar there.
Separating a Runtime 136
...
If you build the project (npm run build), you should see a new file, records.json,
at the project root. The next time webpack builds, it picks up the information and
rewrites the file if it has changed.
Records are particularly valuable if you have a complicated setup with code splitting
and want to make sure the split parts gain correct caching behavior. The biggest
problem is maintaining the record file.
If you change the way webpack handles module IDs, possible existing
records are still taken into account! If you want to use the new module
ID scheme, you have to delete your records file as well.
Separating a Runtime 137
20.4 Conclusion
The project has basic caching behavior now. If you try to modify index.js or
component.js, the vendor bundle should remain the same.
To recap:
You’ll learn to analyze the build in the next chapter as it’s essential for understanding
and improving your build.
¹https://www.npmjs.com/package/webpack-manifest-plugin
²https://www.npmjs.com/package/webpack-assets-manifest
21. Build Analysis
Analyzing build statistics is a good step towards understanding webpack better. The
available tooling helps to answer the following questions:
{
"scripts": {
"build:stats": "wp --mode production --json > stats.json"
}
}
The above is the basic setup you need, regardless of your webpack configuration.
Execute npm run build:stats now. After a while you should find stats.json at your
project root. This file can be pushed through a variety of tools to understand better
what’s going on.
Node API
Stats can be captured through Node. Since stats can contain errors, so it’s a good idea
to handle that case separately:
if (stats.hasErrors()) {
return console.error(stats.toString("errors-only"));
}
console.log(stats);
});
The technique can be valuable if you want to do further processing on stats although
often the other solutions are enough.
If you want JSON output from stats, use stats.toJson(). To get verbose
output, use stats.toJson("verbose"). It follows all stat options webpack
supports.
¹https://www.npmjs.com/package/0x
Build Analysis 140
²https://www.npmjs.com/package/webpack-stats-plugin
³https://www.npmjs.com/package/webpack-bundle-tracker
Build Analysis 141
In case your project exceeds the limits, you should see a warning similar to below:
If you want to enforce a strict limit in a CI environment, set hints to error. Doing
this will fail the build in case it is reached and force the developers either go below
the limit or raise a discussion about good limits.
• The official analyse tool⁴ gives you recommendations and a good idea of your
application’s dependency graph. It can be run locally as well.
• Statoscope⁵ is comparable to the official analyse tool except for the lack of a
graph view and it comes with additional filters to understand the output better.
• circular-dependency-plugin⁶ lets you detect cycles in the module graph. Often
this implies a bug, and it can be a good idea to refactor cycles out.
• dependency-cruiser⁷ is a bundler independent tool for analyzing project depen-
dencies.
• madge⁸ is another independent tool that can output a graph based on module
input. The graph output allows you to understand the dependencies of your
project in greater detail.
⁴https://github.com/webpack/analyse
⁵https://statoscope.tech/
⁶https://www.npmjs.com/package/circular-dependency-plugin
⁷https://www.npmjs.com/package/dependency-cruiser
⁸https://www.npmjs.com/package/madge
Build Analysis 142
• Arkit⁹ goes a step beyond madge and it constructs entire architectural overviews
of projects.
Pie charts
Webpack Visualizer
Webpack Visualizer¹⁰ provides a pie chart showing your bundle composition, allow-
ing to understand which dependencies contribute to the size of the overall result.
Webpack Chart¹¹ is another similar option.
⁹https://arkit.pro
¹⁰https://chrisbateman.github.io/webpack-visualizer/
¹¹https://alexkuz.github.io/webpack-chart/
Build Analysis 143
Treemaps
webpack-bundle-analyzer
¹²http://auxpack.com/
¹³https://www.npmjs.com/package/webpack-bundle-analyzer
¹⁴https://www.npmjs.com/package/source-map-explorer
¹⁵https://www.npmjs.com/package/bundle-wizard
Build Analysis 144
webpack-bundle-size-analyzer
webpack-bundle-size-analyzer¹⁶ emits a text based composition:
$ webpack-bundle-size-analyzer stats.json
react: 93.99 KB (74.9%)
purecss: 15.56 KB (12.4%)
style-loader: 6.99 KB (5.57%)
fbjs: 5.02 KB (4.00%)
object-assign: 1.95 KB (1.55%)
css-loader: 1.47 KB (1.17%)
<self>: 572 B (0.445%)
• inspectpack²⁷ has both a command line tool and a webpack plugin for finding
duplicate packages.
• find-duplicate-dependencies²⁸ achieves the same on an npm package level.
• depcheck²⁹ goes further and warns if there are redundant dependencies or
dependencies missing from the project.
²¹https://www.npmjs.com/package/bundle-stats-webpack-plugin
²²https://www.npmjs.com/package/webpack-bundle-diff
²³https://www.npmjs.com/package/size-plugin
²⁴https://github.com/trainline/webpack-bundle-delta
²⁵https://www.npmjs.com/package/unused-webpack-plugin
²⁶https://www.npmjs.com/package/remnants
²⁷https://www.npmjs.com/package/inspectpack
²⁸https://www.npmjs.com/package/find-duplicate-dependencies
²⁹https://www.npmjs.com/package/depcheck
Build Analysis 146
21.11 Conclusion
When you are optimizing the size of your bundle output, these tools are invaluable.
The official tool has the most functionality, but even basic visualization can reveal
problem spots. You can use the same technique with old projects to understand their
composition.
To recap:
• Webpack allows you to extract a JSON file containing information about the
build. The data can include build composition and timing.
• The generated data can be analyzed using various tools that give insight into
aspects such as the bundle composition.
• Performance budget allows you to set limits to the build size. Maintaining a
budget can keep developers more conscious of the size of the generated bundles.
• Understanding the bundles is the key to optimizing the overall size, what to
load and when. It can also reveal more significant issues, such as redundant
data.
Sometimes optimizations come with a cost. You could, for example, trade memory
for performance or end up making your configuration more complicated.
If you hit memory limits with webpack, you can give it more memory
with node --max-old-space-size=4096 node_modules/.bin/wp --mode
development kind of invocation. Size is given in megabytes, and in the
example you would give 4 gigabytes of memory to the process.
¹https://webpack.js.org/plugins/profiling-plugin/
²https://github.com/jantimon/cpuprofile-webpack-plugin
Performance 148
• Use faster source map variants during development or skip them. Skipping is
possible if you don’t process the code in any way.
• Use @babel/preset-env⁵ to transpile fewer features for modern browsers and
make the code more readable and more comfortable to debug while dropping
source maps.
• Skip polyfills during development. Attaching a package, such as core-js⁶, to the
development version of an application adds processing overhead.
• Polyfill less of Node and provide nothing instead. For example, a package could
be using Node process which in turn will bloat your bundle if polyfilled. See
webpack documentation⁷ for the default values.
• Starting from version 5, there’s a file system level cache⁸ that can be enabled by
setting cache.type = "filesystem". To invalidate it on configuration change,
you should set cache.buildDependencies.config = [__filename]. Webpack
handles anything watched by the build automatically including plugins, loaders,
and project files.
³https://www.npmjs.com/package/thread-loader
⁴https://www.npmjs.com/package/webpack-plugin-ramdisk
⁵https://www.npmjs.com/package/@babel/preset-env
⁶https://www.npmjs.com/package/core-js
⁷https://webpack.js.org/configuration/node/
⁸https://github.com/webpack/changelog-v5/blob/master/guides/persistent-caching.md
Performance 149
dontParse({
name: "react",
path: path.resolve(
__dirname, "node_modules/react/cjs/react.production.min.js",
),
}),
After this change, the application should be faster to rebuild, depending on the
underlying implementation. The technique can also be applied to production.
Given module.noParse accepts a regular expression if you wanted to ignore all
*.min.js files, you could set it to /\.min\.js/.
Not all modules support module.noParse. They should not have a reference
to require, define, or similar, as that leads to an Uncaught ReferenceError:
require is not defined error.
22.6 Conclusion
You can optimize webpack’s performance in multiple ways. Often it’s a good idea
to start with more accessible techniques before moving to more involved ones. The
exact methods you have to use depend on the project.
To recap:
¹²https://webpack.js.org/guides/build-performance/
¹³https://developers.google.com/web/fundamentals/performance/webpack/
VI Output
This part covers different output techniques webpack provides. You see how to
manage a multi-page setup, how to implement server-side rendering, and how to
use module federation to develop micro frontends.
23. Build Targets
Even though webpack is used most commonly for bundling web applications, it can
do more. You can use it to target Node or desktop environments, such as Electron.
Webpack can also bundle as a library while writing an appropriate output wrapper
making it possible to consume the library.
Webpack’s output target is controlled by the target field. You’ll learn about the
primary targets next and dig into library-specific options after that.
Web workers
The webworker target wraps your application as a web worker¹. Using web workers
is valuable if you want to execute computation outside of the main thread of the
application without slowing down the user interface. There are a couple of limitations
you should be aware of:
• You cannot use webpack’s hashing features when the webworker target is used.
• You cannot manipulate the DOM from a web worker. If you wrapped the book
project as a worker, it would not display anything.
Web workers and their usage are discussed in detail in the Web Workers
chapter.
¹https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
²https://www.npmjs.com/package/nodemon-webpack-plugin
Build Targets 155
23.4 Conclusion
Webpack supports targets beyond the web. Based on this, you can say name
“webpack” is an understatement considering its capabilities.
To recap:
• Webpack’s output target can be controlled through the target field. It defaults
to web but accepts other options too.
• Webpack can target the desktop, Node, and web workers in addition to its web
target.
• The Node targets come in handy if especially in Server-Side Rendering setups.
In practice, you have more dimensions. For example, you have to generate i18n
variants for pages. These ideas grow on top of the basic approaches. Here we’ll set
up single configuration based on which to experiment further.
¹https://www.npmjs.com/package/directory-tree-webpack-plugin
²https://developers.google.com/web/progressive-web-apps/
Multiple Pages 157
const {
MiniHtmlWebpackPlugin,
} = require("mini-html-webpack-plugin");
To generate multiple pages using the new helper, set up a configuration file:
webpack.multi.js
module.exports = merge(
{ mode: "production", entry: { app: "./src/multi.js" } },
parts.page({ title: "Demo" }),
parts.page({ title: "Another", url: "another" })
);
{
"scripts": {
"build:multi": "wp --config webpack.multi.js"
}
}
To control which entries are used on each page, use the chunks parameter of
parts.page. If you set it to chunks: [] for one of the pages, you should see nothing
on the page for example. While experimenting, match the name given at parts.entry.
The parameter allows capturing chunks generated by Bundle Splitting and doing this
would allow you to load a shared vendor bundle for all pages.
Twitter⁷ and Tinder⁸ case studies illustrate how the PWA approach can
improve platforms.
³https://github.com/webpack/webpack-pwa
⁴https://developer.mozilla.org/en/docs/Web/API/Service_Worker_API
⁵https://developers.google.com/web/tools/workbox/
⁶https://www.npmjs.com/package/workbox-webpack-plugin
⁷https://developers.google.com/web/showcase/2017/twitter
⁸https://medium.com/@addyosmani/a-tinder-progressive-web-app-performance-case-study-78919d98ece0
Multiple Pages 160
24.4 Conclusion
Webpack allows you to manage multiple page setups. The PWA approach allows the
application to be loaded as it’s used and webpack allows implementing it.
To recap:
• Webpack can be used to generate separate pages either through its multi-
compiler mode or by including all the page configuration into one.
• The multi-compiler configuration can run in parallel using external solutions,
but it’s harder to apply techniques such as bundle splitting against it.
• A multi-page setup can lead to a Progressive Web Application. In this case,
you use various webpack techniques to come up with an application that is
fast to load and that fetches functionality as required. Both two flavors of this
technique have their own merits.
¹https://www.npmjs.com/package/next
²https://www.npmjs.com/package/razzle
³https://facebook.github.io/jsx/
Server-Side Rendering 162
{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react"
]
}
Next, the React code needs a small entry point. For browser, we’ll render a div and
show an alert on click. For server, we return JSX to render.
As ES2015 style imports and CommonJS exports cannot be mixed, the entry point
has to be written in CommonJS style. Adjust as follows:
src/ssr.js
module.exports = {
mode: "production",
entry: { index: path.join(APP_SOURCE, "ssr.js") },
output: {
path: path.join(__dirname, "static"),
filename: "[name].js",
libraryTarget: "umd",
globalObject: "this",
},
module: {
rules: [
{
test: /\.js$/,
include: APP_SOURCE,
use: "babel-loader",
},
],
},
};
Server-Side Rendering 164
{
"scripts": {
"build:ssr": "wp --config webpack.ssr.js"
}
}
If you build the SSR demo (npm run build:ssr), you should see a new file at
./static/index.js. The next step is to set up a server to render it.
function renderMarkup(html) {
return `<!DOCTYPE html>
<html>
<head><title>SSR Demo</title><meta charset="utf-8" /></head>
<body>
<div id="app">${html}</div>
<script src="./index.js"></script>
</body>
</html>`;
}
Run the server now (node ./server.js) and go below http://localhost:8080, you
should see a “Hello World”. Clicking the text should show an alert and you should
see pre-rendered HTML in the source.
Even though there is a React application running now, it’s difficult to develop. If you
try to modify the code, nothing happens. The problem can be solved for example by
using webpack-dev-middleware⁴.
• How to deal with styles? Node doesn’t understand CSS related imports.
• How to deal with anything other than JavaScript? If the server side is processed
through webpack, this is less of an issue as you can patch it at webpack.
• How to run the server through something else other than Node? One option
would be to wrap the Node instance in a service you then run through your
host environment. Ideally, the results would be cached, and you can find more
specific solutions for this particular per platform (i.e. Java and others).
⁴https://www.npmjs.com/package/webpack-dev-middleware
Server-Side Rendering 166
Questions like these are the reason why solutions such as Next.js or razzle exist. They
have been designed to solve SSR-specific problems like these.
25.6 Prerendering
SSR isn’t the only solution to the SEO problem. Prerendering is an alternate
technique that is easier to implement. The point is to use a headless browser to render
the initial HTML markup of the page and then serve that to the crawlers. The caveat
is that the approach won’t work well with highly dynamic data.
The following solutions exist for webpack:
25.7 Conclusion
SSR comes with a technical challenge, and for this reason, specific solutions have
appeared around it. Webpack is a good fit for SSR setups.
To recap:
• Server-Side Rendering (SSR) can provide more for the browser to render
initially. Instead of waiting for the JavaScript to load, you can display markup
instantly.
• SSR also allows you to pass initial payload of data to the client to avoid
unnecessary queries to the server.
• Webpack can manage the client-side portion of the problem. It can be used to
generate the server as well if a more integrated solution is required. Abstrac-
tions, such as Next.js, hide these details.
• SSR does not come without a cost, and it leads to new problems as you need
better approaches for dealing with aspects, such as styling or routing. The server
and the client environment differ in essential manners, so the code has to be
written so that it does not rely on platform-specific features too much.
In the next chapter, we’ll learn about micro frontends and module federation.
26. Module Federation
Micro frontends¹ take the idea of microservices to frontend development. Instead of
developing the application or a site as a monolith, the point is to split it as smaller
portions programmed separately that are then tied together during runtime.
With the approach, you can use different technologies to develop other parts of the
application and have separate teams developing them. The reasoning is that splitting
up development this way avoids the maintenance costs associated with a traditional
monolith.
As a side effect, it enables new types of collaboration between backend and frontend
developers as they can focus on a specific slice of an application as a cohesive team.
For example, you could have a team focusing only on the search functionality or
other business-critical portion around a core feature.
Starting from webpack 5, there’s built-in functionality to develop micro frontends.
Module federation and gives you enough functionality to tackle the workflow
required by the micro frontend approach.
¹https://micro-frontends.org/
²https://github.com/module-federation/module-federation-examples/
³https://medium.com/swlh/webpack-5-module-federation-a-game-changer-to-javascript-architecture-
bcdd30e02669
Module Federation 169
<body>
<h1>Demo</h1>
<aside>
<ul>
<li><button>Hello world</button></li>
<li><button>Hello federation</button></li>
<li><button>Hello webpack</button></li>
</ul>
</aside>
<main>
The content should change based on what's clicked.
</main>
</body>
The idea is that as any button is clicked, the content is updated to match the text.
const configs = {
development: merge(
{ entry: ["webpack-plugin-serve/client"] },
parts.devServer()
),
production: {},
};
The configuration is a subset of what we’ve used in the book so far. It relies on the
following .babelrc:
.babelrc
{
"presets": [
"@babel/preset-react",
["@babel/preset-env", { "modules": false }]
]
}
{
"scripts": {
"build:mf": "wp --config webpack.mf.js --mode production",
"start:mf": "wp --config webpack.mf.js --mode development"
}
}
The idea is to have one script to run the project and one to build it.
If you want to improve the setup further, add Hot Module Replacement to it, as
discussed in the related chapter.
If you haven’t completed the book examples, check out the demonstration
from GitHub⁴ to find the configuration.
⁴https://github.com/survivejs-demos/webpack-demo
Module Federation 172
function App() {
const options = ["Hello world", "Hello fed", "Hello webpack"];
const [content, setContent] = React.useState("Changes on click.");
return (
<main className="max-w-md mx-auto space-y-8">
<h1 className="text-xl">Demo</h1>
<aside>
<ul className="flex space-x-8">
{options.map((option) => (
<li key={option}>
<button
className="rounded bg-blue-500 text-white p-2"
onClick={() => setContent(option)}
>
{option}
</button>
</li>
))}
</ul>
</aside>
<article>{content}</article>
</main>
);
}
Module Federation 173
The styling portion uses Tailwind setup from the Eliminating Unused CSS chapter
for styling so we can make the demonstration look better.
If you npm run start:mf, you should see the application running. In case you click
on any of the buttons, the selection should change.
import("./mf");
It’s using the syntax you likely remember from the Code Splitting chapter. Although
it feels trivial, we need to do this step as otherwise, the application would emit an
error while loading with ModuleFederationPlugin.
Module Federation 174
To test the new bootstrap and the plugin, adjust webpack configuration as follows:
...
...
If you run the application (npm run start:mf), it should still look the same.
In case you change the entry to point at the original file, you’ll receive an Uncaught
Error: Shared module is not available for eager consumption error in the
browser.
To get started, let’s split the header section of the application into a module of its
own and load it during runtime through module federation.
Module Federation 175
Note the singleton bits in the code above. In this case, we’ll treat the current code
as a host and mark react and react-dom as a singleton for each federated module to
ensure each is using the same version to avoid problems with React rendering.
We should also alter the application to use the new component. We’ll go through a
custom namespace, mf, which we’ll manage through module federation:
src/mf.js
...
function App() {
...
return (
<main className="max-w-md mx-auto space-y-8">
<h1 className="text-xl">Demo</h1>
<Header />
...
</main>
);
}
Module Federation 176
Next, we should connect the federated module with our configuration. It’s here
where things get more complicated as we have to either run webpack in multi-
compiler mode (array of configurations) or compile modules separately. I’ve gone
with the latter approach, as it works better with the current configuration.
exports.federateModule = ({
name,
filename,
exposes,
remotes,
shared,
}) => ({
plugins: [
new ModuleFederationPlugin({
name,
filename,
exposes,
remotes,
shared,
}),
],
});
⁵https://github.com/shellscape/webpack-plugin-serve/blob/master/test/fixtures/multi/webpack.config.js
Module Federation 177
The next step is more involved, as we’ll have to set up two builds. We’ll have to
reuse the current target and pass --component parameter to it to define which one to
compile. That gives enough flexibility for the project.
Change the webpack configuration as below:
webpack.mf.js
const shared = {
react: { singleton: true },
"react-dom": { singleton: true },
};
Module Federation 178
const componentConfigs = {
app: merge(
{
entry: [path.join(__dirname, "src", "bootstrap.js")],
},
parts.page(),
parts.federateModule({
name: "app",
remotes: { mf: "mf@/mf.js" },
shared,
})
),
header: merge(
{
entry: [path.join(__dirname, "src", "header.js")],
},
parts.federateModule({
name: "mf",
filename: "mf.js",
exposes: { "./header": "./src/header" },
shared,
})
),
};
To test, compile the header component first using npm run build:mf -- --component
header. Then, to run the built module against the shell, use npm run start:mf --
--component app.
If everything went well, you should still get the same outcome.
26.8 Conclusion
Module federation, introduced in webpack 5, provides an infrastructure-level solu-
tion for developing micro frontends.
To recap:
The technique can be valuable for other purposes, such as testing or adding files for
webpack to watch. In that case, you would set up a require.context within a file
which you then point to through a webpack entry.
If you are using TypeScript, make sure you have installed @types/webpack-
env² for require.context to work.
// Elsewhere in code
import(`translations/${target}.json`).then(...).catch(...);
The same idea works with require as webpack can then perform static analysis.
For example, require(assets/modals/${imageSrc}.js); would generate a context and
resolve against an image based on the imageSrc that was passed to the require.
²https://www.npmjs.com/package/@types/webpack-env
Dynamic Loading 184
When using dynamic imports, specify file extension in the path as that
keeps the context smaller and helps with performance.
27.5 Conclusion
Even though require.context is a niche feature, it’s good to be aware of it. It
becomes valuable if you have to perform lookups against multiple files available
within the file system. If your lookup is more complicated than that, you have to
resort to other alternatives that allow you to perform loading runtime.
To recap:
The next chapter shows how to use web workers with webpack.
³https://www.npmjs.com/package/scriptjs
⁴https://www.npmjs.com/package/little-loader
28. Web Workers
Web workers¹ allow you to push work outside of main execution thread of JavaScript,
making them convenient for lengthy computations and background work.
Moving data between the main thread and the worker comes with communication-
related overhead. The split provides isolation that forces workers to focus on logic
only as they cannot manipulate the user interface directly.
As discussed in the Build Targets chapter, webpack allows you to build your
application as a worker itself. To get the idea of web workers better, we’ll write a
small worker to bundle using webpack.
¹https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
Web Workers 187
return element;
};
After you have these two set up, it should work as webpack detects the Worker
syntax. As you click the text, it should mutate the application state when the worker
completes its execution. To demonstrate the asynchronous nature of workers, you
could try adding delay to the answer and see what happens.
28.5 Conclusion
The critical thing to note is that the worker cannot access the DOM. You can perform
computation and queries in a worker, but it cannot manipulate the user interface
directly.
To recap:
• Web workers allow you to push work out of the main thread of the browser.
This separation is valuable, especially if performance is an issue.
• Web workers cannot manipulate the DOM. Instead, it’s best to use them for
lengthy computations and requests.
• The isolation provided by web workers can be used for architectural benefit. It
forces the programmers to stay within a specific sandbox.
• Communicating with web workers comes with an overhead that makes them
less practical. As the specification evolves, this can change in the future.
See the Intl JavaScript API¹ to find out what utilities the browsers provide
to help with the problem.
Another way is to use webpack’s import() syntax and Dynamic Loading to set up a
small system of your own. That’s what we’ll do next.
translations/fi.json
module.exports = {
mode: "production",
entry: { index: path.join(APP_SOURCE, "i18n.js") },
module: {
rules: [
{
Internationalization 191
test: /\.js$/,
include: APP_SOURCE,
use: "babel-loader",
},
],
},
plugins: [new MiniHtmlWebpackPlugin()],
};
{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react"
]
}
{
"scripts": {
"build:i18n": "wp --config webpack.i18n.js",
}
}
Internationalization 192
src/i18n.js
import "regenerator-runtime/runtime";
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
useEffect(() => {
translate(language, "hello")
.then(setHello)
.catch(console.error);
}, [language]);
return (
<div>
<button onClick={changeLanguage}>Change language</button>
<div>{hello}</div>
</div>
);
};
root.setAttribute("id", "app");
document.body.appendChild(root);
If you build (npm run build:i18n) and run (npx serve dist) the application, you
should see that it’s loading the translation dynamically and as you click the button,
it’s changing the translation.
29.5 Conclusion
An internationalization and localization approach can be built on top of webpack.
Specific loaders can help in the task as you can push tasks like processing gettext PO
files to them.
To recap:
The next chapter covers various testing setups and tools that work with webpack.
30. Testing
Testing is a vital part of development. Even though techniques, such as linting, can
help to spot and solve issues, they have their limitations. Testing can be applied to
the code and an application on many different levels.
You can unit test a specific piece of code, or you can look at the application from the
user’s point of view through acceptance testing. Integration testing fits between
these ends of the spectrum and is concerned about how separate units of code operate
together.
Often you won’t need webpack to run your tests. Tools such as Jest¹, Cypress²,
Puppeteer³, and Playwright⁴ cover the problem well. Often there are ways to adapt
to webpack specific syntax in case you are using webpack features within your code.
30.1 Jest
Facebook’s Jest⁵ is an opinionated alternative that encapsulates functionality, includ-
ing coverage and mocking, with minimal setup. It can capture snapshots of data
making it valuable for projects where you have the behavior you would like to record
and retain.
Jest follows Jasmine⁶ test framework semantics, and it supports Jasmine-style asser-
tions out of the box. Especially the suite definition is close enough to Mocha so that
the current test should work without any adjustments to the test code itself. Jest
provides jest-codemods⁷ for migrating more complicated projects to Jest semantics.
¹https://jestjs.io/
²https://www.cypress.io/
³https://pptr.dev/
⁴https://playwright.dev/
⁵https://facebook.github.io/jest/
⁶https://www.npmjs.com/package/jasmine
⁷https://www.npmjs.com/package/jest-codemods
Testing 195
30.2 Mocking
Mocking is a technique that allows you to replace test objects. Consider using Sinon¹²
for this purpose as it works well with webpack.
const config = {
plugins: [
new webpack.NormalModuleReplacementPlugin(
/\.(gif|png|scss|css)$/,
"lodash/noop"
),
],
};
⁸https://facebook.github.io/jest/docs/en/configuration.html
⁹https://jestjs.io/docs/webpack
¹⁰https://www.npmjs.com/package/babel-jest
¹¹https://www.npmjs.com/package/babel-plugin-module-resolver
¹²https://www.npmjs.com/package/sinon
Testing 196
30.4 Conclusion
Webpack can be configured to work with a large variety of testing tools. Each tool
has its sweet spots, but they also have quite a bit of common ground.
To recap:
• Running testing tools allows you to benefit from webpack’s module resolution
mechanism.
• Sometimes the test setup can be quite involved. Tools like Jest remove most of
the boilerplate and allow you to develop tests with minimal configuration.
• You can find multiple mocking tools for webpack. They allow you to shape test
environment. Sometimes you can avoid mocking through design, though.
Setting up gh-pages
To get started, execute
¹https://www.npmjs.com/package/gh-pages
Deploying Applications 198
{
"scripts": {
"deploy": "gh-pages -d dist"
}
}
To make the asset paths work on GitHub Pages, output.publicPath field has to be
adjusted. Otherwise, the asset paths end up pointing at the root, and that doesn’t
work unless you are hosting behind a domain root (say survivejs.com) directly.
publicPath gives control over the resulting urls you see at index.html for instance.
If you are hosting your assets on a CDN, this would be the place to tweak.
In this case, it’s enough to set it to point the GitHub project as below:
webpack.config.js
After building (npm run build) and deploying (npm run deploy), you should have
your application from the dist/ directory hosted on GitHub Pages. You should find
it at https://<name>.github.io/<project> assuming everything went fine.
If you need a more elaborate setup, use the Node API that gh-pages
provides. The default command line tool it gives is enough for essential
purposes, though.
Deploying Applications 199
GitHub Pages allows you to choose the branch where you deploy. It’s
possible to use the master branch even as it’s enough for minimal sites
that don’t need bundling. You can also point below the ./docs directory
within your master branch and maintain your site.
1. Copy the old version of the site in a temporary directory and remove archive
directory from it. You can name the archival directory as you want.
2. Clean and build the project.
3. Copy the old version below dist/archive/<version>
4. Set up a script to call gh-pages through Node as below and capture possible
errors in the callback:
To get access to the generated files and their paths, consider using assets-
webpack-plugin⁵. The path information allows you to integrate webpack
with other environments while deploying.
To make sure clients relying on the older bundles still work after deploying
a new version, do not remove the old files until they are old enough. You
can perform a specific check on what to remove when deploying instead
of removing every old asset.
⁵https://www.npmjs.com/package/assets-webpack-plugin
⁶https://webpack.js.org/api/module-variables/
Deploying Applications 201
31.4 Conclusion
Even though webpack isn’t a deployment tool, you can find plugins for it.
To recap:
• It’s possible to handle the problem of deployment outside of webpack. You can
achieve this in an npm script for example.
• You can configure webpack’s output.publicPath dynamically. This technique
is valuable if you don’t know it compile-time and want to decide it later. This
is possible through the __webpack_public_path__ global.
32. Consuming Packages
Sometimes packages have not been packaged the way you expect, and you have to
tweak the way webpack interprets them. Webpack provides multiple ways to achieve
this.
32.1 resolve.alias
Sometimes packages do not follow the standard rules and their package.json
contains a faulty main field. It can be missing altogether. resolve.alias is the field
to use here as in the example below:
const config = {
resolve: {
alias: {
demo: path.resolve(
__dirname,
"node_modules/demo/dist/demo.js"
),
},
},
};
The idea is that if webpack resolver matches demo in the beginning, it resolves from
the target. You can constrain the process to an exact name by using a pattern like
demo$.
Light React alternatives, such as Preact¹ or Inferno², offer smaller size while trading
off functionality like propTypes and synthetic event handling. Replacing React with
¹https://www.npmjs.com/package/preact
²https://www.npmjs.com/package/inferno
Consuming Packages 203
a lighter alternative can save a significant amount of space, but you should test well
if you do this.
The same technique works with loaders too. You can use
resolveLoader.alias similarly. You can use the method to adapt a
RequireJS project to work with webpack.
32.2 resolve.modules
The module resolution process can be altered by changing where webpack looks for
modules. By default, it will look only within the node_modules directory. If you want
to override packages there, you could tell webpack to look into other directories first:
After the change, webpack will try to look into the my_modules directory first. The
method can be applicable in large projects where you want to customize behavior.
32.3 resolve.extensions
By default, webpack will resolve only against .js, .mjs, and .json files while
importing without an extension, to tune this to include JSX files, adjust as below:
32.4 resolve.plugins
resolve.plugins field allows you to customize the way webpack resolves modules.
directory-named-webpack-plugin³ is a good example as it’s mapping import foo
from "./foo"; to import foo from "./foo/foo.js";. The pattern is popular with
React and using the plugin will allow you to simplify your code. babel-plugin-
module-resolver⁴ achieves the same behavior through Babel.
³https://www.npmjs.com/package/directory-named-webpack-plugin
⁴https://www.npmjs.com/package/babel-plugin-module-resolver
Consuming Packages 204
You still have to point to a CDN and ideally provide a local fallback, so there is
something to load if the CDN does not work for the client:
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js\
"></script>
<script>
window.jQuery ||
document.write(
'<script src="js/jquery-3.1.1.min.js"><\/script>'
);
</script>
⁵https://webpack.js.org/configuration/externals/#externalstype
Consuming Packages 205
Injecting globals
imports-loader⁶ allows you to inject globals to modules. In the example below, import
$ from 'jquery'; is injected as a global to each:
const config = {
module: {
rules: [
{
test: /\.js$/,
loader: "imports-loader",
options: {
imports: ["default jquery $"],
},
},
],
},
};
Resolving globals
Webpack’s ProvidePlugin allows webpack to resolve globals as it encounters them:
const config = {
plugins: [new webpack.ProvidePlugin({ $: "jquery" })],
};
⁶https://www.npmjs.com/package/imports-loader
Consuming Packages 206
const config = {
test: require.resolve("react"),
loader: "expose-loader",
options: {
exposes: ["React"],
},
};
const config = {
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
],
};
You can use the same mechanism to work around problematic de-
pendencies. Example: new webpack.IgnorePlugin({ resourceRegExp:
/^(buffertools)$/ }).
const config = {
plugins: [
new webpack.ContextReplacementPlugin(
/moment[\/\\]locale$/,
/de|fi/
),
],
};
¹⁰https://www.npmjs.com/package/moment
Consuming Packages 208
There’s a Stack Overflow question¹¹ that covers these ideas in detail. See
also Ivan Akulov’s explanation of ContextReplacementPlugin¹².
WARNING in ../~/jasmine-promises/dist/jasmine-promises.js
Critical dependencies:
1:113-120 This seems to be a pre-built javascript file. Though this is \
possible, it's not recommended. Try to require the original source to g\
et better results.
@ ../~/jasmine-promises/dist/jasmine-promises.js 1:113-120
The warning can happen if a package points at a pre-built (i.e., minified and already
processed) file. Webpack detects this case and warns against it.
The warning can be eliminated by aliasing the package to a source version as
discussed above. Given sometimes the source is not available, another option is to tell
webpack to skip parsing the files through module.noParse. It accepts either a RegExp
or an array of RegExps and can be configured as below:
¹¹https://stackoverflow.com/questions/25384360/how-to-prevent-moment-js-from-loading-locales-with-
webpack/25426019
¹²https://iamakulov.com/notes/webpack-contextreplacementplugin/
¹³https://github.com/date-fns/date-fns/blob/main/docs/webpack.md
Consuming Packages 209
const config = {
module: { noParse: /node_modules\/demo\/index.js/ },
};
32.11 Conclusion
Webpack can consume most npm packages without a problem. Sometimes, though,
patching is required using webpack’s resolution mechanism.
To recap:
• Use webpack’s module resolution to your benefit. Sometimes you can work
around issues by tweaking resolution. Often it’s a good idea to try to push
improvements upstream to the projects themselves, though.
• Webpack allows you to patch resolved modules. Given specific dependencies
expect globals, you can inject them. You can also expose modules as globals as
this is necessary for certain development tooling to work.
¹⁴https://github.com/webpack/webpack/issues/1617
VIII Extending
Even though there are a lot of available loaders and plugins for webpack, it’s good
to be able to extend it. In this part, you go through a couple of short examples to
understand how to get started.
33. Extending with Loaders
As you have seen so far, loaders are one of the building blocks of webpack. If you
want to load an asset, you most likely need to set up a matching loader definition.
Even though there are a lot of available loaders¹, it’s possible you are missing one
fitting your purposes.
You’ll learn to develop a couple of small loaders next. But before that, it’s good to
understand how to debug them in isolation.
To have something to test, set up a loader that returns twice what’s passed to it:
loaders/demo-loader.js
¹https://webpack.js.org/loaders/
²https://www.npmjs.com/package/loader-runner
Extending with Loaders 212
There’s nothing webpack specific in the code yet. The next step is to run the loader
through loader-runner:
run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
If you run the script now (node ./run-loader.js), you should see output:
{
result: [ 'foobar\nfoobar\n' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: [],
missingDependencies: []
}
The output tells the result of the processing, the resource that was processed as a
buffer, and other meta information. The data is enough to develop more complicated
loaders.
It’s possible to refer to loaders installed to the local project by name instead
of resolving a full path to them. Example: loaders: ["babel-loader"].
Given webpack injects its API through this, the shorter function form (()
=> ...) cannot be used here.
If you want to pass a source map to webpack, give it as the third parameter
of the callback.
Running the demo script (node ./run-loader.js) again should give the same result
as before.
Extending with Loaders 214
The result should contain Error: Demo error with a stack trace showing where the
error originates.
But what’s the point? You can pass to loaders through webpack entries. Instead of
pointing to pre-existing files as you would in a majority of the cases, you could give
to a loader that generates code dynamically.
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
context: { emitFile: () => {} },
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
To implement the essential idea of asset loading, you have to do two things: emit the
file and return path to it.
To interpolate the file name, you need to use loader-utils⁴. Install it first:
⁴https://www.npmjs.com/package/loader-utils
Extending with Loaders 216
this.emitFile(url, content);
• this.emitWarning(<string>)
• this.emitError(<string>)
These calls should be used over console based alternatives. As with this.emitFile,
you have to mock them for loader-runner to work.
The next question is how to pass a file name to the loader.
Extending with Loaders 217
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
],
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
Extending with Loaders 218
To connect it to the loader, set it to capture name and pass it through webpack’s
interpolator:
loaders/demo-loader.js
module.exports = function(content) {
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
const { name } = this.getOptions();
const url = loaderUtils.interpolateName(this, name, { content });
...
};
{
result: [ 'export default __webpack_public_path__ + "demo.txt";' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: [],
missingDependencies: []
}
You can see that the result matches what the loader should have returned. You can
try to pass more options to the loader or use query parameters to see what happens
with different combinations.
It’s a good idea to validate options and rather fail hard than silently if the
options aren’t what you expect. schema-utils⁵ has been designed for this
purpose.
⁵https://www.npmjs.com/package/schema-utils
Extending with Loaders 219
import "!../loaders/demo-loader?name=foo!./main.css";
import "!../loaders/demo-loader?name=foo!./main.css";
import "!demo-loader?name=foo!./main.css";
You could also handle the loader definition through rules and publish it as an npm
package to consume.
Extending with Loaders 220
The official documentation⁶ covers the loader API in detail. You can see all
fields available through this there. For example, mode is exposed.
Webpack evaluates loaders in two phases: pitching and evaluating. If you are used to
web event semantics, these map to capturing and bubbling. The idea is that webpack
allows you to intercept execution during the pitching (capturing) phase. It goes
through the loaders left to right first and executes them from right to left after that.
⁶https://webpack.js.org/api/loaders/
Extending with Loaders 221
A pitch loader allows you shape the request and even terminate it. Set it up:
loaders/pitch-loader.js
return "pitched";
};
runLoaders(
{
resource: "./demo.txt",
loaders: [
...
path.resolve(__dirname, "./loaders/pitch-loader"),
],
...
},
(err, result) => (err ? console.error(err) : console.log(result))
);
If you run (node ./run-loader.js) now, the pitch loader should log intermediate
data and intercept the execution.
Extending with Loaders 222
return content;
};
module.exports.pitch = function () {
if (cache.has(this.resourcePath)) {
const item = cache.get(this.resourcePath);
A pitch loader can be used to attach metadata to the input to use later. In this example,
a cache was constructed during the pitching stage, and it was accessed during normal
execution.
Extending with Loaders 223
33.9 Conclusion
Writing loaders is fun in the sense that they describe transformations from a format
to another. Often you can figure out how to achieve something specific by either
studying either the API documentation or the existing loaders.
To recap:
You’ll learn to write plugins in the next chapter. Plugins allow you to intercept
webpack’s execution process, and they can be combined with loaders to develop more
advanced functionality.
34. Extending with Plugins
Compared to loaders, plugins are a more flexible means to extend webpack. You have
access to webpack’s compiler and compilation processes. It’s possible to run child
compilers, and plugins can work in tandem with loaders as MiniCssExtractPlugin
shows.
Plugins allow you to intercept webpack’s execution through hooks. Webpack itself
has been implemented as a collection of plugins. Underneath it relies on tapable¹
plugin interface that allows webpack to apply plugins in different ways.
You’ll learn to develop a couple of small plugins next. Unlike for loaders, there is no
separate environment where you can run plugins, so you have to run them against
webpack itself. It’s possible to push smaller logic outside of the webpack facing
portion, though, as this allows you to unit test it in isolation.
test();
⁵https://stackoverflow.com/questions/39923743/is-there-a-way-to-get-the-output-of-webpack-node-api-as-a-
string
Extending with Plugins 227
...
const DemoPlugin = require("./demo-plugin");
...
If you run the test (node ./test.js), you should see applying message at the console.
Given most plugins accept options, it’s a good idea to capture those and pass them
to apply.
Extending with Plugins 228
Running the plugin now would result in apply undefined kind of message given no
options were passed.
Adjust the configuration to pass an option:
plugins/test.js
plugins/demo-plugin.js
After running, you should see a lot of data. Especially options should look familiar
as it contains webpack configuration. You can also see familiar names like records.
If you go through webpack’s plugin development documentation⁶, you’ll see a
compiler provides a large number of hooks. Each hook corresponds to a specific stage.
For example, to emit files, you could listen to the emit event and then write.
Change the implementation to listen and capture compilation:
plugins/demo-plugin.js
Running the build should show more information than before because a compilation
object contains the whole dependency graph traversed by webpack. You have access
to everything related to it here, including entries, chunks, modules, assets, and more.
Many of the available hooks expose compilation, but sometimes they reveal
a more specific structure, and it takes a more particular study to understand
those.
compiler.hooks.thisCompilation.tap(
pluginName,
(compilation) => {
compilation.hooks.processAssets.tap(
⁷https://www.npmjs.com/package/webpack-sources
Extending with Plugins 231
{
name: pluginName,
// See lib/Compilation.js in webpack for more
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() =>
compilation.emitAsset(
name,
new sources.RawSource("hello", true)
)
);
}
);
}
};
If you run the test again (node ./test.js), you should see { demo: 'hello' } in the
console output.
Extending with Plugins 232
compilation.warnings.push("warning");
compilation.errors.push("error");
There’s a logging API that lets you pass messages to webpack. Consider the API
below:
You can use the API familiar from console so warning, error, and group amongst
other methods will work. See the logging documentation⁹ for further details.
34.9 Conclusion
When you begin to design a plugin, spend time studying existing plugins that are
close enough. Develop plugins piece-wise so that you validate one piece at a time.
Studying webpack source can give more insight, given it’s a collection of plugins
itself.
To recap:
• Plugins can intercept webpack’s execution and extend it making them more
flexible than loaders.
• Plugins can be combined with loaders. MiniCssExtractPlugin works this way.
The accompanying loader is used to mark assets to extract.
• Plugins have access to webpack’s compiler and compilation processes. Both
provide hooks for different stages of webpack’s execution flow and allow you
to manipulate it. Webpack itself works this way.
• Plugins can emit new assets and shape existing assets.
• Plugins can implement plugin systems of their own. HtmlWebpackPlugin is an
example of such a plugin.
Conclusion
As this book has demonstrated, webpack is a versatile tool. To make it easier to recap
the content and techniques, go through the checklists below.
General checklist
• Source maps allow you to debug your code in the browser during development.
They can also give better quality stack traces during production usage if you
capture the output. The Source Maps chapter delves into the topic.
• To keep your builds fast, consider optimizing. The Performance chapter dis-
cusses a variety of strategies you can use to achieve this.
• To keep your configuration maintainable, consider composing it. As webpack
configuration is JavaScript code, it can be arranged in many ways. The Com-
posing Configuration chapter discusses the topic.
• The way webpack consumes packages can be customized. The Consuming
Packages chapter covers specific techniques related to this.
• Sometimes you have to extend webpack. The Extending with Loaders and
Extending with Plugins chapters show how to achieve this. You can also work
on top of webpack’s configuration definition and implement an abstraction of
your own for it to suit your purposes.
Development checklist
• To get most out of webpack during development, use webpack-plugin-serve
(WPS) or webpack-dev-server (WDS). You can also find middlewares which
235
you can attach to your Node server during development. The Development
Server chapter covers both in greater detail.
• Webpack implements Hot Module Replacement (HMR). It allows you to
replace modules without forcing a browser refresh while your application is
running. The Hot Module Replacement appendix covers the topic in detail.
• Consider using Module Federation when a project gains complexity and it’s us-
ing multiple different technologies or it has multiple teams working on various
functionalities. The approach takes microservices to frontend development and
allows you to align your frontend with microbackends.
Production checklist
Styling
• Webpack inlines style definitions to JavaScript by default. To avoid this, sep-
arate CSS to a file of its own using MiniCssExtractPlugin or an equivalent
solution. The Separating CSS chapter covers how to achieve this.
• To decrease the number of CSS rules to write, consider autoprefixing your
rules. The Autoprefixing chapter shows how to do this.
• Unused CSS rules can be eliminated based on static analysis. The Eliminating
Unused CSS chapter explains the basic idea of this technique.
Assets
• When loading images through webpack, optimize them, so the users have less
to download. The Loading Images chapter shows how to do this.
• Load only the fonts you need based on the browsers you have to support. The
Loading Fonts chapter discusses the topic.
• Minify your source files to make sure the browser to decrease the payload the
client has to download. The Minifying chapter shows how to achieve this.
Caching
• To benefit from client caching, split a vendor bundle out of your application.
This way the client has less to download in the ideal case. The Bundle Splitting
236
chapter discusses the topic. The Adding Hashes to Filenames chapter shows how
to achieve cache invalidation on top of that.
• Use webpack’s code splitting functionality to load code on demand. The
technique is handy if you don’t need all the code at once and instead can push
it behind a logical trigger such as clicking a user interface element. The Code
Splitting chapter covers the technique in detail. The Dynamic Loading chapter
shows how to handle more advanced scenarios.
• Add hashes to filenames as covered in the Adding Hashes to Filenames chapter
to benefit from caching and separate a runtime to improve the solution further
as discussed in the Separating a Runtime chapter.
Optimization
• Use ES2015 module definition to leverage tree shaking. It allows webpack
to eliminate unused code paths through static analysis. See the Tree Shaking
chapter for the idea.
• Set application-specific environment variables to compile it production mode.
You can implement feature flags this way. See the Environment Variables
chapter to recap the technique.
• Analyze build statistics to learn what to improve. The Build Analysis chapter
shows how to do this against multiple available tools.
• Push a part of the computation to web workers. The Web Workers chapter covers
how to achieve this.
Output
• Clean up and attach information about the build to the result. The Tidying Up
chapter shows how to do this.
Conclusion
Webpack allows you to use a lot of different techniques to splice up your build. It
supports multiple output formats as discussed in the Output part of the book. Despite
its name, it’s not only for the web. That’s where most people use it, but the tool does
far more than that.
Appendices
As not everything that’s worth discussing fits into the main content, you can find
related material in brief appendices. These support the primary content and explain
specific topics, such as Hot Module Replacement, in greater detail. You will also learn
to troubleshoot webpack.
Comparison of Build Tools
Back in the day, it was enough to concatenate scripts together. Times have changed,
though, and distributing your JavaScript code can be a complicated endeavor. This
problem has escalated with the rise of single-page applications (SPAs) as they tend to
rely on many big libraries. For this reason, many loading strategies exist. The basic
idea is to defer loading instead of loading all at once.
The popularity of Node and npm¹¹, its package manager, provide more context. Before
npm became popular, it was hard to consume dependencies. There was a period when
people developed frontend specific package managers, but npm won in the end. Now
dependency management is more comfortable than before, although there are still
challenges to overcome.
Task runners
Historically speaking, there have been many build tools. Make is perhaps the best
known, and it’s still a viable option. Specialized task runners, such as Grunt and
Gulp were created particularly with JavaScript developers in mind. Plugins available
through npm made both task runners powerful and extendable. It’s possible to use
even npm scripts as a task runner. That’s common, particularly with webpack.
Make
Make¹³ goes way back, as it was initially released in 1977. Even though it’s an old
tool, it has remained relevant. Make allows you to write separate tasks for various
¹¹https://www.npmjs.com/
¹²https://bundlers.tooling.report/
¹³https://en.wikipedia.org/wiki/Make_%28software%29
Comparison of Build Tools 239
purposes. For instance, you could have different tasks for creating a production build,
minifying your JavaScript or running tests. You can find the same idea in many other
tools.
Even though Make is mostly used with C projects, it’s not tied to C in any way.
James Coglan discusses in detail how to use Make with JavaScript¹⁴. Consider the
abbreviated code based on James’ post below:
Makefile
PATH := node_modules/.bin:$(PATH)
SHELL := /bin/bash
libraries := vendor/jquery.js
all: $(app_bundle)
build/%.js: %.coffee
coffee -co $(dir $@) $<
clean:
rm -rf build
¹⁴https://blog.jcoglan.com/2014/02/05/building-javascript-projects-with-make/
Comparison of Build Tools 240
With Make, you model your tasks using Make-specific syntax and terminal com-
mands making it possible to integrate with webpack.
{
"scripts": {
"start": "wp --mode development",
"build": "wp --mode production",
"build:stats": "wp --mode production --json > stats.json"
}
}
These scripts can be listed using npm run and then executed using npm run <script>.
You can also namespace your scripts using a convention like test:watch. The
problem with this approach is that it takes care to keep it cross-platform.
Instead of rm -rf, you likely want to use utilities such as rimraf¹⁵ and so on. It’s
possible to invoke other tasks runners here to hide the fact that you are using one.
This way you can refactor your tooling while keeping the interface as the same.
Grunt
Grunt¹⁶ was the first famous task runner for frontend developers. Its plugin architec-
ture contributed towards its popularity. Plugins are often complicated by themselves.
As a result, when configuration grows, it can become tricky to understand what’s
going on.
¹⁵https://www.npmjs.com/package/rimraf
¹⁶http://gruntjs.com/
Comparison of Build Tools 241
grunt.loadNpmTasks("grunt-contrib-jshint");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.registerTask("default", ["lint"]);
};
In practice, you would have many small tasks for specific purposes, such as building
the project. An essential part of the power of Grunt is that it hides a lot of the wiring
from you.
Taken too far, this can get problematic. It can become hard to understand what’s
going on under the hood. That’s the architectural lesson to take from Grunt.
¹⁷http://gruntjs.com/sample-gruntfile
Comparison of Build Tools 242
Gulp
Gulp¹⁹ takes a different approach. Instead of relying on configuration per plugin, you
deal with actual code. If you are familiar with Unix and piping, you’ll like Gulp. You
have sources to match files, filters to operate on these sources, and sinks to pipe the
build results.
Here’s an abbreviated sample Gulpfile adapted from the project’s README to give
you a better idea of the approach:
Gulpfile.js
const paths = {
scripts: [
"client/js/**/*.coffee",
"!client/external/**/*.coffee",
],
};
// The default task (called when you run `gulp` from CLI).
gulp.task("default", ["watch", "scripts"]);
Given the configuration is code, you can always hack it if you run into troubles. You
can wrap existing Node packages as Gulp plugins, and so on. Compared to Grunt,
you have a clearer idea of what’s going on. You still end up writing a lot of boilerplate
for casual tasks, though. That is where newer approaches come in.
Script loaders
For a while, RequireJS²¹, a script loader, was popular. The idea was to provide an
asynchronous module definition and build on top of that. Fortunately, the standards
have caught up, and RequireJS seems more like a curiosity now.
²⁰https://www.npmjs.com/package/webpack-stream
²¹http://requirejs.org/
Comparison of Build Tools 244
RequireJS
RequireJS²² was perhaps the first script loader that became genuinely popular. It gave
the first proper look at what modular JavaScript on the web could be. Its greatest
attraction was AMD. It introduced a define wrapper:
// or
define(["./MyModule.js"], function (MyModule) {
return {
hello: function() {...}, // Export as a module function
};
});
This latter approach eliminates a part of the clutter. You still end up with code that
feels redundant. ES2015 and other standards solve this.
Jamund Ferguson has written an excellent blog series on how to port from
RequireJS to webpack²³.
²²http://requirejs.org/
²³https://gist.github.com/xjamundx/b1c800e9282e16a6a18e
Comparison of Build Tools 245
JSPM
Using JSPM²⁴ is entirely different than previous tools. It comes with a command-line
tool of its own that is used to install new packages to the project, create a production
bundle, and so on. It supports SystemJS plugins²⁵ that allow you to load various
formats to your project.
Bundlers
Task runners are great tools on a high level. They allow you to perform operations in a
cross-platform manner. The problems begin when you need to splice various assets
together and produce bundles. bundlers, such as Browserify, Brunch, or webpack,
exist for this reason and they operate on a lower level of abstraction. Instead of
operating on files, they operate on modules and assets.
Browserify
Dealing with JavaScript modules has always been a bit of a problem. The language
itself didn’t have the concept of modules till ES2015. Ergo, the language was stuck in
the ’90s when it comes to browser environments. Various solutions, including AMD²⁶,
have been proposed.
Browserify²⁷ is one solution to the module problem. It allows CommonJS modules
to be bundled together. You can hook it up with Gulp, and you can find smaller
transformation tools that allow you to move beyond the basic usage. For example,
watchify²⁸ provides a file watcher that creates bundles for you during development
saving effort.
The Browserify ecosystem is composed of a lot of small modules. In this way,
Browserify adheres to the Unix philosophy. Browserify is more comfortable to adopt
than webpack, and is, in fact, a good alternative to it.
²⁴http://jspm.io/
²⁵https://github.com/systemjs/systemjs#plugins
²⁶http://requirejs.org/docs/whyamd.html
²⁷http://browserify.org/
²⁸https://www.npmjs.com/package/watchify
Comparison of Build Tools 246
Brunch
Compared to Gulp, Brunch³¹ operates on a higher level of abstraction. It uses a
declarative approach similar to webpack’s. To give you an example, consider the
following configuration adapted from the Brunch site:
module.exports = {
files: {
javascripts: {
joinTo: {
"vendor.js": /^(?!app)/,
"app.js": /^app/,
},
},
stylesheets: {
joinTo: "app.css",
},
},
plugins: {
babel: {
presets: ["react", "env"],
},
postcss: {
processors: [require("autoprefixer")],
},
},
};
²⁹https://www.npmjs.com/package/splittable
³⁰https://www.npmjs.com/package/bankai
³¹http://brunch.io/
Comparison of Build Tools 247
Brunch comes with commands like brunch new, brunch watch --server, and brunch
build --production. It contains a lot out of the box and can be extended using
plugins.
Rollup
Rollup³² focuses on bundling ES2015 code. Tree shaking is one of its selling points
and it supports code splitting as well. You can use Rollup with webpack through
rollup-loader³³.
vite³⁴ is an opinionated wrapper built on top of Rollup and it has been designed
especially with Vue 3 in mind. nollup³⁵ is another wrapper and it comes with features
like Hot Module Replacement out of the box.
Webpack
You could say webpack³⁶ takes a more unified approach than Browserify. Whereas
Browserify consists of multiple small tools, webpack comes with a core that provides
a lot of functionality out of the box.
Webpack core can be extended using specific loaders and plugins. It gives control
over how it resolves the modules, making it possible to adapt your build to match
specific situations and workaround packages that don’t work correctly out of the box.
Compared to the other tools, webpack comes with initial complexity, but it makes
up for this through its broad feature set. It’s an advanced tool that requires patience.
But once you understand the basic ideas behind it, webpack becomes powerful.
To make it easier to use, tools such as create-react-app³⁷, poi³⁸, and instapack³⁹ have
been built around it.
³²https://www.npmjs.com/package/rollup
³³https://www.npmjs.com/package/rollup-loader
³⁴https://www.npmjs.com/package/vite
³⁵https://www.npmjs.com/package/nollup
³⁶https://webpack.js.org/
³⁷https://www.npmjs.com/package/create-react-app
³⁸https://poi.js.org/
³⁹https://www.npmjs.com/package/instapack
Comparison of Build Tools 248
Vite
Vite⁴⁰ is tool comparable to webpack. It comes with features like lazy loading, ESM,
JSX, and TypeScript support out of the box. The build functionality relies on Rollup
and the development server is custom code. Originally it was developed with Vue
in mind but since the scope of the tool has grown to support popular frameworks
like React. It’s possible to extend the tool using Vite specific plugins and also Rollup
plugins are supported making it a versatile solution.
Other Options
You can find more alternatives as listed below:
Conclusion
Historically there have been a lot of build tools for JavaScript. Each has tried to solve
a specific problem in its way. The standards have begun to catch up, and less effort
is required around basic semantics. Instead, tools can compete on a higher level and
push towards better user experience. Often you can use a couple of separate solutions
together.
To recap:
• Task runners and bundlers solve different problems. You can achieve similar
results with both, but often it’s best to use them together to complement each
other.
• Older tools, such as Make or RequireJS, still have influence even if they aren’t
as popular in web development as they once were.
• Bundlers like Browserify or webpack solve an important problem and help you
to manage complex web applications.
⁴⁹https://www.npmjs.com/package/assetgraph
⁵⁰https://www.npmjs.com/package/hyperlink
⁵¹https://www.npmjs.com/package/assetviz
⁵²https://www.npmjs.com/package/webpack-assetgraph-plugin
⁵³https://stealjs.com/
⁵⁴https://www.npmjs.com/package/blendid
⁵⁵https://swc-project.github.io/
⁵⁶https://packem.github.io/
⁵⁷https://www.npmjs.com/package/sucrase
Comparison of Build Tools 250
Enabling HMR
The following steps need to be enabled for HMR to work:
1. The development server has to run in the hot mode to expose the hot module
replacement interface to the client.
2. Webpack has to provide hot updates to the server and can be achieved using
webpack.HotModuleReplacementPlugin.
3. The client has to run specific scripts provided by the development server. They
will be injected automatically but can be enabled explicitly through entry
configuration.
4. The client has to implement the HMR interface through module.hot.accept
and optionally module.hot.dispose to clean module before replacing it.
⁵⁸https://www.npmjs.com/package/react-refresh-webpack-plugin
⁵⁹https://www.npmjs.com/package/vue-hot-reload-api
Hot Module Replacement 252
{
devServer: {
// Don't refresh if hot loading fails. Good while
// implementing the client interface.
hotOnly: true,
If you implement configuration like above without implementing the client interface,
you will most likely end up with an error:
No refresh
The message tells that even though the HMR interface notified the client portion of
the code of a hot update, nothing was done about it and this is something to fix next.
You should not enable HMR for your production configuration. It likely
works, but it makes your bundles bigger than they should be.
If you are using Babel, configure it so that it lets webpack control module
generation as otherwise, HMR logic won’t work! See the Loading JavaScript
chapter for the exact setup.
Hot Module Replacement 254
document.body.appendChild(demoComponent);
// HMR interface
if (module.hot) {
// Capture hot update
module.hot.accept("./component", () => {
const nextComponent = component();
demoComponent = nextComponent;
});
}
If you refresh the browser, try to modify src/component.js after this change, and
alter the text to something else, you should notice that the browser does not refresh at
all. Instead, it should replace the DOM node while retaining the rest of the application
as is.
The idea is the same with styling, React, Redux, and other technologies. Sometimes
you don’t have to implement the interface yourself even as available tooling takes
care of that for you.
entry: {
hmr: [
// Include the client code. Note host/post.
"webpack-dev-server/client?http://localhost:8080",
⁶¹https://www.npmjs.com/package/hot-accept-webpack-plugin
⁶²https://www.npmjs.com/package/module-hot-accept-loader
⁶³https://nativescript.org/blog/deep-dive-into-hot-module-replacement-with-webpack-part-two-handling-
updates/
Hot Module Replacement 257
Conclusion
HMR is one of those aspects of webpack that makes it attractive for developers
and webpack has taken its implementation far. To work, HMR requires both client
and server-side support. For this purpose, webpack-dev-server provides both. You
will have to take care with the client-side, though, and either find a solution that
implements the HMR interface or implement it yourself.
CSS Modules
Perhaps the most significant challenge of CSS is that all rules exist within global
scope, meaning that two classes with the same name will collide. The limitation is
inherent to the CSS specification, but projects have workarounds for the issue. CSS
Modules⁶⁴ introduces local scope for every module by making every class declared
within unique by including a hash in their name that is globally unique to the module.
{
use: {
loader: "css-loader",
options: {
modules: true,
},
},
},
After this change, your class definitions remain local to the files. In case you want
global class definitions, you need to wrap them within :global(.redButton) { ...
} kind of declarations.
⁶⁴https://github.com/css-modules/css-modules
CSS Modules 259
In this case, the import statement gives you the local classes you can then bind to
elements. Assume you had CSS as below:
app/main.css
body {
background: cornsilk;
}
.redButton {
background: red;
}
...
body remains as a global declaration still. It’s that redButton that makes the
difference. You can build component-specific styles that don’t leak elsewhere this
way.
CSS Modules allows composition to make it easier to work with your styles and you
can also combine it with other loaders as long as you apply them before css-loader.
Conclusion
CSS Modules solve the scoping problem of CSS by defaulting to local scope per file.
You can still have global styling, but it requires additional effort. Webpack can be set
up to support CSS Modules easily as seen above.
Searching with React
Let’s say you want to implement a rough little search for an application without a
proper backend. You could do it through lunr⁶⁷ and generate a static search index to
serve.
The problem is that the index can be sizable depending on the amount of the content.
The good thing is that you don’t need the search index straight from the start. You
can do something smarter instead. You can start loading the index when the user
selects a search field.
Doing this defers the loading and moves it to a place where it’s more acceptable for
performance. The initial search is going to be slower than the subsequent ones, and
you should display a loading indicator. But that’s fine from the user’s point of view.
Webpack’s Code Splitting feature allows doing this.
The beautiful thing is that this gives error handling in case something goes wrong
(network is down, etc.) and gives a chance to recover. You can also use Promise based
utilities like Promise.all for composing more complicated queries.
⁶⁷http://lunrjs.com/
Searching with React 262
In this case, you need to detect when the user selects the search element, load the data
unless it has been loaded already, and then execute search logic against it. Consider
the React implementation below:
App.js
return;
}
};
return (
<div className="app-container">
<div className="search-container">
<label>Search against README:</label>
<input type="text" value={value} onChange={onChange} />
</div>
<div className="results-container">
<Results results={results} />
</div>
</div>
);
};
function loadIndex() {
// Here's the magic. Set up `import` to tell Webpack
// to split here and load our search index dynamically.
//
// Shim Promise.all for older browsers and Internet Explorer!
return Promise.all([
import("lunr"),
Searching with React 264
import("../search_index.json"),
]).then(([{ Index }, { index, lines }]) => ({
index: Index.load(index),
lines,
}));
}
In the example, webpack detects the import statically. It can generate a separate
bundle based on this split point. Given it relies on static analysis, you cannot
generalize loadIndex in this case and pass the search index path as a parameter.
Conclusion
Beyond search, the approach can be used with routers too. As the user enters a route,
you can load the dependencies the resulting view needs. Alternately, you can start
loading dependencies as the user scrolls a page and gets adjacent parts with actual
functionality. import provides a lot of power and allows you to keep your application
lean.
You can find the full example⁶⁸ showing how it all goes together with lunr, React,
and webpack. The basic idea is the same, but there’s more setup in place.
To recap:
⁶⁸https://github.com/survivejs-demos/lunr-demo
⁶⁹http://lunrjs.com/
Troubleshooting
Using webpack can lead to a variety of runtime warnings or errors. Often a particular
part of the build fails for a reason or another. A basic process can be used to figure
out these problems:
11. If everything fails and you are convinced you have found a bug, report the
problem at the issue tracker that’s closest to it. Follow the issue template
carefully, and provide a minimal runnable example as that will help the
maintainers.
Sometimes it’s fastest to drop the error to a search engine and gain an answer that
way. Other than that this is an excellent debugging order. If your setup worked in
the past, you could also consider using commands like git bisect⁷⁶ to figure out what
has changed between the known working state and the current broken one.
You’ll learn about the most common errors next and how to deal with them.
DeprecationWarning
Node may give a DeprecationWarning especially after webpack has been updated to a
new major version. A plugin or a loader you are using may require updates. Often the
changes required are minimal. To figure out where the warning is coming from, run
webpack through Node: node --trace-deprecation node_modules/.bin/wp --mode
production.
It’s important to pass the --trace-deprecation flag to Node to see where the
warning originates from. Using --trace-warnings is another way and it will capture
the tracing information for all warnings, not only deprecations.
Troubleshooting 268
Conclusion
These are only examples of errors. Specific errors happen on the webpack side, but
the rest comes from the packages it uses through loaders and plugins. Simplifying
your project is a good step as that makes it easier to understand where the error
happens.
In most cases, the errors are fast to solve if you know where to look, but in the worst
case, you have come upon a bug to fix in the tooling. In that case, you should provide
a high-quality report to the project and help to resolve it.
Glossary
Given webpack comes with specific terminology, the principal terms and their
explanations have been gathered below.
• Asset is a general term for the media and source files of a project that are the
raw material used by webpack in building a bundle.
• Bundle is the result of bundling. Bundling involves processing the source
material of the application into a final bundle that is ready to use. A bundler
can generate more than one bundle.
• Bundle splitting offers one way of optimizing a build, allowing webpack to
generate multiple bundles for a single application. As a result, each bundle can
be isolated from changes affecting others, reducing the amount of code that
needs to be republished and therefore re-downloaded by the client and taking
advantage of browser caching.
• Chunk is a webpack-specific term that is used internally to manage the
bundling process. Webpack composes bundles out of chunks, and there are
several types of those.
• Code splitting produces more granular bundles than bundle splitting. To use
it, the developer has to enable it through specific calls in the source code. Using
a dynamic import() is one way.
• Entry refers to a file used by webpack as a starting point for bundling. An
application can have multiple entries and depending on configuration, each
entry can result in multiple bundles. Entries are defined in webpack’s entry
configuration. Entries are modules at the beginning of the dependency graph.
• Hashing refers to the process of generating a hash that is attached to the
asset/bundle path to invalidate it on the client. Example of a hashed bundle
name: app.f6f78b2fd2c38e8200d.js.
• Hot Module Replacement (HMR) refers to a technique where code running in
the browser is patched on the fly without requiring a full page refresh. When
an application contains complex state, restoring it can be difficult without HMR
or a similar solution.
Glossary 270
ple, running ES2015 code through Babel generates completely new ES5 code.
Without a source map, a developer would lose the link from where something
happens in the generated code and where it happens in the source code. The
same is true for style sheets when they run through a pre or post-processor.
• Static analysis - When a tool performs static analysis, it examines the code
without running it which is how tools like ESLint or webpack operate. Statically
analyzable standards, like ES2015 module definition, enable features like tree
shaking.
• Target options of webpack allow you to override the default web target. You
can use webpack to develop code for specific JavaScript platforms.
• Tree shaking is the process of dropping unused code based on static analysis.
ES2015 module definition allows this process as it’s possible to analyze in this
particular manner.