A build that works isn't necessarily lean. This lesson covers actively measuring and shrinking a Vite production bundle: the bundle visualizer, manual vendor chunk splitting, writing genuinely tree-shakeable imports, and tuning minification and the browser target.
1A Working Build Isn't Necessarily a Lean Build
A successful build can still ship far more JavaScript than users actually need. Build optimization means actively measuring and reducing the shipped bundle, rather than assuming Rollup's defaults are automatically optimal for a specific app's dependency graph.
2Visualizing the Bundle
Before making any optimization changes, rollup-plugin-visualizer generates an interactive treemap of the final bundle, showing exactly which packages and modules consume the most space — the most valuable first step for any optimization effort, since it replaces guessing with data.
3Manual Chunk Splitting for Vendor Code
By default, third-party dependencies are bundled together with application code, so any app-code change invalidates the browser cache for dependencies too. build.rollupOptions.output.manualChunks can split stable vendor code, like React itself, into its own chunk that stays cached across deploys where only app code changed.
4Tree-Shaking Depends on Real ESM
Rollup can only remove unused exports from code written as genuine ES modules with static import/export statements — it can't safely analyze CommonJS require() code. Preferring ESM-native libraries and importing specific named exports, rather than a library's entire default export, keeps tree-shaking effective.
5Minification and the Build Target
Vite minifies with esbuild by default for speed, with Terser available as an alternative for marginally smaller output at the cost of build time. The build.target option controls which JavaScript syntax level output assumes, and targeting only modern browsers avoids unnecessary legacy transpilation, producing smaller output.
6Step-by-Step Breakdown
A Working Build Isn't Necessarily a Lean Build. Your app builds successfully and works — but is it shipping a 2MB JavaScript bundle to every visitor on a phone with a slow connection? Build optimization is about actively measuring and shrinking what you ship, not just trusting that Rollup's defaults are automatically optimal for your specific app.
Visualizing the Bundle. Before optimizing anything, measure it. The rollup-plugin-visualizer plugin generates an interactive treemap of your final bundle after a build, showing exactly which packages and modules take up the most space — the single most useful first step in any optimization effort.
What's the recommended first step before attempting to optimize a bundle's size?
- →Visualize the bundle to see exactly what's taking up space
- →Guess which dependencies are large and remove them
Manual Chunk Splitting for Vendor Code. By default, third-party dependencies get bundled alongside your app code, meaning any app code change invalidates the cache for your dependencies too. build.rollupOptions.output.manualChunks lets you split stable vendor code (like React itself) into its own chunk, so it stays cached across deploys where only your app code changed.
Tree-Shaking Depends on Real ESM. Rollup can only remove unused exports (tree-shake) from code written as genuine ES modules with static import/export statements — it can't safely analyze old-style CommonJS require() code. Prefer ESM-native libraries, and import only the specific functions you need rather than an entire library's default export.
Why does import _ from 'lodash' typically bundle much more code than import { debounce } from 'lodash-es'?
- →The default export pulls in the whole library, defeating tree-shaking
- →lodash-es is written in a faster language
Minification and the Build Target. Vite minifies JavaScript with esbuild by default (fast) and can be configured to use Terser instead (slightly smaller output, slower build). build.target controls which JavaScript syntax level the output assumes — targeting only modern browsers lets Vite skip legacy transpilation, producing smaller, faster code.
Mastery Achieved. You now have a real build optimization toolkit: visualizing the bundle before making changes, manually splitting stable vendor code for better caching, writing genuinely tree-shakeable imports, and tuning minification and the browser target. This closes out the Vite section — next, you'll move into Advanced Hooks, starting with useId.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Targeting a modern build.target assumes recent browser versions across the board.
Fully supported with a modern target.
Verify target syntax support against your minimum supported Safari version.
Fully supported with a modern target.
Accessibility (A11y)
1Smaller Bundles Improve Time-to-Interactive for Assistive Technology Users
Users relying on screen readers or switch devices are often equally or more sensitive to slow, janky page loads — bundle optimization work has a real, if indirect, accessibility benefit.
SEO Implications
- 1
Bundle Size Directly Affects Core Web Vitals
A smaller, well-split JavaScript bundle reduces Largest Contentful Paint and Interaction to Next Paint on real devices, both of which factor into Google's Core Web Vitals-based ranking signals.
Best Practices
Re-run the Visualizer After Every Significant Dependency Change
A new library or major version bump can silently add substantial bundle weight — periodically re-checking the treemap catches regressions before they reach production.
Split Vendor Code That Changes Rarely From App Code That Changes Often
Isolating stable dependencies like React into their own manualChunks entry maximizes how often returning users get a full cache hit on your largest, least-frequently-changing code.
Frequent Bugs
A tiny utility function import somehow adds hundreds of kilobytes to the bundle.
Check whether the import pulls in a library's entire default export instead of one named function — switch to a named import from an ESM-native package (or the library's /esm entry point) so tree-shaking can remove the unused code.
The bundle size looks identical before and after adding manualChunks configuration.
manualChunks changes how code is split across files, not necessarily the total combined size — its benefit is caching behavior across deploys, not raw byte count. Verify by checking whether the vendor chunk's hash stays the same across a deploy that only changed app code.
Real-World Examples
Cutting a Dashboard's Initial Bundle by Half
A dashboard app's visualizer treemap revealed that a rarely used charting library was bundled into the main chunk even though only one settings page used it. Lazy-loading that page's charting component with React.lazy, combined with a manualChunks entry for React itself, reduced the initial JavaScript payload by roughly 50%.
const AdvancedChart = React.lazy(() => import('./AdvancedChart'));
// vite.config.ts
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'react-dom'] } },
},
}