Our services
If you can think it, we can make it brainsoft.
If you can think it, we can make it brainsoft.
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.
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.
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.
import debounce from 'lodash/debounce' rather than the whole lodash namespace.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.
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.
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.
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.
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.