Reducing Micro Frontend LCP by 41% with Rolldown codeSplitting and Module Federation 2.0
After adopting micro frontends, I felt proud for a while. Each team could deploy independently without touching each other's code. But at some point, build times started creeping up, and I found that bundle sizes had grown far beyond expectations. The situation where chunks were split into hundreds of pieces causing a flood of network requests was — honestly — an irony where LCP had gotten worse than before adopting micro frontends.
This article summarizes two technologies I encountered while digging into that problem. Combining Rolldown's codeSplitting (formerly advancedChunks) with Module Federation 2.0 lets you finely control the chunk structure of a micro frontend build pipeline, and Framer used this combination to cut LCP by 41%. This isn't from my own full production deployment experience — it's the result of analyzing that case study and running experiments locally. Any frontend developer who has worked with Vite and bundlers should be able to follow along directly.
Core Concepts
Rolldown codeSplitting — How to Design Chunks Yourself
Rolldown is a JavaScript bundler written in Rust, aimed at significantly boosting build speed while maintaining compatibility with Rollup's plugin API. According to official benchmarks, it is 10–30x faster than Rollup and delivers speeds comparable to esbuild. Vite 8 Beta has already adopted Rolldown as its default bundler, and once it reaches stable release, the entire Vite ecosystem will effectively transition to Rolldown.
However, a fast bundler and well-split bundles are different problems. The default code splitting algorithm separates chunks at dynamic import (import()) boundaries, which can sometimes create too many chunks or duplicate shared code across multiple chunks. At first I thought, "Isn't it enough to just split out the vendor?" — but when I inspected the build output with a visualizer, I found a single component copied verbatim into three separate chunks.
codeSplitting is a manual chunking configuration that supplements this automatic algorithm. Before Rolldown 1.0 RC it was called output.advancedChunks, but from RC onward it was consolidated into output.codeSplitting. The configuration object structure is the same, so migration is just a matter of renaming.
minShareCount: Determines how many entry points must use a module before it gets split into its own chunk. SettingminShareCount: 2means shared components used by two or more pages are automatically extracted, preventing duplicate loading.
Module Federation 2.0 — A Micro Frontend Runtime Independent of the Bundler
Module Federation (MF) is an architectural pattern where independently deployed apps share each other's components and libraries at runtime. Version 1.0 was Webpack-only, but 2.0 fully decouples the runtime from the build tool.
Host App ←──── remoteEntry.js ──── Remote App A
│ (Vite + Rolldown)
└──── remoteEntry.js ──── Remote App B
(Rspack — Rust-based Webpack-compatible bundler)Thanks to this structure, even if the Host uses Vite 8 (Rolldown-based), Remote A uses Rspack (a Rust-based bundler that can use Webpack config with minimal changes), and Remote B uses Webpack, the @module-federation/core runtime handles module loading in a unified way. The fact that legacy teams don't need to immediately switch bundlers to share modules with new teams is particularly useful in practice.
In MF terminology, a Remote is an app that exposes components, and a Host is an app that consumes those components. A single app can be both a Host and a Remote at the same time. Version 2.0, declared Stable in April 2026, is sufficiently validated for production based on ByteDance's internal infrastructure experience and collaboration with the original MF authors.
Combining the Two Technologies
Using codeSplitting together with MF 2.0 produces the following pipeline:
[Host: Vite 8 + Rolldown]
└── @module-federation/vite
└── Loads Remotes at runtime
[Remote: Vite 8 + Rolldown]
└── @module-federation/vite
└── Exposes remoteEntry.js
└── Chunk optimization via output.codeSplittingMF 2.0 establishes the independently deployable Remote structure, while Rolldown's codeSplitting takes responsibility for finely controlling the bundle boundaries within each Remote.
Practical Application
Example 1: The Chunking Strategy Framer Used to Reduce LCP by 41%
Framer was using esbuild but ran into a problem where excessive chunk counts severely degraded LCP. By collaborating with VoidZero to introduce Rolldown's advancedChunks (the name at the time), they achieved the following results: 67% reduction in chunk count, and 41% reduction in LCP for large sites. Over 200,000 Framer pages are currently built this way.
The key was minShareCount. The problem was that components shared across multiple entries were being duplicated in each chunk, and this single option made it possible to extract common code into a separate chunk.
// vite.config.ts — Based on Rolldown 1.0 RC+ (the option was advancedChunks before RC)
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor-react',
test: /node_modules\/(react|react-dom)/,
priority: 20,
},
{
name: 'shared-components',
minShareCount: 2, // Split into its own chunk when shared by 2+ entries
minSize: 20_000,
maxSize: 150_000,
priority: 10,
},
],
},
},
},
},
});After building, checking the dist/ folder will reveal a structure like this:
dist/
├── assets/
│ ├── vendor-react-[hash].js ← dedicated chunk for react/react-dom
│ ├── shared-components-[hash].js ← shared components chunk
│ └── index-[hash].js
└── index.html| Option | Role |
|---|---|
name |
Name prefix for the generated chunk file |
test |
Regex to match which modules to include in this group |
priority |
Priority when multiple groups match (higher value applies first) |
minShareCount |
Threshold for how many entries must share a module before it's split |
minSize / maxSize |
Chunk size range limits (in bytes) |
If you omit priority, it becomes hard to predict which group a module will end up in when multiple group rules overlap on the same module. Explicitly specifying priority: 20 for React-related modules makes debugging much easier.
Example 2: Setting Up a Remote App with MF 2.0 + Rolldown
This is the pattern of attaching @module-federation/vite (version 2.x) to a Remote app in an actual micro frontend environment and optimizing the vendor chunk with codeSplitting.
If you're seeing it for the first time, you might wonder why singleton: true is set in shared — it's because having multiple React instances coexisting causes Hook rule violation errors, so only one instance must be alive at a time.
// remote app vite.config.ts
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
server: { port: 5001 },
plugins: [
federation({
name: 'order-remote',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/components/OrderList.tsx',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
build: {
rollupOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor',
test: /node_modules/,
minSize: 100_000,
maxSize: 250_000,
priority: 10,
},
],
},
},
},
},
});// host app vite.config.ts
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
server: { port: 5000 },
plugins: [
federation({
name: 'shell',
remotes: {
// Local dev: http://localhost:5001/remoteEntry.js
// Production: https://cdn.example.com/order/remoteEntry.js
orderRemote: 'order-remote@http://localhost:5001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
});The Host registers Remotes in the format [name]@[remoteEntry URL]. It's convenient to branch via environment variables — using http://localhost:5001/remoteEntry.js during local development and swapping to a CDN URL in production.
Example 3: Cross-Bundler Scenario — Coexisting with a Legacy Team
A situation frequently encountered in practice: the legacy team uses Rspack/Rsbuild while the new team uses Vite/Rolldown. Thanks to the MF 2.0 runtime, Remote modules can be consumed across different bundlers.
// Legacy team — rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginModuleFederation({
name: 'legacy-remote',
exposes: {
'./LegacyHeader': './src/LegacyHeader',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
});// New team Host — vite.config.ts (Rolldown-based)
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
federation({
name: 'new-shell',
remotes: {
legacyRemote: 'legacy-remote@https://cdn.example.com/legacy/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
});The two teams use different bundlers, but since the @module-federation/core runtime acts as a common layer, consumption is possible with just the remoteEntry.js URL. Platforms like Zephyr Cloud can further automate the deployment and version orchestration of this mixed architecture.
Pros and Cons
Advantages
| Item | Details |
|---|---|
| Build performance | Rolldown is 10–30x faster than Rollup and delivers speeds approaching esbuild |
| Fine-grained chunk control | codeSplitting group rules let you directly optimize bundle sizes and loading order by domain |
| Build tool independence | The MF 2.0 runtime is decoupled from bundlers, allowing Webpack, Rspack, Vite, and Rolldown to be used together |
| Automatic TypeScript type sharing | MF 2.0 auto-generates and loads Remote module types during development, maintaining type safety without npm link |
| Independent deployment | Teams can maintain independent release cycles while synchronizing shared modules at runtime |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Complexity tax | Separate repos, CI pipelines, and version management become far more complex than a monorepo | For small teams, it may be better to first consider a monorepo + MF approach |
| Insufficient defaults | Using only the default chunking strategy can produce thousands of tiny chunks | It's better to explicitly define vendor and shared chunks via groups configuration |
| Rolldown RC stage | Rolldown 1.0 RC is usable in production but some edge cases may exist | Like Framer, you can test thoroughly and apply incrementally |
| Runtime overhead | Additional network requests and runtime reassembly from the MF runtime affect initial load time | Can be mitigated with preload hints or CDN caching strategies |
The Most Common Mistakes in Practice
-
Registering React in
sharedwithoutsingleton: true— The Host and Remote each end up with different React instances, causing Hook rule violation errors. It's best to always declarereactandreact-domwithsingleton: true. -
Omitting
priorityincodeSplittinggroups — When multiple group rules overlap on the same module, the outcome becomes hard to predict. Specifying explicitpriorityvalues for each group makes debugging much easier. -
Hardcoding Remote URLs — URL management becomes complex as the number of Remotes grows. Setting up a structure that dynamically injects URLs via environment variables or an orchestration layer like Zephyr Cloud from the start will save a lot of pain later.
Closing Thoughts
Before combining Rolldown and MF 2.0, I had the thought: "How much difference could changing the bundler really make?" But what I realized while analyzing the Framer case was that how precisely you can control the chunk structure has a greater impact on LCP than the bundler's speed. codeSplitting is the tool that enables that control, and MF 2.0 is the runtime that keeps that structure stable even when teams use different bundlers.
Three steps you can start with right now:
-
Add
codeSplitting.groupstobuild.rollupOptions.outputinvite.config.tsand runpnpm build. If you visualize the chunk structure withrollup-plugin-visualizer, you'll see that adding justminShareCount: 2noticeably reduces duplicate chunks. -
Install the plugin with
pnpm add @module-federation/vite, then locally spin up a Remote app (port 5001) and a Host app (port 5000) and build a minimal example that renders a Remote component inside the Host. The official@module-federation/viteexample repo has a starter ready to go. -
Apply the
federation()plugin andcodeSplitting.groupstogether to the Remote app — while the vendor chunk remains in CDN cache, only the changed portions are re-downloaded when the Remote is updated, without re-downloading the whole thing. You can verify this directly in the Chrome DevTools Network tab.
References
- Rolldown Official Docs - advancedChunks
- Rolldown Official Docs - codeSplitting
- Rolldown Official Docs - Manual Code Splitting
- Announcing Rolldown 1.0 RC | VoidZero
- How Framer reduced LCP using Rolldown | VoidZero Case Study
- Bundling at Framer — Rolldown for faster sites | Framer Blog
- Module Federation 2.0 Stable Release | InfoQ
- MF 2.0 Stable Release Official Blog | module-federation.io
- Module Federation 2.0 Official Release Discussion | GitHub
- @module-federation/vite | npm
- Module Federation in Rolldown — What It Means for Vite? | Medium
- advancedChunks API Design Discussion | GitHub Discussion #2118
- Module Federation | Rspack Official Docs
- Vite + Webpack + Rspack with Module Federation | Zephyr Cloud Docs
- Code Splitting Algorithms | DeepWiki