Rule one: draw a line between the host (navigation, auth shell, design tokens) and remotes (feature MFEs). Migrations that try to “upgrade the whole org” in one Jira epic usually stall; migrations that make the host Vue 3–ready and then cut over remotes in product order tend to finish.
1. Dependency alignment board
Run one matrix: Vue, Vue Router, Pinia/Vuex, major UI kit, and build tool per MFE. Anything in the “shared” layer must be single version per runtime—or explicitly duplicated only where you mean it (e.g. legacy widget islands).
2. Migrate revenue-critical remotes first—or last on purpose
There is no universal rule except choose. Some orgs need the cash path on Vue 3 before touching internal tools. Others use low-risk MFEs as a training ground. Document the decision; do not let it be an accident of sprint luck.
3. Contract tests between shell and remotes
Expose integration smoke tests for mount/unmount, routing handoffs, and shared state boundaries. A green unit suite on each MFE is not enough when the failure is “two copies of the router in memory.”
4. Choosing an integration model
Most Vue 2 micro-frontend stacks fall into one of three integration shapes. The migration calculus differs sharply between them, and pretending they are the same is how teams end up with two routers in memory and a shell that double-mounts on every navigation.
Module Federation (Webpack 5)
The shell exposes vue, vue-router, and design tokens as shared singletons. Each remote consumes them. To migrate a remote to Vue 3, you must either bump the shared vue singleton (which forces every remote at once) or scope a second runtime that hosts only Vue 3 remotes. We almost always recommend the second path during a long migration window.
single-spa
single-spa-vue mounts each app independently, so Vue 2 and Vue 3 can coexist per route. The cost is that each remote ships its own framework copy. If LCP and bundle size matter—see our deep dive on bundle size and Core Web Vitals—budget for an aggressive consolidation pass once the migration completes.
iframe-based composition
Easiest to migrate per-remote (full isolation), worst for UX, hardest for cross-app state. We treat iframes as a tactical choice for the riskiest legacy app—not a long-term home.
5. The shared dependency contract
Write down what the shell guarantees and what each remote must bring itself. A typical contract for a Vue 3-ready shell looks like this:
// shell-contract.md
shell.provides:
vue: "^3.4" # singleton, eager
vue-router: "^4.3" # singleton, eager
pinia: "^2.1" # singleton, eager
@company/design-tokens: "^2"
@company/auth-client: "^1"
remote.must.bring:
- its own UI kit (Element Plus, PrimeVue, etc.)
- feature-scoped stores
- i18n messages bundle
remote.must.NOT.bring:
- a second copy of vue
- a second router instance
- global CSS resetsWhen a remote violates the contract, CI fails the pipeline. We use webpack-bundle-analyzer on every remote and a small script that scans stats.json for forbidden duplicates.
6. Common pitfalls we see
- Two reactive systems in one tab. A Vue 2 remote and a Vue 3 remote each instantiate their own reactivity. Pinia stores from one are not reactive in the other—do not try to share a store across the boundary. Share serialized state via events or a thin pub/sub.
- Router collisions. Vue Router 3 and 4 both want to own
window.history. Pick one outer router (usually in the shell) and have remotes mount under/app/<remote>/*with their own internal routers operating on a sub-path. Our Vue Router 3 to 4 guide covers the per-remote upgrade. - CSS leakage. Element UI and Element Plus ship overlapping global selectors. Scope each remote with a CSS layer or a unique attribute selector before you ship them on the same page.
- Auth token races. Two remotes refreshing the same token in parallel will log users out. Centralize token refresh in the shell.
- Telemetry double-counting. Each remote registering its own analytics SDK will double-count page views. Provide a shell-level analytics shim.
7. Sequencing trade-offs
| Order | Pros | Cons | When to choose |
|---|---|---|---|
| Shell first | Sets contract; remotes can land independently | Requires senior platform team upfront | Almost always the right call |
| Smallest remote first | Builds team muscle, cheap to roll back | Doesn't prove the hard cases | Junior team, no migration scars |
| Revenue path first | EOL risk retired on the most-watched flow | Highest blast radius | When EOL deadlines drive contractual exposure |
| Internal tools first | Real production traffic, low customer risk | Doesn't address external SLAs | When you need a training ground |
8. What good looks like at the end of each remote
- One copy of
vuein the runtime; verified by a CI assertion against the bundle stats. - Mount and unmount tests pass with the shell, with no console errors after a 30-second soak.
- Auth refresh, navigation, and feature flags continue to work after the remote is hot-swapped.
- Bundle size for the remote did not grow more than 10% versus its Vue 2 baseline (or there is a written reason).
- Production error rate is flat or lower for one full release cycle before the next remote is started.
9. Anti-patterns we see
- "Let's federate everything and migrate later." Federation is not a migration strategy; it is a deployment strategy. Without a shared contract, you have just multiplied the surface area.
- Per-team Vue versions as a feature. Letting each team pick a Vue minor "for autonomy" guarantees that the shell or a peer will eventually break in production. Autonomy belongs at the feature level, not the framework level.
- Skipping the shell upgrade. Migrating remotes while the shell still injects Vue 2 globals leads to remotes that work in isolation and break when composed.
- Treating
@vue/compatas a migration end-state. Compat mode is a bridge inside one bundle. Across MFEs, it leaks abstractions. See our strangler fig pattern post for how to scope compat to a single deployable.
10. Migration checklist for an MFE estate
- Inventory every deployable, owner, Vue version, router version, UI kit, and build tool.
- Publish the shared dependency contract and gate CI against it.
- Upgrade the shell to be Vue 3-ready (load Vue 3 runtime, but still host Vue 2 remotes).
- Write contract tests for mount/unmount, auth refresh, route handoff, and shared events.
- Pick the sequencing strategy and document the reason in an ADR.
- Migrate one remote, soak in production for at least one release cycle, then move on.
- After the last remote is on Vue 3, drop the dual-runtime shell and consolidate.
- Track cost vs. estimate per remote so the next one budgets correctly.
FAQ
Can a Vue 2 remote and a Vue 3 remote share a Pinia store?
No. Each Vue major has its own reactivity system. Use a serialized event bus or a tiny shared store wrapper that exposes a snapshot API.
Should we wait until all remotes are ready and cut over together?
Almost never. Big-bang cutovers across MFEs combine the worst of both worlds: distributed coordination cost and monolith-sized risk.
How do we handle remotes owned by teams that won't prioritize migration?
Make the EOL deadline visible at the executive level (our CEO's guide helps), and offer a centralized migration squad that rotates through laggards.
Is module federation still the right call in 2026?
For new estates, we lean toward simpler composition (a single Vite app with route-level code splits) unless there is a strong organizational reason for separate deployables. Existing federation works fine—just keep the contract honest.
Multiple teams, one EOL date
We help align architecture, sequencing, and technical program management across MFEs.
Plan a cross-team migrationConclusion
Micro-frontends buy independence; Vue 2 EOL forces redependence on shared standards. Get those standards explicit, then migrate one bundle at a time with the host leading the way.
