Webpack is actually designed to bundle everything into your project, making the idea of excluding things feel like fighting the system.
Let’s see what happens when we set up externals in Webpack. Imagine you have a project that uses React and Lodash. You want to serve these from a CDN instead of bundling them into your main application file.
Here’s a webpack.config.js:
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development', // or 'production'
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
externals: {
react: 'React',
lodash: 'lodash',
},
};
And your src/index.js:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
console.log('React:', React);
console.log('Lodash:', _);
const element = React.createElement('h1', null, 'Hello from Webpack!');
ReactDOM.render(element, document.getElementById('app'));
If you run npx webpack with this configuration, Webpack will process src/index.js, see the import React and import _, but because they’re listed in externals, it won’t try to find them in your node_modules or bundle them. Instead, it expects them to be available globally in the environment where your bundle.js will run.
Your dist/bundle.js will look something like this (simplified):
// dist/bundle.js (simplified output)
// ... other webpack boilerplate ...
// This is where the magic happens for externals:
// Webpack assumes 'React' and 'lodash' are already defined in the global scope.
// It essentially replaces your imports with references to these global variables.
var __WEBPACK_EXTERNAL_MODULE_react__ = __webpack_require__(1); // This is a placeholder, actually it's just referencing the global
var __WEBPACK_EXTERNAL_MODULE_lodash__ = __webpack_require__(2); // Same here
// Your code is then compiled assuming these globals exist:
var element = __WEBPACK_EXTERNAL_MODULE_react__.createElement('h1', null, 'Hello from Webpack!');
__WEBPACK_EXTERNAL_MODULE_react_dom__.render(element, document.getElementById('app'));
// ... more webpack boilerplate ...
The key is that Webpack doesn’t include the code for react or lodash in bundle.js. It’s a promise: "When this script runs, I expect React and lodash to be defined globally."
To make this work, you’d need to include script tags for React and Lodash before your bundle.js in your index.html:
<!DOCTYPE html>
<html>
<head>
<title>Webpack Externals Example</title>
</head>
<body>
<div id="app"></div>
<!-- Load from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<!-- Your bundled application -->
<script src="dist/bundle.js"></script>
</body>
</html>
This setup is often used for libraries like React, Vue, Angular, jQuery, or Lodash when you want to leverage browser caching for these large, stable dependencies, or when you’re working with a pre-existing global environment that already provides them.
The externals configuration tells Webpack to treat certain import or require statements as "external" dependencies. Instead of bundling the module’s code, Webpack will generate code that assumes the module is available globally or via another mechanism. The string value associated with each external ('React', 'lodash') specifies the global variable name that Webpack should expect.
When you’re defining externals, you’re essentially mapping the module identifier used in your import statements (e.g., 'react') to the name of the global variable that will hold the module’s exports (e.g., 'React'). This mapping is crucial for Webpack to correctly generate the code that references these external modules.
The most surprising thing is that externals don’t inherently care how the external module becomes available; they just expect it to be there. You could load them via <script> tags, a separate JavaScript file, or even inject them via a browser extension. Webpack’s job is simply to not bundle them and to generate code that references the expected global.
The externals configuration can also accept more complex patterns. For instance, if you’re using a library that exposes itself in different ways depending on the module system (like CommonJS vs. AMD), you can specify different global names. A common pattern for libraries that support multiple module formats is to use a function or an object for the externals value.
For example, if a library my-library is exposed as MyLibrary globally but as mylib via CommonJS:
// webpack.config.js
module.exports = {
// ... other config
externals: {
'my-library': {
commonjs: 'mylib', // For require('mylib')
root: 'MyLibrary', // For <script src="...">MyLibrary</script>
amd: 'mylib', // For define(['mylib'], ...)
},
},
};
This allows Webpack to generate compatible code regardless of how your project or other dependencies are loading my-library. The root property is particularly useful for UMD (Universal Module Definition) bundles, where the library is designed to work in various environments, including browser global scope.
One thing most people don’t realize is that the externals configuration is not just for external CDNs. It’s also the primary mechanism for building libraries with Webpack. When you’re creating a library to be published on npm, you’d often use externals to prevent your library from bundling its own dependencies (like React if your library uses it). This allows the consuming application to manage the versions of those shared dependencies, preventing multiple copies from being included in the final bundle and avoiding version conflicts. This is why many popular libraries have peerDependencies in their package.json and use externals in their build configuration.
The next logical step after mastering externals is understanding how to configure Webpack for library mode itself, using output.library and output.libraryTarget.