Build Optimization
Bundlers do more than combine files. They also apply optimizations that reduce how much code you ship to users. Knowing what they do helps you in two ways. You write code that bundles well, and you can work out what is going on when a production build behaves differently from a development build.
Removing Unused Code with Tree Shaking
Tree shaking is the process of eliminating code that is imported but never actually used. The name comes from the idea of shaking a tree so the dead leaves fall off.
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.js — only `add` is used, so `multiply` is removed from the bundle
import { add } from "./math.js";
console.log(add(1, 2)); // 3
Tree shaking works because ES modules have static imports. The bundler can analyze at build time exactly which exports are used. This is one reason why import/export syntax is preferred over CommonJS require().
Shrinking Code with Minification
Minification removes whitespace, shortens variable names, and applies other transformations to reduce file size without changing behavior:
// Before minification
function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price;
}
return total;
}
// After minification
function calculateTotal(t) {
let o = 0;
for (const l of t) o += l.price;
return o;
}
Most bundlers handle minification automatically in production mode. Vite uses a fast default minifier and can be configured to use alternatives such as terser when needed.
Debugging with Source Maps
Minified code is unreadable. Source maps solve this by mapping the bundled output back to your original source files, so browser DevTools show your actual code when debugging.
// vite.config.js
export default {
build: {
sourcemap: true,
},
};
Managing Environment Variables
Build tools let you inject environment variables that differ between development and production. In Vite, variables prefixed with VITE_ are exposed to your client-side code:
# .env
VITE_API_URL=https://api.example.com
// In your source code
console.log(import.meta.env.VITE_API_URL);
// "https://api.example.com"
The values are replaced at build time, so they become hardcoded strings in the bundle. That is why you should never put a secret in a client-side environment variable.
Importing Assets in Your Code
Modern bundlers let you import non-code assets like images, CSS, and JSON directly from your modules:
import logo from "./logo.png"; // resolved to a URL
import "./styles.css"; // injected into the page
import data from "./data.json"; // parsed as an object
document.getElementById("logo").src = logo;
console.log(data.version); // reads from JSON
The bundler handles hashing filenames for cache-busting and optimizing assets for production.
Comparing Development and Production Builds
In development you want the build to be fast and the code easy to debug. In production you want the output to be small and to run well.
| Aspect | Development | Production |
|---|---|---|
| Build speed | Fast, uses HMR | Slower, full optimization |
| Source maps | Full | Optional or hidden |
| Minification | No | Yes |
| Tree shaking | Usually not applied | Full |
| Error messages | Detailed | Minimal |
A typical package.json makes this distinction through separate scripts:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
Choosing the Right Bundler
Use this guide to choose a bundler:
- New web apps — start with Vite. It covers most use cases with almost no configuration.
- Complex enterprise apps — Webpack gives you fine-grained control over every aspect of the build.
- Publishing a library — Rollup produces the cleanest, smallest output for npm packages.
- Quick prototypes — Parcel gets you running with zero setup.
- Fastest builds — esbuild is the fastest option when you need minimal features.