Bundle size optimization: Auditing Dependency Graphs for Faster Loads
Master bundle size optimization by auditing your dependency graph. Learn to strip unused code, use subpath imports, and enforce performance budgets today.
Last month, I spent about three days chasing a 400KB regression in our main dashboard bundle. It turned out that a single utility library, imported via a barrel file, was pulling in half the React ecosystem despite us only using a single formatDate function.
If you’ve ever felt like your app is getting heavier despite writing "clean" code, you’re likely fighting the same ghost. Achieving consistent bundle size optimization isn't just about deleting unused imports; it's about understanding how your bundler interprets your dependency graph.
Why Tree-Shaking Fails in the Real World
Most of us assume that modern bundlers like Webpack 5 or Vite will automatically handle tree-shaking for us. In theory, they perform static analysis to remove code that isn't exported or referenced. In practice, they are often blocked by side effects.
If a package has a sideEffects property set to true in its package.json—or worse, lacks the field entirely—the bundler must assume that importing the file might change global state. It won’t prune anything. To verify if your dependencies are playing along, I start by running webpack-bundle-analyzer or the rollup-plugin-visualizer.
When you see a giant block for a library you barely use, check these two things:
- Does the package support ESM? CommonJS is notoriously difficult to tree-shake.
- Are you importing from the root?
We once tried to fix a massive bundle by switching to barrel files, thinking it would make imports cleaner. We were wrong. Barrel files often force the bundler to load every export in the module, effectively killing our tree-shaking efforts.
Dependency Analysis through Subpath Imports
Instead of importing from the root, I’ve moved toward subpath imports. This forces the bundler to look at a specific, smaller file rather than the entire package index.
Instead of this:
JAVASCRIPTimport { heavyUtility } from CE9178">'large-library';
Try this:
JAVASCRIPTimport heavyUtility from CE9178">'large-library/dist/heavyUtility';
This simple change often saves roughly 150KB of minified JS. If the library doesn't expose subpaths, you might need to use a tool like babel-plugin-import or vite-plugin-imp to transform those imports automatically.
Comparison of Import Strategies
| Strategy | Tree-Shaking Potential | Complexity | Impact on Bundle |
|---|---|---|---|
| Root Import | Low (if side effects) | Low | High Bloat |
| Barrel Files | Very Low | Medium | Very High Bloat |
| Subpath Imports | High | Medium | Minimal |
| Direct ESM | Highest | High | Optimal |
Putting it into Practice: The Audit Flow
When I perform a dependency analysis, I follow a strict process. I don't just look at the total KB; I look at the "cost" of the dependency relative to its utility.
Flow diagram: Run Bundle Analyzer → Large Chunk Found?; B -- Yes → Check Package Source; Check Package Source → ESM Supported?; D -- No → Find Smaller Alternative; D -- Yes → Implement Subpath Import; Implement Subpath Import → Verify Bundle Reduction
If you want to automate this, look into Performance budgets: Enforcing Bundle Size and Vital Thresholds. It prevents regressions from creeping back in after you’ve done the hard work of cleaning up. I also recommend referencing Cutting JavaScript bundle size: A practical guide for developers to see how these techniques scale across larger codebases.
Frequently Asked Questions
How do I know if a library is tree-shakeable?
Check the package.json for the sideEffects: false flag. If it’s missing, test it by importing a single function and checking the bundle output size.
Are subpath imports always safe?
Not always. Some libraries use internal file structures that change between minor versions. Always pin your dependencies in package.json if you rely on deep imports.
Does this help with INP? Indirectly, yes. Smaller bundles mean faster parsing and compilation, which reduces the time the main thread spends blocked. For more on that, read about INP Optimization: Strategies for Reducing Long Tasks.
Final Thoughts
JavaScript performance is a moving target. What works today might break with a package update tomorrow. I’ve stopped trying to reach "perfect" bundle sizes and instead focus on maintaining a budget that forces me to be intentional about every kilobyte I add. Next time, I’m planning to experiment with Module Federation to offload parts of our app entirely, but for now, subpath imports keep our users happy.