正在加载,请稍候…

Module Federation in 2026: Building Micro-Frontends with Webpack and Vite

Learn Module Federation from first principles: host/remote architecture, shared dependencies, dynamic loading, version management, and independent deployment pipelines.

Module Federation in 2026: What Has Changed

Module Federation, introduced with Webpack 5 in 2020, has matured significantly. In 2026, it is no longer just a Webpack feature — the ecosystem includes native Vite plugins, framework-agnostic implementations, and production battle-testing across hundreds of enterprise deployments.

This guide covers Module Federation from first principles through advanced production patterns, including the Vite Federation plugin that has made it accessible to Vite users.

What Is Module Federation?

Module Federation allows JavaScript applications to dynamically load code from other applications at runtime. Unlike traditional code splitting (which splits your own code), Federation allows you to share code across entirely separate deployments.

The core model:

  • Host: The shell application that loads remote modules
  • Remote: An independent application that exposes modules
  • Shared modules: Dependencies (like React) shared to avoid duplicate loading
Browser loads shell app (host)
        |
        |-- At runtime, discovers remote URLs
        |
        |-- Loads product-catalog.example.com/remoteEntry.js
        |       └── Provides: ProductCatalog, ProductCard components
        |
        |-- Loads cart.example.com/remoteEntry.js
                └── Provides: CartWidget, useCart hook

Each remote can be deployed independently. A change to the cart service does not require redeploying the shell or any other remote.

Webpack 5 Module Federation Setup

// webpack.config.js — Host application
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',

      remotes: {
        // Format: name@url/remoteEntry.js
        productCatalog: 'productCatalog@https://catalog.example.com/remoteEntry.js',
        cart: 'cart@https://cart.example.com/remoteEntry.js',
        userProfile: 'userProfile@https://profile.example.com/remoteEntry.js',
      },

      shared: {
        react: {
          singleton: true,        // Only one instance of React across all remotes
          requiredVersion: '^18.0.0',
          eager: true,            // Load with host, not lazily
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
          eager: true,
        },
        // Shared design system
        '@company/design-system': {
          singleton: true,
          requiredVersion: '^3.0.0',
        },
      },
    }),
  ],
}
// webpack.config.js — Remote application (product catalog)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'productCatalog',
      filename: 'remoteEntry.js',

      exposes: {
        // What this remote makes available to hosts
        './ProductCatalog': './src/components/ProductCatalog',
        './ProductCard': './src/components/ProductCard',
        './useProductSearch': './src/hooks/useProductSearch',
      },

      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        '@company/design-system': { singleton: true, requiredVersion: '^3.0.0' },
      },
    }),
  ],
}
// In the host — using remote components
// These are dynamically loaded at runtime from catalog.example.com

const ProductCatalog = React.lazy(() => import('productCatalog/ProductCatalog'))
const ProductCard    = React.lazy(() => import('productCatalog/ProductCard'))

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <ProductCatalog />
    </Suspense>
  )
}

Vite Federation Plugin

The @originjs/vite-plugin-federation plugin brings Module Federation to Vite projects:

npm install -D @originjs/vite-plugin-federation
// vite.config.ts — Host
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'shell',
      remotes: {
        productCatalog: 'https://catalog.example.com/assets/remoteEntry.js',
        cart: 'https://cart.example.com/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    modulePreload: false,
    target: 'esnext', // Required for module federation
    minify: false,    // Optional: easier debugging
    cssCodeSplit: false,
  },
})
// vite.config.ts — Remote (product catalog)
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'productCatalog',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductCatalog': './src/components/ProductCatalog.tsx',
        './ProductCard': './src/components/ProductCard.tsx',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext',
  },
})

Dynamic Remote Discovery

Hardcoding remote URLs in configuration is inflexible. Dynamic discovery enables runtime flexibility:

// remoteRegistry.ts
interface RemoteConfig {
  name: string
  url: string
  version: string
}

class RemoteRegistry {
  private registry: Map<string, RemoteConfig> = new Map()

  async initialize() {
    // Fetch remote configurations from a registry service
    const response = await fetch('/api/micro-frontend-registry')
    const configs: RemoteConfig[] = await response.json()

    configs.forEach(config => {
      this.registry.set(config.name, config)
    })
  }

  async loadRemote(name: string, module: string) {
    const config = this.registry.get(name)
    if (!config) throw new Error(`Remote '${name}' not found in registry`)

    // Dynamic remote loading
    // @ts-ignore — __webpack_init_sharing__ and __webpack_share_scopes__ are injected by MF
    await __webpack_init_sharing__('default')

    const container = window[name as keyof Window] as any
    if (!container) {
      // Load the remote entry script dynamically
      await loadScript(config.url)
    }

    await container.init(__webpack_share_scopes__.default)
    const factory = await container.get(module)
    return factory()
  }
}

function loadScript(url: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url
    script.onload = () => resolve()
    script.onerror = () => reject(new Error(`Failed to load remote: ${url}`))
    document.head.appendChild(script)
  })
}

export const remoteRegistry = new RemoteRegistry()

Shared State Across Micro-Frontends

Micro-frontends need to communicate without tight coupling:

// shared-state/eventBus.ts — published as a shared module
type EventMap = {
  'cart:item-added': { productId: string; quantity: number }
  'user:authenticated': { userId: string; token: string }
  'navigation:route-change': { path: string }
}

class EventBus {
  private listeners: Map<keyof EventMap, Set<Function>> = new Map()

  on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void) {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set())
    this.listeners.get(event)!.add(handler)
    return () => this.listeners.get(event)!.delete(handler)
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
    this.listeners.get(event)?.forEach(handler => handler(data))
  }
}

// Ensure singleton across all micro-frontends
declare global {
  interface Window { __EVENT_BUS__?: EventBus }
}

export const eventBus = window.__EVENT_BUS__ ?? (window.__EVENT_BUS__ = new EventBus())

Version Management and Compatibility

The trickiest part of micro-frontends: what happens when Host expects React 18.2 but Remote ships React 18.3?

// Strict version pinning — safest but requires coordinated deployments
shared: {
  react: {
    singleton: true,
    strictVersion: true,
    requiredVersion: '18.2.0', // Exact version
  }
}

// Flexible version range — more independent deployments
shared: {
  react: {
    singleton: true,
    strictVersion: false,      // Warn instead of error on mismatch
    requiredVersion: '^18.0.0', // Any 18.x version is acceptable
  }
}

Shared dependency strategy:

  • React, ReactDOM: Always singleton, strict version pinning
  • Design system: Singleton, semver minor flexibility
  • Utility libraries (lodash, date-fns): Non-singleton, each remote manages its own version

Independent Deployment Pipeline

# .github/workflows/deploy-catalog.yml
name: Deploy Product Catalog
on:
  push:
    branches: [main]
    paths:
      - 'apps/product-catalog/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: |
          cd apps/product-catalog
          npm ci
          npm run build

      - name: Integration test with host
        run: |
          # Start host app pointing to new catalog build
          HOST_URL=http://localhost:3000           CATALOG_REMOTE_URL=http://localhost:3001/assets/remoteEntry.js           npx playwright test tests/integration/catalog.spec.ts

      - name: Deploy to CDN
        run: |
          aws s3 sync apps/product-catalog/dist             s3://company-mfe-catalog/latest/             --cache-control "public, max-age=31536000"

          # Update registry to point to new version
          curl -X PUT https://registry.example.com/api/remotes/productCatalog             -d '{"url": "https://cdn.example.com/catalog/latest/assets/remoteEntry.js"}'

When to Use Module Federation

Use micro-frontends with Module Federation when:

  • Multiple teams own different parts of the same application
  • Independent deployment of features is a business requirement
  • Different parts of the app have genuinely different tech stacks or upgrade cadences
  • Application is large enough that a monorepo with shared packages is insufficient

Do not use when:

  • Single team owns the entire frontend
  • Application is small to medium sized
  • Network latency for loading remotes is unacceptable in your use case
  • Team does not have the operational maturity to run multiple deployment pipelines

The honest truth: Most applications do not need micro-frontends. A well-organized monorepo with clear ownership and a shared component library solves the same organizational problems with much less complexity. Reach for Module Federation when the team and deployment requirements genuinely demand it.

Production Checklist

Before going live with Module Federation:

  • Remote entry URLs are served from a CDN with proper cache headers
  • Fallback behavior is implemented for when a remote fails to load
  • Error boundaries wrap every remote component
  • Integration tests verify cross-remote communication
  • Performance budget accounts for remote entry overhead (~5-15KB each)
  • Version registry has rollback capability
  • Monitoring alerts on remote load failures