Find What's Making Your React Bundle Huge
14 Jan 2026Your bundle didn’t get big overnight. It got big one import at a time, each one reasonable on its own. Finding the actual offenders takes fifteen minutes with the right tool — guessing takes an afternoon.
Start with the analyzer, not a guess
For a webpack-based setup (Create React App, custom webpack, older Next.js):
npm install --save-dev webpack-bundle-analyzer
For Next.js specifically, use the built-in wrapper:
npm install --save-dev @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
ANALYZE=true npm run build
For Vite:
npm install --save-dev rollup-plugin-visualizer
Any of these gives you a treemap: rectangles sized by actual bytes in the final bundle. Stop guessing which import is heavy — the treemap tells you.
The usual offenders, and what to do about each
| Offender | Typical size hit | Fix |
|---|---|---|
| Moment.js | 200-300KB | Switch to date-fns or dayjs (2-15KB, tree-shakeable) |
Full lodash (import _ from 'lodash') |
70KB+ | Import specific functions: import debounce from 'lodash/debounce' |
| Icon libraries importing the whole set | 50-150KB | Import individual icons, not the barrel export |
| Chart libraries (full Chart.js, full Recharts) | 100KB+ | Use tree-shakeable imports or a lighter alternative if you only need one chart type |
| Duplicate React/React-DOM versions | Varies, often 40KB+ | Check for multiple versions via npm ls react |
| A whole UI library for one component | 100KB+ | Copy the one component, don’t import the library |
The lodash case is the clearest example of a fix with zero downside:
// 70KB+, pulls in the entire library
import _ from 'lodash'
_.debounce(fn, 300)
// A few KB, only the function you use
import debounce from 'lodash/debounce'
debounce(fn, 300)
Code-split what isn’t needed on first paint
Anything gated behind a route change, a modal, or an admin-only view doesn’t need to ship in the initial bundle:
import { lazy, Suspense } from 'react'
const AdminPanel = lazy(() => import('./AdminPanel'))
function App() {
return (
<Suspense fallback={<Spinner />}>
{showAdmin && <AdminPanel />}
</Suspense>
)
}
This doesn’t shrink your total JavaScript — it moves the weight out of the chunk that has to load before your page becomes interactive, which is the number that actually affects users.
Check for duplicate dependencies
A common silent bloat source: two versions of the same library, usually because one dependency pins an old version and your package manager can’t dedupe it.
npm ls react
npm ls react-dom
If you see more than one version tree, check package.json overrides/resolutions to force a single version, or update the dependency that’s pinning the old one.
What “good” looks like
There’s no universal target — a data-heavy dashboard legitimately ships more JS than a marketing page. The useful comparison is your own bundle over time: track the initial JS payload (not the total, the amount needed before interactivity) on every PR, and treat an unexplained jump as a regression to investigate, not a number to shrug at.
The takeaway
Run the analyzer before you touch anything — it tells you exactly which import is heavy, in exact kilobytes, instead of you guessing. Fix the top three offenders from the treemap, code-split what’s not needed immediately, and dedupe dependency versions. That’s usually most of the win.