// message.jsconst message = "Hello World";printMessage();
// printMessage.jsfunction printMessage() { console.log(message);}
message.js
is included before printMessage.js
?message
?<script>
order is importantAs your application grows, this becomes a problem.
And there's an additional problem:
All module systems are slow to initialize when a lot of modules need to be loaded.
This is due to the roundtrip problem:
Module bundlers...
...analyze JavaScript modules by reading their source code...
...to generate compatible and efficient script JavaScript files (bundle).
Module bundlers leverage the fact that all
module systems share the same mental model:
This model is called the dependency graph.
A dependency graph can be simple...
...or complex...
...or very complex.
Module bundlers...
...try to gather as much information as possible about the dependency graph in order to apply clever optimizations.
Module bundlers...
...try to do as much as possible on build time...
...so that there's less to do on runtime.
AMD | CommonJS | ESM | |
---|---|---|---|
Analyzable | With limitations | With limitations | Yes |
Resolving | Asynchronous | Synchronous | Implementation specific |
Linking | Copy by value | Copy by value | Live binding |
Scoping | Function wrapper | Function wrapper | New language environment |
The best way to understand webpack is to follow the steps webpack takes when someone runs:
webpack
Compiler
compilations
compilation
processwatch
modeEverything inside webpack is built of plugins which rely on a common interface, called Tapable:
npm install tapable
A tapable instance exposes hooks...
...and plugins can listen on these hooks.
Let's take a closer look at the shouldEmit
hook.
The shouldEmit
hook can be used by plugins to tell the compiler
that no files should be written on disk.
NoEmitOnErrorsPlugin
which returns false if an error is found in the compilation stats.Compiler
triggers the shouldEmit
hook after all compilation hooks have been triggered, asking "anyone" if the next emitAssets
hook should be triggered?You might wonder:
NoEmitOnErrorsPlugin
get activated?optimization.noEmitOnErrors
in your webpack.config.js
.WebpackOptionsApply
is a good starting point if you want to know which webpack option triggers which plugin.Compiler
calls the make
hook which tells the compilation to add the first module or modules to the dependency graph. These are called entry dependencies.Compilation
watch
modeNormalModuleFactory
.Module
parser
to parse the source codegenerator
to generate source codeUntil now, no source code has been loaded from disk. We just have a path to a module, which can even be relative:
"./src/main.js""../../other.js""react"
NormalModuleFactory
calls a resolver
to resolve the module specifier.Resolver
Resolving is complicated nowadays...
Resolvers should at least mimic the Node.js resolving algorithm:
But for compatibility reasons, it should also look for:
browser
entry in package.json
module
entry in package.json
bower_components
folders (not activated by default anymore)bower.json
(not activated by default anymore)Resolving in webpack is done by enhanced-resolve:
Typical processing rules include loader configurations:
{ test: /\.jsx?$/, include: [resolve(projectRoot, "src")], use: [ { loader: "babel-loader", options: { ...babelConfig(), cacheDirectory: true, }, }, ],},
Heads up
All rules will be applied, not just the first one that matches.
{ test: /app\.js$/, use: ["babel-loader"],},{ test: /\.jsx?$/, use: ["babel-loader"],},
Applies the babel-loader
twice to app.js
.
Compilation
starts the module build.loader-runner
, which reads the file contents and processes the loader pipeline.A loader is a function that:
Loaders work on a per-file basis.
The simplest loader:
module.exports = function (content) { return content;};
A simplified file-loader
:
module.exports = function (content) { const url = generateHashedUrl(content); this.emitFile(url, content); return "export default " + JSON.stringify(url) + ";";};
this.emitFile()
tells webpack to emit a file withreturn
the JavaScript representation of that fileThe loader-runner
executes the loaders from right to left:
use: ["css-loader", "sass-loader"],// like: cssLoader(sassLoader(fileContent));
The sass-loader
receives Sass source code and produces CSS source code:
use: ["css-loader", "sass-loader"],// like: cssLoader(sassLoader(fileContent));
The css-loader
receives CSS source code and produces JavaScript code:
use: ["css-loader", "sass-loader"],// like: cssLoader(sassLoader(fileContent));
Before the actual execution, the loader-runner
performs a pitching phase from left to right.
This allows loaders to intercept the execution.
For instance, the thread-loader
uses the pitching phase to push the execution to worker threads:
use: ["thread-loader", "css-loader", "sass-loader"],
The result of the loader pipeline should either be:
Parser
Webpack uses acorn to parse JavaScript modules.
If you want to find out how an AST looks like: astexplorer.net
lib/dependencies/CommonJsRequireDependencyParserPlugin.js
Once a module has been built and parsed, all the previous steps starting from resolve are repeated until all dependencies have been processed.
The dependency graph might now look like this:
optimizeDependencies
, the FlagDependencyUsagePlugin
starts to walk through the dependency graph while keeping track of exports and imports.This works reliable with ESMs:
import {a, b} from "some-module";
export {a, b, c, d, e, f};
Only a and b from "some-module"
are imported.
But doesn't work with CommonJS:
const someModule = require("some-module");
module.exports = {a, b, c, d, e, f};
What properties of someModule
are used?
Also during optimizeDependencies
, the
SideEffectsFlagPlugin
looks for modules that have no side-effects.
Consider the following situation:
import {a} from "some-library";
// some-library/index.jsimport {b} from "./b.js";export const a = "a";export {b};
// some-library/b.jsexport const b = "b";loginUser();
Webpack can't remove the unused export b
because b.js
contains a side-effect that needs to be executed.
It's impossible to detect that a module contains no side-effects if there's code to be executed.
Thus, module authors need to tell webpack explicitly in their package.json
that their module won't execute side-effects when being imported:
{ "name": "some-library", "sideEffects": false}
The chunk graph tells webpack which module to put in which file (chunk).
The chunk graph get's more interesting when you're using async import()
:
import("./a.js") .then(a => { console.log("module a has been loaded"); });
This tells webpack that a.js
and all its dependencies are only necessary when the import()
function is called.
b.css
a.js
importing b.js
asynchronouslyCalculating the optimal chunk graph is a trade-off between code duplication...
...and big initial bundle size.
There are also other important things to consider:
lib/optimize/ModuleConcatenationPlugin.js
ModuleConcatenationPlugin
tries to merge as many modules as possible into one scope (function wrapper).This feature is also known as scope hoisting.
It decreases the bundle size and drastically improves the app startup time.
Old benchmark when webpack did not implement scope hoisting.
module
, chunk
, ... to source codeThe webpack output is an IIFE that can be divided into two parts:
(function (modules) { // module runtime})([ // module function wrappers function () { // source code of module 1 }, function () { // source code of module 2, 3, 4 }])
Write ECMAScript modules.
Write idiomatic JavaScript code (listen to Benedikt & Sigurd).
Make sure that your babel- or typescript-loader generate ESMs:
// .babelrc.js{ presets: [ // babel 7 does that by default // babel 6 ["env", { // don't transpile modules modules: false }], ],}
// tsconfig.json{ "compilerOptions": { "module": "esnext", },}
Don't do side-effects in the top-level module scope...
import {loginUser} from "./loginUser.js";loginUser() // please don't do that .then(() => { console.log("User logged in"); })
...unless it's the entry module.
Tell webpack about your good intentions by setting sideEffects: false
in your package.json
{ "sideEffects": false}
Use the module
field in your package.json
to point webpack to ESM code instead of an unoptimizable dist.js
.
{ "main": "./dist.js" "module" "./esm.js"}
esm.js
should still be transpiled down to ES5, except imports and exports.
Use import()
where possible to defer less important code.
async function openModal() { const modal = await import("./modal.js"); modal.open();}
Use the magic webpack comment webpackPrefetch: true
.
import(/* webpackPrefetch: true */"./modal.js");
Webpack will use <link rel="prefetch">
to prefetch chunks in browser idle times.
Enable persistent loader caches if supported to reduce the build time.
{ loader: "babel-loader", options: { cacheDirectory: true }}
(or use the cache-loader)
Restrict the scope of your loader pipelines to your src
folder.
{ test: /\.jsx?$/, include: [resolve(projectRoot, "src")], use: [ "babel-loader" ]},
Remember that each import
of a .scss
or .less
file is a separate Sass or Less compilation.
import "./modal.less"; // <---- may contain duplicate cssimport "./map.less"; // <---- may contain duplicate css
Listen to webpack's performance hints:
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
b | Toggle blackout mode |
f | Toggle fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
w | Pause/Resume the presentation |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |