esbuild’s define flag lets you swap out global constants at build time, effectively making them disappear from your final bundle and injecting their values directly.
Imagine you’ve got a feature flag, FEATURE_ENABLED, that you want to toggle on or off for different builds. Instead of manually editing your source code, you can tell esbuild to replace every instance of FEATURE_ENABLED with true or false during the build process.
Here’s a simplified example of how it works. Let’s say your source code (src/index.js) looks like this:
// src/index.js
const isProduction = process.env.NODE_ENV === 'production';
const API_URL = 'http://localhost:3000';
function logMessage(message) {
if (isProduction) {
console.log(message);
} else {
console.log(`[DEV] ${message}`);
}
}
function fetchData() {
console.log(`Fetching data from ${API_URL}`);
// ... actual fetch logic
}
logMessage('App started');
fetchData();
And you want to build a production version where API_URL is https://api.example.com and isProduction is always true.
You’d run esbuild like this:
esbuild src/index.js --bundle --outfile=dist/bundle.js --define:isProduction=true --define:API_URL="'http://localhost:3000'"
Notice the quotes around 'http://localhost:3000'. When you use define for string literals, you need to provide the quoted string value. esbuild will then replace API_URL with the literal string 'http://localhost:3000' in the generated code.
After running this command, your dist/bundle.js will effectively look like this (whitespace and comments removed for clarity):
const isProduction = true;
const API_URL = 'http://localhost:3000';
function logMessage(message) {
if (isProduction) {
console.log(message);
} else {
console.log(`[DEV] ${message}`);
}
}
function fetchData() {
console.log(`Fetching data from ${API_URL}`);
}
logMessage('App started');
fetchData();
If you wanted to build a development version where isProduction is false and the API URL is local:
esbuild src/index.js --bundle --outfile=dist/bundle.js --define:isProduction=false --define:API_URL="'http://localhost:3000'"
The generated dist/bundle.js would then be:
const isProduction = false;
const API_URL = 'http://localhost:3000';
function logMessage(message) {
if (isProduction) {
console.log(message);
} else {
console.log(`[DEV] ${message}`);
}
}
function fetchData() {
console.log(`Fetching data from ${API_URL}`);
}
logMessage('App started');
fetchData();
This mechanism is incredibly powerful for several reasons:
- Tree Shaking: When you define
isProductionastrue, esbuild can statically analyze your code. It sees that theelseblock inlogMessagewill never be executed. Because of this, it can safely remove that entireelseblock from the final bundle. This is pure dead code elimination, leading to smaller file sizes. - Configuration Management: You can externalize configuration values like API endpoints, feature flags, or even environment-specific settings into your build process. This keeps your source code clean and avoids hardcoding sensitive or environment-dependent information.
- Performance: By inlining values and removing dead code, the final JavaScript is more efficient. There are no runtime lookups for these constants, and less code to parse and execute.
The most surprising thing is how effectively this can optimize your code. When you define a constant like isProduction = true, esbuild doesn’t just substitute the text; it performs a full static analysis. It understands control flow and can eliminate entire branches of code that become unreachable. This means that even code written inside an if (isProduction) block will be completely absent from your production bundle if isProduction is defined as true.
The define flag accepts multiple key-value pairs. Each pair is a substitution. The key is the identifier you want to replace, and the value is what it will be replaced with. For boolean or numeric values, you can pass them directly (e.g., --define:MY_NUM=123). For strings, you must wrap the value in quotes (e.g., --define:MY_STRING="'hello world'"). If you forget the inner quotes for strings, esbuild will try to interpret "hello world" as a variable name, which will likely lead to a build error or unexpected runtime behavior.
The next step is understanding how to integrate this with your package manager’s scripts for automated builds.