Frontend Development

Building Scalable Micro-Frontends Using Module Federation in Webpack 5

In the evolving landscape of frontend development, monolithic applications often become bottlenecks for large engineering teams. As applications grow, so does the complexity of dependency management, deployment cycles, and code ownership. This is where the Micro-Frontend architecture shines, allowing teams to build, test, and deploy independent frontend applications. While early implementations relied on complex iframe shims or runtime bundling solutions, Webpack 5 introduced a native, robust solution: Module Federation.

This post explores how to leverage Webpack 5’s Module Federation Plugin to create truly scalable, independent micro-frontends that can be shared and consumed across different applications without the traditional overhead.

What is Module Federation?

Module Federation is a concept that allows JavaScript applications to dynamically import code from other runtimes (browsers, servers, or Web Workers) at runtime. Unlike traditional bundlers that resolve dependencies at build time, Module Federation resolves them at execution time. This means Application A can run code written in Application B, as if it were local, provided both applications are running.

The key benefits include:

  • Independent Deployments: Teams can update and deploy their specific micro-frontend without touching the host application.
  • Shared Dependencies: Critical libraries like React or Redux can be shared between apps to reduce bundle size and ensure consistency.
  • Technology Agnosticism: While Webpack is the primary tool, the protocol allows integration with other build systems, though Webpack offers the most mature implementation.

Setting Up the Host and Remote Applications

To demonstrate Module Federation, let’s assume a scenario with two applications:

  1. Host App: The main shell application that orchestrates the UI.
  2. Remote App: A feature-specific module (e.g., a user dashboard or chat component) that will be consumed by the Host.

Step 1: Exposing Modules in the Remote App

First, we need to configure the Remote App to expose its components. In the webpack.config.js file of the remote application, we import the ModuleFederationPlugin and define the exposes field.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // ... other config
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './UserProfile': './src/components/UserProfile',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
};

Here, we expose ./UserProfile. We also configure shared dependencies. By setting singleton: true, we ensure that only one instance of React is loaded across both applications, preventing state mismatches and reducing payload size.

Step 2: Consuming Modules in the Host App

Next, we configure the Host App to consume the exposed module. We add the remote configuration in the ModuleFederationPlugin of the host application.

new ModuleFederationPlugin({
  name: 'hostApp',
  remotes: {
    remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
  }
})

The remotes field tells Webpack where to find the remote entry file. You can use a static URL or a dynamic endpoint that returns the configuration based on the environment. Note that the shared configuration must match the remote app to ensure compatibility.

Dynamic Importing in React

With the configuration in place, you can dynamically import the remote component using the System.import API (polyfilled by Webpack) or standard dynamic imports. Here is an example in a React component:

import React, { lazy, Suspense } from 'react';

// Lazily load the remote component
const UserProfile = lazy(() => import('remoteApp/UserProfile'));

const HostComponent = () => {
  return (
    
Loading...
}>
); }; export default HostComponent;

Using lazy and Suspense is crucial here. It ensures that the remote code is downloaded only when needed and provides a fallback UI during the network request.

Best Practices and Challenges

While Module Federation simplifies micro-frontend architecture, it introduces new challenges:

Version Compatibility

Shared dependencies must have compatible versions. If the Host uses React 18 and the Remote uses React 17, the application will likely fail. Always use semantic versioning and clearly document shared dependency constraints.

Error Boundaries

If a remote application crashes, it should not break the entire host shell. Wrap remote components in React Error Boundaries to catch and display graceful error messages.

Communication Between Apps

Micro-frontends are loosely coupled. Communication should happen via custom events, state management libraries (like Redux or Zustand), or web components. Avoid direct DOM manipulation across app boundaries.

Conclusion

Webpack 5’s Module Federation is a game-changer for frontend architecture. It democratizes the micro-frontend concept by providing a standardized, build-time integrated solution that doesn’t require complex runtime bootstrapping. By enabling independent deployments and shared dependencies, it allows large organizations to scale their frontend engineering teams effectively.

Whether you are migrating a legacy monolith or building a new platform from scratch, Module Federation offers a powerful toolset to achieve agility, resilience, and scalability in your JavaScript applications.

Share: