Your application might consist of hundreds of files. To run those files in production you need to optimize them. There are many ways to optimize your source code for production. Webpack is helping you here.
yarn add --dev webpack webpack-cli
Webpack is a built tool that “bundles” code for development or production, depending on its configured mode. All configuration is usually done within webpack.config.js
at project root level. You configure one or more JS files as an entry point, depending on whether you have a Single- or Multi Page application. Then you configure an output path. Webpack will create a dependency graph, meaning it traverses all your imported JS modules and keeps track of the references. You can also configure loaders to make Webpack pre-process/transform certain files such as CSS files, TypeScript files, image files and many more. Webpack plugins allow for further optimization such as asset management and injection of environment variables. In the end Webpack will produce one or more optimized bundle files.
Configuration
All configuration should be done in webpack.config.js
located in your project root folder. That file exports the Webpack configuration as a Node.js CommonJS module which means that you can require(...)
other modules and use control flow expressions, constants, variables and functions. You should avoid creating large configuration files, but instead split them up into multiple ones.
# loading webpack with configuration webpack --config webpack.config.js // or simply webpack
Note that webpack will not alter any code other than import
and export
statements. If you are using other ES2015 features, make sure to use a transpiler such as Babel.
Entry
A single entry as you would use for Single Page Applications:
module.exports = { entry: './path/to/my/entry/file.js' };
or multiple (named) entries for Multi Page applications:
module.exports = { entry: { pageOne: './src/pageOne/index.js', pageTwo: './src/pageTwo/index.js', pageThree: './src/pageThree/index.js' } };
Output
Single chunk
This configuration would output a single bundle.js
file into the dist
directory
module.exports = { output: { filename: 'bundle.js', } };
Multiple chunks
If your configuration creates more than a single “chunk” (as with multiple entry points or when using plugins like CommonsChunkPlugin) you can ensure that each file has a unique name:
module.exports = { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name].js', path: __dirname + '/dist' } }; // writes to disk: ./dist/app.js, ./dist/search.js
Hashed chunks
You should create bundle files that contain their hash values in their names, so that they are unique. That is important to prevent caching issues, for example showing old content despite new files – only because they have the same name.
module.exports = { //... output: { path: '/home/proj/cdn/assets/[hash]', publicPath: 'https://cdn.example.com/assets/[hash]/' } };
Dynamic runtime variables
In cases where the eventual publicPath of output files isn't known at compile time, it can be left blank and set dynamically at runtime
__webpack_public_path__ = myRuntimePublicPath; // rest of your application entry
Loaders
As already mentioned in the intro, loaders allow you to pre-process files as you import
or “load” them. The recommended way is to specify them in the webpack.config.js
file, though it is also possible inline or via CLI. Loaders are evaluated/executed from right to left or from bottom to top regarding the rules
array configuration below. Loaders can be synchronous or asynchronous, run in NodeJS environment, can be configured via an options
object and emit additional arbitrary files. Also plugins can enhance their functionality.
TypeScript loader
Configure Webpack, so you can convert TypeScript to JavaScript:
yarn add --dev ts-loader
module.exports = { module: { rules: [ { test: /\.ts$/, use: 'ts-loader' } ] } };
Loading CSS
yarn add -dev style-loader css-loader
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', ], }, ], }, };
Loading images
yarn add --dev file-loader
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader', ], }, ], }, };
Loading fonts
yarn add --dev file-loader
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader', ], }, ], }, };
Now when decalring your fonts the url(...)
directive will be picked up by webpack.
Plugins
That is an example on how a Plugin looks like:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tap(pluginName, compilation => { console.log('The webpack build process is starting!!!'); }); } } module.exports = ConsoleLogOnBuildWebpackPlugin;
And here is an example on how to use/configure a plugin:
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm const webpack = require('webpack'); //to access built-in plugins const path = require('path'); module.exports = { entry: './path/to/my/entry/file.js', output: { filename: 'my-first-webpack.bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.(js|jsx)$/, use: 'babel-loader' } ] }, plugins: [ new webpack.ProgressPlugin(), new HtmlWebpackPlugin({template: './src/index.html'}) ] };
Cleaning the dist folder
yarn add --dev clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { plugins: [ new CleanWebpackPlugin(), ], };
Webpack DevServer
yarn add --dev webpack-dev-server webpack-dev-middleware express
Add devServer
and output
publicPath
to webpack.config.js
:
const path = require('path'); module.exports = { mode: 'development', entry: { app: './src/index.js', print: './src/print.js', }, devtool: 'inline-source-map', devServer: { contentBase: './dist', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/', }, };
The publicPath
will be used within our server script as well in order to make sure files are served correctly on http://localhost:3000
We configure devtool
in order to make it easier to track down errors and warnings, JavaScript offers source maps, which map your compiled code back to your original source code
const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const app = express(); const config = require('./webpack.config.js'); const compiler = webpack(config); // Tell express to use the webpack-dev-middleware and use the webpack.config.js // configuration file as a base. app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath, })); // Serve the files on port 3000. app.listen(3000, function () { console.log('Example app listening on port 3000!\n'); });
"scripts": { "watch": "webpack --watch", "start": "webpack-dev-server --open", "build": "webpack" },
Run npm start
. If you now change any of the source files and save them, the web server will automatically reload after the code has been compiled.
This tells webpack-dev-server
to serve the files from the dist
directory on localhost:8080
. webpack-dev-server doesn’t write any output files after compiling. Instead, it keeps bundle files in memory and serves them as if they were real files mounted at the server’s root path
Hot module replacement
HMR allows all kinds of modules to be updated at runtime without the need for a full refresh. The following only works if you are NOT using Dev Server with the NodeJS API.
devServer: { contentBase: './dist', hot: true, },
When using Webpack Dev Server with the NodeJS API, don’t put the dev server options on the webpack configuration object. Instead, pass them as a second parameter upon creation. For example:
const webpackDevServer = require('webpack-dev-server'); const webpack = require('webpack'); const config = require('./webpack.config.js'); const options = { contentBase: './dist', hot: true, host: 'localhost', }; webpackDevServer.addDevServerEntrypoints(config, options); const compiler = webpack(config); const server = new webpackDevServer(compiler, options); server.listen(5000, 'localhost', () => { console.log('dev server listening on port 5000'); });
And if you’re using webpack-dev-middleware
you have to install and configure webpack-hot-middleware:
yarn add --dev webpack-hot-middleware
plugins: [ new webpack.HotModuleReplacementPlugin(), ]
Next, add 'webpack-hot-middleware/client'
into the entry
array.
var webpack = require('webpack'); var webpackConfig = require('./webpack.config'); var compiler = webpack(webpackConfig); app.use(require("webpack-dev-middleware")(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath })); app.use(require("webpack-hot-middleware")(compiler));
Adjusting your code to use HMR
if (module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated printMe module!'); document.body.removeChild(element); element = component(); // Re-render the "component" to update the click handler document.body.appendChild(element); }) }
Targets
Because JavaScript can be written for both server and browser, webpack offers multiple deployment targets that you can set in your webpack configuration.
Target node
will compile for usage in a Node.js-like environment (uses Node.js require
to load chunks and not touch any built in modules like fs
or path
).
module.exports = { target: 'node' };
Code splitting
You can split your code into various bundles which can then be loaded on demand or in parallel. There are three general approaches to code splitting available: Entry Points, SplitChunksPlugin and Dynamic Imports.
Entry points
The easiest way to split code is by defining an additional entry.
const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, };
The big downside to this is that the two resulting bundles might have duplicated code. We can prevent duplication using dependOn
:
const path = require('path'); module.exports = { mode: 'development', entry: { index: { import: './src/index.js', dependOn: 'shared' }, another: { import: './src/another-module.js', dependOn: 'shared' }, shared: 'lodash', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, };
SplitChunksPlugin
A better way to prevent duplication is by using SplitChunksPlugin
(formerly CommonsChunkPlugin
).
const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, optimization: { splitChunks: { chunks: 'all', }, }, };
We should now see that the duplicate dependency got removed from our index.bundle.js
and another.bundle.js
and extracted to its own file.
More useful plugins are mini-css-extract-plugin
for splitting CSS out from the main application and bundle-loader
to split code and lazy load the resulting bundles (or promise-loader
to use Promises).
Dynamic imports
const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', }, output: { filename: '[name].bundle.js', chunkFilename: '[name].bundle.js', publicPath: 'dist/', path: path.resolve(__dirname, 'dist'), }, };
chunkFilename
determines the name of non-entry chunk files. Now, instead of statically importing lodash
, we’ll use dynamic importing to separate a chunk:
// src/index.js function getComponent() { return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }).catch(error => 'An error occurred while loading the component'); } getComponent().then(component => { document.body.appendChild(component); })
Here /* webpackChunkName: “lodash” */ is not just any comment, it is a so called magic comment that gets interpreted – in this case as the file name for the generated chunk.
As import()
returns a promise, it can be used with async
functions. However, this requires using a pre-processor like Babel and the Syntax Dynamic Import Babel Plugin.
Variabels in dynamic imports
// imagine we had a method to get language from cookies or other storage const language = detectVisitorLanguage(); import(`./locale/${language}.json`).then(module => { // do something with the translations });
import(`./locale/${language}.json`)
will cause every .json
file in the ./locale
directory to be bundled into the new chunk. At run time, when the variable language
has been computed, any file like english.json
or german.json
will be available for consumption.
Prefetching/Preloading modules
The following will result in <link rel="prefetch" href="login-modal-chunk.js">
being appended in the head of the page, which will instruct the browser to prefetch in idle time the login-modal-chunk.js
file.
import(/* webpackPrefetch: true */ 'LoginModal');
Bundle Analysis
You can find a bunch of tools in the Webpack documentation that lets you analyze the output to check where modules have ended up or how much space they use.
Environment variables
webpack --env.NODE_ENV=local --env.production --progress
const path = require('path'); module.exports = env => { // Use env.<YOUR VARIABLE> here: console.log('NODE_ENV: ', env.NODE_ENV); // 'local' console.log('Production: ', env.production); // true return { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, }; };
Troubleshooting Webpack
I kept getting messages like this, for example when using expressJS:
Critical dependencies: the request of a dependency is an expression
The solution is to install and configure webpack-node-externals which creates an externals function that ignores node_modules
when bundling in Webpack.
var nodeExternals = require('webpack-node-externals'); ... module.exports = { ... target: 'node', // in order to ignore built-in modules like path, fs, etc. externals: [nodeExternals()], // in order to ignore all modules in node_modules folder ... };
Why is that feature not implemented in Webpack right away? Why does it need to install and configure an external package? I don’t know.