title: The Art of Tree-Shaking: Practical Techniques for Auditing and Reducing Bundle Size in React Applications date: 2026-05-22T08:24:15.222Z description: Blog about The Art of Tree-Shaking: Practical Techniques for Auditing and Reducing Bundle Size in React Applications
Hey everyone, I’m Prashuk. I spend most of my time building full-stack apps with React and Next.js. If there’s one thing that keeps me up at night, it’s a slow-loading UI. We spend weeks perfecting the design, but if the browser has to download a 2MB JavaScript file before the user sees anything, the experience is already ruined.
Lately, I’ve been obsessed with tree-shaking. Not the theoretical stuff you read in documentation, but the actual "how do I fix this mess" part of it.
Here is how I approach auditing and trimming down React bundles based on what has actually worked for me in production.
1. The Reality Check: Visualizing the Bloat
Before I touch a single line of code, I need to see what I’m fighting. I ran into this issue when a dashboard project I was working on started feeling "heavy." The Lighthouse score was tanking, and I couldn't figure out why.
I used webpack-bundle-analyzer (or @next/bundle-analyzer since I use Next.js a lot). It gives you a visual map of your JS.
What I found was embarrassing:
- A massive chunk of the bundle was just
moment.jslocales we weren't even using. - Another big square was a PDF library that was only used on one hidden admin page.
What worked for me was setting this up as a script in my package.json. Now, I run it once a week just to make sure no "accidental" large dependencies crawled into the codebase.
2. The Barrel File Trap
We all love clean imports. We create index.ts files in our folders and export everything from them so we can write import { Button, Input } from '@/components'.
But here’s the catch: Sometimes, your bundler gets confused. If you import one tiny component from a "barrel file" that also exports a massive 3D library or a heavy chart component, the bundler might end up pulling in parts of the heavy stuff anyway.
In one of my projects, I noticed the landing page was loading heavy data-grid logic. It was all because of a shared index.ts file in the UI folder. I’ve started being more intentional. If a component is heavy, I import it directly from its path instead of the shared index. It’s a bit more typing, but the performance gain is real.
3. Replacing the "Legacy" Giants
Tree-shaking only works if the library you’re using is written in ES modules. Old-school libraries like lodash or moment.js are notorious for being hard to shake.
If you do import { debounce } from 'lodash', you might accidentally pull in the entire library.
What worked for me was switching to lodash-es or, even better, just writing the utility myself. For dates, I’ve moved entirely to date-fns or dayjs. They are modular by design, meaning if you only use format, you only pay for format.
4. Code Splitting is Tree-Shaking’s Best Friend
You can't tree-shake what the app actually needs, but you can delay loading it.
I had a project with a "Settings" modal that had a lot of heavy form logic and image cropping libraries. 90% of users never even opened that modal. Why make them download that code on the home page?
I started using React.lazy and dynamic imports for anything that isn't visible on the initial page load.
// Instead of this:
// import HeavyModal from './HeavyModal';
// I do this:
const HeavyModal = React.lazy(() => import('./HeavyModal'));
function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(true)}>Edit Profile</button>
{show && (
<Suspense fallback={<div>Loading...</div>}>
<HeavyModal />
</Suspense>
)}
</>
);
}
5. Be Careful with UI Libraries
I love MUI and Lucide-React, but they can bloat your app fast if you aren't careful.
I once saw a project where the main.js chunk was 500KB just because of icons. The dev had imported icons like this:
import { * } from 'lucide-react'.
Even with a good bundler, this can sometimes fail to shake properly. Always use specific imports. If your UI library supports it, check their documentation on "Top-level imports vs. Path imports."
My Best Practices for a Lean App
- Audit early: Don't wait until the app is slow. Use a bundle analyzer during development.
- Check
package.jsonregularly: If a library hasn't been updated in 4 years, it probably doesn't support tree-shaking well. - Prefer "Micro-libraries": Use libraries that do one thing well instead of "Swiss Army Knife" libraries.
- Use Production Builds for Testing: Tree-shaking often doesn't happen in development mode. Always run a production build (
npm run build) before checking your bundle size.
Final Thoughts
At the end of the day, clean code isn't just about how it looks to you; it's about how it performs for the user. Tree-shaking isn't some magic button you toggle in Webpack—it’s a habit of being mindful about what you’re adding to your project.
I’ve learned the hard way that "it’s just one more small library" is how you end up with a 3MB bundle. Keep it lean, keep it fast.
What’s the biggest "bundle-killer" you’ve found in your projects? Let me know!