esbuild is lightning-fast, but it bundles everything by default. Sometimes, you want to tell it, "Hey, don’t bundle this specific Node.js module or browser global; assume it’s already available in the environment." This is crucial for optimizing build sizes, avoiding duplicate code, and leveraging pre-existing runtime features.
Let’s see this in action. Imagine we have a simple Node.js script that uses lodash and also relies on the browser’s window object.
// src/index.js
import _ from 'lodash';
function greet(name) {
return `Hello, ${name}! The time is ${window.performance.now()}.`;
}
console.log(greet('World'));
If we build this with esbuild without any special configuration, we’ll get a large bundle containing all of lodash and no reference to window.
# Initial build (no externalization)
esbuild src/index.js --bundle --outfile=dist/bundle.js --platform=node --format=cjs
Now, let’s make lodash and window external.
// src/index.js
import _ from 'lodash';
function greet(name) {
return `Hello, ${name}! The time is ${window.performance.now()}.`;
}
console.log(greet('World'));
# Build with externalization
esbuild src/index.js --bundle --outfile=dist/bundle.js --platform=node --format=cjs --external:lodash --external:window
The resulting dist/bundle.js will be much smaller and will look something like this (simplified):
// dist/bundle.js (after externalization)
// ... other esbuild boilerplate ...
const lodash = require('lodash');
function greet(name) {
return `Hello, ${name}! The time is ${window.performance.now()}.`;
}
console.log(greet('World'));
Notice how lodash is now a require('lodash') call, and window is still window. When this code runs in Node.js, require('lodash') will fetch the installed lodash package. The window global will cause a runtime error because it doesn’t exist in Node.js. Conversely, if we targeted the browser, window would be available, but require('lodash') would fail unless lodash was bundled or provided via a <script> tag.
The external flag in esbuild is your primary tool for this. It accepts multiple arguments, allowing you to specify any module or global you want to exclude from the bundle.
The syntax is straightforward:
esbuild src/index.js --bundle --outfile=dist/bundle.js --platform=<node|browser|neutral> --format=<iife|esm|cjs> --external:<module-name> --external:<another-module>
--platform: This is crucial.node: Assumes Node.js built-ins (fs,path, etc.) and installed npm packages are available viarequire().browser: Assumes browser globals (window,document,navigator, etc.) are available. It will not automatically resolve Node.js-stylerequire()calls for npm packages unless you’re using a bundler/loader setup for the browser (like with Webpack’sresolve.aliasor a similar mechanism).neutral: Tries to be compatible with both, often by relying onimportfor ES modules andrequirefor CommonJS.
--format: Determines the output module system (iife,esm,cjs).--external:<module-name>: This is where you list your externals.
Common Use Cases & How They Work:
-
Node.js Libraries with Peer Dependencies:
- Problem: You’re building a library intended to be consumed by other Node.js projects. You use a package like
reactas a peer dependency. You don’t want to bundlereactinto your library; you want the consuming project to provide it. - Command:
esbuild src/my-library.js --bundle --outfile=dist/my-library.js --platform=node --format=cjs --external:react - Why it works: The output will contain
const react = require('react');. The Node.js runtime of the consuming application will resolverequire('react')from its ownnode_modules.
- Problem: You’re building a library intended to be consumed by other Node.js projects. You use a package like
-
Browser Applications Using CDNs:
- Problem: You’re building a browser app and want to load large libraries like
react,react-dom, orlodashfrom a CDN to speed up initial loads or benefit from browser caching. - Setup:
- Your
index.htmlincludes script tags:<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="dist/app.js"></script> - Your esbuild command:
esbuild src/app.js --bundle --outfile=dist/app.js --platform=browser --format=iife --external:react --external:react-dom
- Your
- Why it works: esbuild generates an IIFE (Immediately Invoked Function Expression) that assumes
ReactandReactDOMare globally available (because they were loaded by the preceding<script>tags inindex.html). It doesn’t include their code.
- Problem: You’re building a browser app and want to load large libraries like
-
Leveraging Browser Globals:
- Problem: Your code directly uses browser APIs like
localStorage,fetch, orperformance. - Command:
esbuild src/browser-app.js --bundle --outfile=dist/browser-app.js --platform=browser --format=esm --external:localStorage --external:fetch --external:performance - Why it works: While esbuild usually knows about browser globals when
platform=browser, explicitly marking themexternalcan sometimes be necessary if esbuild’s inference isn’t perfect or if you want to be absolutely explicit. It tells esbuild not to try and resolve these as modules or include any polyfills for them, assuming they exist in the target environment.
- Problem: Your code directly uses browser APIs like
-
Isolating Node.js Built-ins for Specific Platforms:
- Problem: You’re building a frontend framework plugin that might be run in Node.js (e.g., for SSR) but shouldn’t bundle Node.js modules like
fsorpath. - Command:
esbuild src/plugin.js --bundle --outfile=dist/plugin.js --platform=browser --external:fs --external:path - Why it works: For a
browserplatform, esbuild normally wouldn’t try to resolvefsorpathanyway, as they aren’t browser APIs. However, if you were building aneutralplatform or wanted to prevent accidental inclusion, marking them external ensures they are treated as ambient globals that should be provided by the runtime environment, not bundled.
- Problem: You’re building a frontend framework plugin that might be run in Node.js (e.g., for SSR) but shouldn’t bundle Node.js modules like
-
Sharing Code Between Node.js and Browser with Externalized Dependencies:
- Problem: You have a core logic that needs to run in both environments but relies on different implementations or global availability for certain things.
- Strategy: You’d typically have separate build processes. For the Node.js build, you’d externalize npm packages. For the browser build, you’d externalize browser globals or specific CDN-loaded libraries. The
--externalflag is applied independently for each target platform.
A subtle point: when you use --external:some-module for a node platform, esbuild generates require('some-module'). If you’re targeting esm format for Node.js, it would generate import ... from 'some-module'. The external flag tells esbuild to not include the code for some-module in its output and instead rely on the runtime’s module resolution.
When esbuild sees import { x } from 'y' and y is marked as external, it doesn’t analyze the contents of y. It simply outputs an import or require statement for y. This is why it’s critical that the environment where your bundled code runs actually provides y – either as an installed npm package (for Node.js) or as a global variable (for browsers loaded via script tags). If you try to run Node.js code that externalized window in a browser, you’ll get a ReferenceError: window is not defined (because window is a browser global, not a Node.js module). If you run browser code that externalized fs in Node.js, you’ll get a ReferenceError: fs is not defined (because fs is a Node.js module, not a browser global).
The external flag is not just about reducing bundle size; it’s about correctly defining the boundaries between your code and its execution environment.
If you’re using a bundler like Webpack or Rollup, they have similar concepts (e.g., externals in Webpack, external in Rollup). esbuild’s --external flag is its direct equivalent, offering a highly performant way to achieve the same result.
The next challenge is often managing different externalization strategies for different environments or build targets within the same project.