Bundle Size Optimization: Mastering Module Federation and Code Splitting
Learn how to optimize your bundle size, module federation, and code splitting strategies to improve your Core Web Vitals and overall web performance.
Managing bundle size while scaling a frontend architecture is a constant battle against bloat. When we started integrating Module Federation into our micro-frontend setup last year, we assumed the runtime nature of shared dependencies would magically solve our performance woes. We were wrong.
It turns out that misconfigured remote containers can inflate your bundle size faster than you can add a feature. If you aren't careful, you end up shipping duplicate versions of React or redundant utility libraries, which directly tanks your Core Web Vitals.
The Reality of Bundle Size and Module Federation
When you implement Module Federation, the browser doesn't just download your app; it coordinates a dance of shared dependencies. I’ve seen teams ship three different versions of lodash because their remote configurations weren't strictly enforcing singletons.
To keep your bundle size in check, you must treat your shared configuration in Webpack as a contract. Here’s how we tightened our webpack.config.js:
JAVASCRIPT// webpack.config.js shared: { ...deps, react: { singleton: true, requiredVersion: deps.react }, CE9178">'react-dom': { singleton: true, requiredVersion: deps[CE9178">'react-dom'] }, },
By setting singleton: true, you force the host and remotes to resolve to a single instance. If a remote requires a different version, the build should fail or warn you during development, rather than silently bloating the user's browser cache.
Why Code Splitting Alone Isn't Enough
Many developers treat code splitting as a "set it and forget it" feature provided by dynamic imports. While React.lazy is great for route-based splitting, it doesn't solve the problem of large, monolithic components that get bundled into your main entry point.
We once spent about two days tracking down a 400KB regression in our dashboard. It turned out a single utility component was importing a heavy charting library at the top level. We shifted to a pattern where we only load heavy dependencies inside an useEffect or a specialized wrapper component.
If you are struggling with these patterns, sometimes a fresh approach to Next.js Website & Landing Page Development helps reset your architecture toward a performance-first mindset.
Balancing Performance Trade-offs
You’ll often face a choice: keep the bundle small or improve the user experience with pre-fetching. We previously tried aggressive pre-fetching for every remote module, but it caused massive network contention on low-end mobile devices.
| Strategy | Impact on LCP | Impact on INP | Complexity |
|---|---|---|---|
| Aggressive Preloading | High | Neutral | Low |
| Route-based Splitting | Medium | Low | Medium |
| Lazy Loading (On-demand) | Low | High | High |
We ultimately moved toward a hybrid approach. We prioritize critical path resources while deferring non-essential modules. For deeper insight into this, I often revisit Resource prioritization for Core Web Vitals: A Practical Guide to remind myself why we shouldn't just preload everything in sight.
Improving Core Web Vitals through Architecture
Your Core Web Vitals are ultimately a reflection of how efficiently you deliver bytes to the client. If your module federation setup is forcing the browser to block the main thread while resolving dependencies, your Interaction to Next Paint (INP) will suffer.
We’ve found that using Fetch Priority: Architecting Non-Blocking Resource Warming helps significantly when you need to load remote chunks without stalling critical rendering tasks. Furthermore, if you’re still seeing high LCP times, consider the impact of your CSS. We’ve had success using Critical Rendering Path Optimization: A Guide to Resource Inlining to ensure the above-the-fold content renders instantly, even while the rest of the federated modules are still fetching.
Common Questions (FAQ)
1. Does Module Federation always increase bundle size?
No, but it increases the risk. If you don't use the shared configuration to deduplicate dependencies, you will definitely see an increase.
2. How do I measure the impact of code splitting on INP? Use the Chrome DevTools Coverage tab to see which code is actually executed. If you're shipping unused code that triggers long tasks during execution, that's where your INP hits are coming from.
3. Is there a "perfect" bundle size? No. Focus on the "Time to Interactive" and the "Total Blocking Time" instead of an arbitrary KB count. A 500KB bundle that executes efficiently is better than a 200KB bundle that blocks the main thread with heavy initialization logic.
Final Thoughts
Performance engineering is rarely about finding a silver bullet. It's about constant measurement and the willingness to refactor when your "smart" architecture starts showing its age. I’m still experimenting with how much we can offload to the edge, and I’m sure my current configuration will look clunky in six months. That’s just the nature of the work—keep measuring, keep splitting, and don't get too attached to your current build config.