Our services

If you can think it, we can make it brainsoft.

Keeping a React bundle small without a rewrite

Written By: BrainSoft In Frontend

A React app that started as a prototype rarely stays small. Someone adds a charting library, someone else imports a whole icon set, and two years later the main bundle is 1.8 MB and the first paint on a mid-range phone feels like watching paint dry. The instinct is to start over with a fresh framework or a new build setup, but that usually costs months and introduces the same problems again, just newer.

You can cut most of that weight in place. Measure what actually ships, split the routes, fix the imports that pull in entire libraries, replace or lazy-load the heaviest dependencies, and add checks so the bundle does not quietly grow back. Here is the order I work in.

Measure before you touch anything

You cannot fix a bundle you have not looked at. The webpack stats file or Vite's build output tells you which modules ended up in which chunk. If you are on webpack, generate the stats and feed them to a visualiser; on Vite, the build already prints chunk sizes, and a plugin gives you the treemap.

npx vite build --mode production
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json

Look for surprises. It is almost never React itself. It is a date library with every locale, a UI kit imported from its root, moment, lodash, or an analytics SDK that ships a full parser. Write down the top five offenders by parsed size before you change a line.

Split by route, then by interaction

The cheapest big win is not loading code for pages the user has not opened. React.lazy plus Suspense does this with no router rewrite if you already use a router that supports it.

const Reports = React.lazy(() => import('./pages/Reports'));

<Suspense fallback={<Spinner />}>
  <Route path="/reports" element={<Reports />} />
</Suspense>

Then do the same for heavy components that only appear after a click: a rich text editor, a PDF preview, a map, a chart. A modal that loads a 300 KB editor on demand costs nothing until the user opens it. Do not lazy-load everything, though. Splitting a 4 KB component just adds a network round trip.

Fix the imports that pull in the world

  • Import from the exact path instead of the package root: import debounce from 'lodash/debounce' rather than the whole lodash namespace.
  • Check whether your UI library has an ES module build. Many ship a CommonJS entry by default, which defeats tree shaking.
  • Replace moment with date-fns or dayjs if you only need formatting and a few calculations. The API differences are small and the size difference is not.
  • Swap a full icon package for direct SVG imports, or use a build plugin that only keeps the icons you reference.
  • Set sideEffects: false in your own package.json only after you have verified nothing relies on import side effects.

After each change, rebuild and compare. Sometimes a fix that looks obvious saves nothing because the module was already excluded. Sometimes a one-line import change drops 200 KB.

Keep it from growing back

A bundle shrinks once and then creeps back over the next year. Add a size check to CI that fails when a chunk crosses a budget you set today. Keep the budgets in the repo so the number is visible in review. If your team needs help wiring this into an existing pipeline, that is the kind of work our services cover, but you can start with a single script.

npx size-limit
# size-limit.config.js
module.exports = [{ path: 'dist/assets/index-*.js', limit: '180 kB' }];

Also worth doing: enable source map analysis in review, ban new dependencies above a size you agree on, and prefer the platform. Fetch, Intl, and CSS often replace a library outright. None of this requires a rewrite, and the app keeps shipping while you do it.

Frequently asked questions

How much can I realistically cut without rewriting?

It depends on what is in there, but route splitting plus fixing a few root imports usually removes a large share of the initial download. The gains come from not shipping code the first screen never uses, so the more pages and heavy widgets you have, the more there is to win.

Does code splitting hurt performance?

It can if you overdo it. Every split adds a request, and a slow connection makes many tiny chunks worse than one medium one. Split at route boundaries and around genuinely heavy components, then measure real load times rather than guessing.

Should I switch to a different framework instead?

Only if the current setup is blocking you in other ways. A framework change is a rewrite with a new set of defaults, and the same careless imports will bloat it too. Fix the measurement and import habits first, then decide whether a migration is still worth it.


#Frontend