Shrinking your webpack bundle
A common first step in any front-end performance pass is cutting the size of what you ship. Webpack gives you several levers to pull, from environment flags to loader configuration. Here's the practical sequence.
Start with production mode and the right environment
Webpack 4's mode flag tells the bundler which environment you're targeting. Set it to 'production' for builds that go live:
// webpack.config.js
module.exports = {
mode: 'production',
};
This one flag toggles a set of optimizations — minification, removal of development-only code from libraries, and more. For webpack 3, apply optimization.minimize and the bundled UglifyJS plugin manually.
Related to this is the NODE_ENV environment variable. Libraries check it at build time to decide whether to include development checks and warnings. Vue, for instance, runs extra validation when NODE_ENV isn't production:
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React behaves the same way, pulling in a heavier development build with warnings:
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
Those branches are dead weight in production. In webpack 4, remove them with:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
In webpack 3, the equivalent is DefinePlugin:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
Both approaches do the same thing: they replace every process.env.NODE_ENV occurrence with the value you specify. The replacement happens before minification, which then strips the now-unreachable if branches because comparing "production" to 'production' is always false.
Minify at two levels
Minification removes whitespace, shortens variable names, and compresses code in other ways. Webpack supports both bundle-level minification and loader-specific options, and you should use both.
Bundle-level minification works on the entire compiled output. The process looks like this: you write readable source, webpack compiles it into a bundle, and then a minifier compresses that result.
Webpack 4 enables bundle-level minification automatically in production mode using UglifyJS. To turn it off, use development mode or set optimization.minimize to false. In webpack 3, you need to add the UglifyJS plugin to the plugins section of your config explicitly:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
Loader-specific options handle what the minifier can't reach. When css-loader imports a CSS file, it converts the content into a JavaScript string:
/* comments.css */
.comment {
color: black;
}
That string is opaque to the bundle-level minifier. You need the loader to compress its own output:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Leverage ES modules for tree-shaking
Using ES module syntax enables tree-shaking, where webpack walks the dependency tree and eliminates exports that are never imported. If a file exports several things but your app uses only one, webpack won't create a module boundary for the unused exports, and the minifier removes the dead variables:
// comments.js
export const render = () => { return 'Rendered!'; };
export const commentRestEndpoint = '/rest/comments';
// index.js
import { render } from './comments.js';
render();
This also works for libraries written with ES modules. Any minifier with dead-code elimination — Babel Minify or Google Closure Compiler, for example — will handle the removal once webpack has flagged what's unused.
Compress images before they hit the wire
Images often account for over half of page weight. Three loaders can reduce that:
url-loaderinlines small files as Base64 data URLs when you set alimit. Files under that threshold become part of the JavaScript, eliminating an HTTP request:
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loaderdoes the same thing but uses URL encoding instead of Base64. Because SVG files are plain text, this encoding is noticeably smaller:
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loadercompresses JPG, PNG, GIF, and SVG files that pass through it. It doesn't embed anything, so it works alongside the other two. To avoid duplicating it in multiple rules, declare it once withenforce: 'pre':
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
The loader's defaults are solid for most projects. For finer control, its options documentation and Addy Osmani's image optimization guide both cover what to adjust.
Trim dependencies you don't fully use
Dependencies typically constitute more than half of a bundle's JavaScript size, and some of that is unused. Lodash v4.17.4 minifies to 72 KB, but if you only call 20 of its methods, roughly 65 KB is unnecessary. Moment.js 2.19.1 weighs 223 KB minified, of which 170 KB comes from localization files for languages you may never render.
You can optimize these libraries without replacing them. A collection of proven approaches for webpack is maintained in a GitHub repository dedicated to library optimizations.
Cut Module Boilerplate With Scope Hoisting
By default, webpack wraps every bundled module in a function to isolate it, producing output like this:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
That wrapper was historically necessary for CommonJS and AMD modules. But it costs bytes and runtime performance for every single module in the bundle.
ES modules changed the picture: they can be safely combined without individual function wrappers. Scope hoisting (also called module concatenation), which webpack 3 introduced and webpack 4 enables by default in production, exploits exactly that. Instead of keeping each module separate and resolving imports at runtime, it inlines the imported code:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
After concatenation, the equivalent code looks like:
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
The call to require disappears, the imported render function is referenced directly, and the intermediate module entry is gone entirely. The result is fewer modules and measurably less overhead.
If you're on webpack 4, make sure this behavior is on by setting optimization.concatenateModules to true:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
Webpack 3 users need to add the ModuleConcatenationPlugin explicitly:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Working With Non-Webpack Code: Use externals
When only part of a project is built with webpack and the rest isn't, both sides may load overlapping dependencies. A typical case is a video hosting site: the player widget is a webpack build, while the surrounding page uses plain scripts. If they share libraries, those libraries get downloaded twice.
Webpack's externals option prevents this duplication by telling the bundler to leave certain modules out of the bundle and resolve them from an external source instead.
Dependencies Exposed on window
The simplest case is when the non-webpack code loads a dependency that ends up as a global variable. Map the module name to that variable name:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
With this configuration, webpack no longer bundles react or react-dom. Instead, the imports are rewritten to reference the globals:
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
Dependencies That Are AMD Packages
A more complex scenario: the non-webpack code does not expose libraries on window, but loads them as AMD packages. There is still a workaround.
Compile the webpack portion as an AMD bundle and alias the imported module names to their canonical library URLs:
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack then wraps the application logic in define() and lists those URLs as required dependencies:
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
If the surrounding page loads its dependencies from the same URLs, both end up sharing one download, as the second request is served from the AMD loader's cache.
Quick Checklist
- Run webpack 4 in production mode.
- Minify at the bundle level and turn on loader-level minification options.
- Replace
NODE_ENVwith the literalproductionto strip development branches. - Prefer ES modules in your source to give tree shaking a chance.
- Compress images before they reach the bundler.
- Look for dependency-specific knobs — many libraries ship a leaner production build.
- Enable scope hoisting to merge modules and drop wrappers.
- Route shared libs through
externalswhen a page mixes webpack and non-webpack scripts.



