The Bundle Analysis Gap
You deployed your AI-generated app, and the Lighthouse performance score is 40. The JavaScript bundle is 500KB, which takes 5 seconds to download and parse on a mobile phone. You check the bundle and find it includes moment.js (67KB, for date formatting that could be done with native Intl), lodash (71KB, for utility functions that could be done with native JavaScript), and three icon libraries (each 20-50KB). This is the bundle analysis gap, and it is one of the most common performance failures in AI-generated apps. AI assistants add dependencies without checking bundle size, which results in bloated bundles that slow down the app. Here are the 5 reasons AI assistants ship bloated bundles, and the production checklist to fix them.
The direct answer is that bundle analysis is the practice of inspecting your JavaScript bundle to identify large dependencies, unused code, and optimization opportunities. AI assistants add dependencies without checking their bundle size, because the LLM does not consider the production impact of each dependency. The 5 reasons are: no bundle analysis, no tree shaking, no code splitting, heavy dependencies, and no bundle budget. For more on performance, see our article on the performance regression trap.
Reason 1: No Bundle Analysis
The most common reason AI assistants ship bloated bundles is the lack of bundle analysis. Without analyzing the bundle, you do not know which dependencies are large, which code is unused, or where the optimization opportunities are. The fix is to use a bundle analyzer (e.g., @next/bundle-analyzer for Next.js, rollup-plugin-visualizer for Vite, webpack-bundle-analyzer for Webpack) that visualizes the bundle as a treemap, showing the size of each dependency. For more on build tools, see our article on the build cache architecture.
Reason 2: No Tree Shaking
The second reason is no tree shaking. Tree shaking is a build optimization that removes unused code from the bundle. Some libraries are not tree-shakeable (e.g., lodash imports the entire library even if you use one function), which means the bundle includes all the library's code. The fix is to use tree-shakeable alternatives (e.g., lodash-es instead of lodash, or native JavaScript functions instead of utility libraries) or to import specific functions (e.g., import debounce from 'lodash/debounce' instead of import { debounce } from 'lodash').
Reason 3: No Code Splitting
The third reason is no code splitting. Without code splitting, the entire app is in a single bundle, which means the user downloads all the code for all pages before they can see the first page. The fix is to use code splitting: load only the code needed for the current page, and lazy-load other pages on demand. For Next.js, the framework handles code splitting automatically. For Vite, use React.lazy and Suspense. For more on code splitting, see our article on fixing module not found in Vite + React.
Reason 4: Heavy Dependencies
The fourth reason is heavy dependencies. AI assistants add heavy libraries (e.g., moment.js for dates, lodash for utilities, jquery for DOM manipulation) that are much larger than necessary. The fix is to use lighter alternatives: date-fns or native Intl instead of moment.js, native JavaScript instead of lodash, native DOM APIs instead of jquery. For more on dependency management, see our article on the dependency hell trap.
Reason 5: No Bundle Budget
The fifth reason is no bundle budget. Without a bundle budget, the bundle grows over time as new dependencies are added, and nobody notices until the performance degrades significantly. The fix is to set a bundle budget (e.g., "JavaScript bundle must be under 200KB") and to enforce it in CI/CD, which prevents the bundle from growing beyond the budget.
Step-by-Step: The 5-Fix Bundle Analysis Checklist
Fix 1: Analyze your bundle
For Next.js:
npm install -D @next/bundle-analyzerAdd to next.config.js:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});Run the analyzer:
ANALYZE=true npm run buildFix 2: Use tree-shakeable alternatives
// Bad: imports the entire lodash (71KB)
import { debounce } from 'lodash';
// Good: imports only debounce (1KB)
import debounce from 'lodash/debounce';
// Better: use a tree-shakeable alternative
import { debounce } from 'lodash-es';
// Best: use native JavaScript
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}Fix 3: Implement code splitting
For Vite (Next.js does this automatically):
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
Loading...