Shared dependencies and runtime integration without a rebuild
Module federation loads Vue components in the browser from separately deployed applications, instead of bundling them at build time. This enables independent deployments across team boundaries, but requires a deliberate strategy for shared dependencies, version conflicts and fallbacks when a remote is unreachable.
Table of contents
- 1. What module federation means for Vue apps
- 2. Host and remote: the basic configuration
- 3. Exposes: sharing Vue components deliberately
- 4. Configuring shared dependencies correctly
- 5. Versioning and compatibility contracts
- 6. Fallback strategies for unreachable remotes
- 7. TypeScript types across remote boundaries
- 8. Performance: preloading and bundle size
- 9. Module federation tooling compared
- 10. Summary
- 11. FAQ
1. What module federation means for Vue apps
Module federation is a bundler feature, originally from Webpack 5, now also available via vite-plugin-federation for Vite projects, that allows loading JavaScript modules at runtime from a different, independently built application. For Vue apps this means concretely: a host application can import a Vue component from a completely separately deployed remote application as if it were a local module, without the host application depending on the remote codebase at build time.
The crucial difference from classic npm packages is the point in time the binding happens. An npm package is embedded firmly into the host bundle at build time, and every change requires a new host build. Module federation in Vue apps resolves this binding only at runtime in the browser, so the remote team can publish a new version, and the host application automatically loads the current version on the next page visit, without anyone rebuilding the host application.
This property makes module federation one of the most popular techniques for micro frontend architectures with Vue, because it combines real deployment independence with the convenience of native JavaScript modules. The cost is additional configuration complexity around shared dependencies, version compatibility and error handling when a remote is unreachable at runtime.
2. Host and remote: the basic configuration
Every module federation configuration distinguishes between host and remote. The host is the application that consumes foreign modules, the remote is the application that provides its own modules via an entry point called remoteEntry.js. A Vue application can take on both roles at once: it consumes modules from one team and provides modules for another team itself.
The basic configuration defines on the remote side which modules are exposed under which name, and on the host side which URL a given remote is reachable at. In module federation with Vue apps, this URL typically points to a CDN address or a storage bucket to which the remote team's CI/CD pipeline uploads a new version on every deployment.
// remote/vite.config.js — Exposes Vue components as federated modules
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
vue(),
federation({
name: 'checkout_remote',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutSummary': './src/components/CheckoutSummary.vue',
'./useCheckoutState': './src/composables/useCheckoutState.js',
},
shared: ['vue', 'vue-router', 'pinia'],
}),
],
build: {
target: 'esnext',
modulePreload: false,
cssCodeSplit: false,
},
});
// host/vite.config.js — Consumes the remote module
export default defineConfig({
plugins: [
vue(),
federation({
name: 'shell_host',
remotes: {
checkout_remote: 'https://cdn.mironsoft.de/checkout/remoteEntry.js',
},
shared: ['vue', 'vue-router', 'pinia'],
}),
],
});
3. Exposes: sharing Vue components deliberately
The exposes block decides which parts of a Vue module are visible to other applications at all. A proven practice in module federation with Vue is not to expose the entire application, but to deliberately share individual, clearly bounded components and composables. This keeps the public interface of the remote module small and stable, while internal implementation details, for example helper components used only internally, are never exported in the first place.
When importing on the host side, the exposed module is loaded via a dynamic import whose module name is composed of the remote name and the exposed path. Vue then treats the loaded component like any other async component, including the ability to combine it with defineAsyncComponent and a loading state while the network request for the remote chunk is still running.
// host/src/components/CheckoutSlot.vue — Consuming a remote Vue component
import { defineAsyncComponent } from 'vue';
const RemoteCheckoutSummary = defineAsyncComponent({
loader: () => import('checkout_remote/CheckoutSummary'),
loadingComponent: CheckoutSkeleton,
errorComponent: CheckoutFallback,
delay: 100,
timeout: 5000,
});
export default {
components: { RemoteCheckoutSummary },
template: `<RemoteCheckoutSummary :order-id="orderId" />`,
props: ['orderId'],
};
4. Configuring shared dependencies correctly
Without explicit configuration, every remote would bring its own copy of Vue, leading to duplicated framework code and, worse, to reactivity bugs, because two different Vue instances cannot share the same component instances and reactivity graphs. The shared block in module federation solves this problem by defining which libraries are shared between host and remote instead of loaded multiple times.
For Vue apps, at least vue, vue-router and, if used, pinia are candidates for the shared configuration. The singleton: true setting is important, forcing exactly one instance to exist at runtime instead of loading several compatible versions in parallel. Without singleton, host and remote may end up using different Vue instances, which leads to subtle bugs around provide/inject or global plugins.
// vite.config.js — Explicit shared dependency configuration with version constraints
federation({
name: 'shell_host',
remotes: {
checkout_remote: 'https://cdn.mironsoft.de/checkout/remoteEntry.js',
},
shared: {
vue: {
singleton: true, // exactly one Vue instance across host and remote
requiredVersion: '^3.4.0',
strictVersion: false, // warn instead of hard-fail on mismatch
},
'vue-router': {
singleton: true,
requiredVersion: '^4.3.0',
},
pinia: {
singleton: true,
requiredVersion: '^2.1.0',
},
},
});
5. Versioning and compatibility contracts
Because host and remote are deployed independently of each other, an organizational problem arises that cannot be fully solved technically: who guarantees that a remote team does not change its exposed Vue component in a way that breaks the host? Module federation in Vue apps therefore needs an explicit compatibility contract between teams, typically in the form of contract tests that run against the current host integration before every remote deployment.
A pragmatic approach is semantic versioning for exposed modules: a breaking change to an exposed Vue component, for example a changed prop signature, requires a new major path in the manifest, so old host versions can keep loading the old remote version, while new host versions deliberately migrate to the new remote version. Without this discipline, host and remote drift apart unnoticed until a production incident makes the incompatibility visible.
#!/usr/bin/env bash
# ci/contract-test-remote.sh — Run before every remote deployment
set -euo pipefail
readonly HOST_STAGING_URL="https://staging.mironsoft.de"
readonly REMOTE_BUILD_DIR="dist"
# Serve the newly built remote locally and point a real host build at it
npx serve "$REMOTE_BUILD_DIR" --listen 4174 &
SERVER_PID=$!
trap 'kill $SERVER_PID' EXIT
# Contract tests run the host's integration test suite against this remote build
npm run test:contract -- --remote-url=http://localhost:4174/remoteEntry.js
echo "[OK] Remote is compatible with the current host integration contract"
6. Fallback strategies for unreachable remotes
A remote that is unreachable at runtime, for example due to a CDN outage or a faulty deployment, must never crash the entire host application. Module federation with Vue therefore requires explicit error handling on the host side: errorComponent on defineAsyncComponent, combined with a timeout so a hanging network request does not block the whole page.
For critical remote modules, such as checkout, a locally bundled fallback version inside the host is additionally recommended, activated only when the remote does not respond within the timeout. This fallback version does not need to be functionally complete, but should at least cover the critical path, such as completing an order, in a simpler form, instead of showing the user a blank page or an error message.
7. TypeScript types across remote boundaries
TypeScript type safety across module federation boundaries is one of the biggest sources of friction in practice, because the host has no idea at build time which props a remote component expects. The common solution is for each remote team to publish a separate .d.ts file with the public types of its exposed modules, either as a small npm package or as a generated file automatically extracted from the Vue components in the CI process.
These type declarations are purely static, included in the host at development time, but do not change the runtime behavior of module federation. This means a host developer gets full autocomplete and type checking in the IDE when importing a remote Vue component, while the actual component is still loaded dynamically at runtime. Without these type declarations, every remote component degrades to any in the host, making prop typos visible only at runtime.
8. Performance: preloading and bundle size
A common misunderstanding about module federation in Vue apps is that shared dependencies automatically solve performance problems. In fact, the browser must trigger an additional network request on the first access to a remote module, even if Vue itself is already loaded. For modules needed on every page, such as a header or navigation, preloading via <link rel="modulepreload"> is worthwhile, starting the additional request already during the initial page build.
The bundle size of every remote should be monitored separately, because a single, carelessly growing remote module can worsen the perceived load time of the entire host application without this being visible in the host bundle itself. Bundle analysis tools such as rollup-plugin-visualizer should therefore run in every remote pipeline, not just in the host, to catch regressions early before they affect users in production.
// host/src/preload.js — Preloading frequently needed remotes during initial page load
export function preloadRemote(remoteEntryUrl) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = remoteEntryUrl;
document.head.appendChild(link);
}
// Called for remotes needed on nearly every page, e.g. header or navigation
preloadRemote('https://cdn.mironsoft.de/header/remoteEntry.js');
// Bundle size budget enforced in the remote's own CI pipeline
// rollup-plugin-visualizer + a size-limit check, not just in the host
9. Module federation tooling compared
Besides vite-plugin-federation and native Webpack 5 federation, there are further approaches for runtime integration in Vue apps, each making different tradeoffs between maturity, bundler binding and feature scope.
| Tool | Bundler binding | Maturity | Distinguishing feature |
|---|---|---|---|
| Webpack 5 module federation | Webpack | Very high | Reference implementation, largest ecosystem |
| vite-plugin-federation | Vite | Good | Faster dev builds, younger project |
| Module federation 2.0 | Bundler agnostic | Growing | Runtime API, built in type sync |
| Native federation (Angular team) | None | Good | Based on import maps, framework agnostic |
| Custom elements (alternative) | None | Very high | No shared Vue needed, but shadow DOM isolation |
For new Vue projects that already use Vite as the build tool, vite-plugin-federation is usually the most pragmatic choice, because it fits seamlessly into the existing Vite configuration. Projects that already use Webpack or need maximum maturity and community support benefit more from the Webpack 5 reference implementation.
Mironsoft
Module federation, micro frontends and Vue runtime architecture
Combine Vue modules safely at runtime?
We configure module federation for your Vue apps, define shared dependency contracts between teams, and build fallback strategies that stay stable even when a remote is disrupted.
Setup
Production ready host and remote configuration with Vite or Webpack
Version contracts
Establishing contract tests and compatibility rules between teams
Resilience
Fallback components and timeout strategies for failed remotes
10. Summary
Module federation in Vue apps solves runtime integration between independently deployed applications by combining JavaScript modules only in the browser instead of wiring them together firmly at build time. Configuring host, remote and exposed modules is the easy part. The real challenge lies in shared dependencies with singleton: true, explicit version contracts between teams and robust fallback strategies for when a remote is unreachable.
TypeScript type safety across remote boundaries requires additional tooling discipline, such as published type declarations per remote. On the performance side, preloading pays off for frequently needed modules, along with separate bundle analysis per remote. Those who implement these building blocks cleanly get, with module federation, one of the most flexible techniques for scalable Vue architectures with multiple independent teams.
Module Federation in Vue Apps — the essentials at a glance
Host & remote
Host consumes, remote exposes via remoteEntry.js. Both roles are combinable.
Shared dependencies
singleton: true for Vue, router and Pinia prevents duplicated instances.
Version contracts
Contract tests between teams prevent breaking changes to exposed modules.
Resilience
errorComponent, timeout and a local fallback version for failed remotes.