← Back to all articles

Engineering / Guide

Micro-Frontend Architecture: Deep Dive into Webpack Module Federation & Single-SPA

Comprehensive architectural analysis of micro-frontends: comparing Webpack 5 Module Federation, Single-SPA, and Native Federation with real-world implementation strategies.

Table of Contents

What is micro-front end

A micro frontend is an architectural pattern in web development where a web application's user interface (UI) is divided into smaller, independently developed and deployable modules.This approach extends the principles of microservices to the frontend, allowing multiple teams to work on different parts of the UI concurrently, each using its own technology stack if desired.

Key Characteristics:

Benefits:

Challenges:

References:

Tools that provide micro-frontend arch

https://repositum.tuwien.at/bitstream/20.500.12708/17595/1/Kroiss Manuel - 2021 - From backend to frontend Case study on adopting Mmcro...pdf


1. Webpack Module Federation


2. Single-SPA


3. Qiankun


4. Web Components


5. Tailor (for Server-Side Rendering)


6. Luigi Framework


7. Piral


8. FrintJS


9. iFrame-based Solutions


Comparison Table

Tool Best Use Case Key Advantage
Module Federation Shared code and runtime-loaded micro frontends Efficient, framework-agnostic, and enables independent deployments
Single-SPA Multiple frameworks in a single app Mature and well-documented
Qiankun Enterprise-grade micro frontends Sandbox isolation
Web Components Framework-agnostic micro frontends Native support, encapsulation
Tailor Server-side composition Performance with ESI
Luigi Framework Secure, large-scale applications Centralized routing and extensibility
Piral Modular frontend applications Integrated state and service sharing
FrintJS React-based micro frontends Dependency injection for components
iFrames Legacy app integration, strict isolation Simple but less flexible

The best tool for implementing micro frontends depends on your specific project requirements, team structure, and technical constraints. Here's a breakdown to help you choose:


1. Webpack Module Federation


2. Single-SPA


3. Qiankun


4. Web Components


5. Tailor (Server-Side Composition)


6. Luigi Framework


7. Piral


8. iFrame-Based Solutions


Which Tool Should You Choose?

Use Case Recommended Tool
Efficient runtime code sharing Webpack Module Federation
Combining multiple frameworks Single-SPA
Enterprise-grade modularization Qiankun
Lightweight and framework-agnostic Web Components
Server-side rendering (SSR) Tailor
Security-focused enterprise apps Luigi Framework
Pre-built modular architecture Piral
Integrating legacy systems iFrames

Companies That Using Micro-Frontend

1. Module Federation

  1. Shopify
  2. Adidas
  3. Etc: https://module-federation.io/showcase/

2. iFrame

  1. Google: https://www.linkedin.com/pulse/micro-frontend-architecture-iframes-x-module-vinicius-marson-k1hwf
  2. Spotify: https://lucamezzalira.com/category/micro-frontends/?utm_source=chatgpt.com
  3. I believe many companies use iFrame but I couldn’t find the article

3. Web Components

  1. Google, Amazon, Salesforce, and Zalando: https://medium.com/swlh/micro-frontend-using-web-components-e9faacfc101b
  2. GitHub: https://github.blog/engineering/architecture-optimization/how-we-use-web-components-at-github/
  3. Youtube, I believe it was using web component but I coulnd’t find article for that

4. Piral

https://piral.io/


Micro Front End using Module Federation

To create a micro front-end architecture where each remote app is hosted independently and integrated into the host application, we can extend the previous example. Here, each remote app will have its own deployment and can be served from different domains or ports.


Objective


Directory Structure

micro-frontend/
├── host/
│   ├── src/
│   │   ├── App.js
│   │   ├── bootstrap.js
│   │   └── index.js
│   ├── webpack.config.js
│   └── package.json
├── remote-a/
│   ├── src/
│   │   ├── App.js
│   │   ├── bootstrap.js
│   │   └── index.js
│   ├── webpack.config.js
│   └── package.json
├── remote-b/
│   ├── src/
│   │   ├── App.js
│   │   ├── bootstrap.js
│   │   └── index.js
│   ├── webpack.config.js
│   └── package.json

Step 1: Host Configuration

host/webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    port: 4000,
  },
  output: {
    publicPath: 'http://localhost:4000/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        remoteA: 'remoteA@http://localhost:4001/remoteEntry.js',
        remoteB: 'remoteB@http://localhost:4002/remoteEntry.js',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

Step 2: Remote A Configuration

remote-a/webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    port: 4001,
  },
  output: {
    publicPath: 'http://localhost:4001/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteA',
      filename: 'remoteEntry.js',
      exposes: {
        './RemoteAppA': './src/App',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

Step 3: Remote B Configuration

remote-b/webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    port: 4002,
  },
  output: {
    publicPath: 'http://localhost:4002/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteB',
      filename: 'remoteEntry.js',
      exposes: {
        './RemoteAppB': './src/App',
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

Step 4: Components and Entry Points

remote-a/src/App.js

import React from 'react';

const RemoteAppA = () => <div>Remote App A Content</div>;

export default RemoteAppA;

remote-b/src/App.js

import React from 'react';

const RemoteAppB = () => <div>Remote App B Content</div>;

export default RemoteAppB;

host/src/App.js

import React, { Suspense } from 'react';

const RemoteAppA = React.lazy(() => import('remoteA/RemoteAppA'));
const RemoteAppB = React.lazy(() => import('remoteB/RemoteAppB'));

const App = () => (
  <div>
    <h1>Host Application</h1>
    <Suspense fallback={<div>Loading Remote A...</div>}>
      <RemoteAppA />
    </Suspense>
    <Suspense fallback={<div>Loading Remote B...</div>}>
      <RemoteAppB />
    </Suspense>
  </div>
);

export default App;

Bootstrap and Entry Files

For all three apps, add the following files:

  1. bootstrap.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
  1. index.js:
import('./bootstrap');

Step 5: HTML Templates

Minimal index.html for each app:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Micro Frontend</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Step 6: Start Applications

  1. Start Remote A:
cd remote-a
npx webpack serve
  1. Start Remote B:
cd remote-b
npx webpack serve
  1. Start Host:
cd host
npx webpack serve

Results

Visit http://localhost:4000 to see the host application displaying content from independently hosted remote apps.

Video how to use module federation:

https://www.youtube.com/watch?v=OmLsV8Dljzo

Creating a micro-frontend architecture using web components allows you to integrate applications built with different technologies (e.g., React, Angular, Vue, etc.) into a single cohesive application. Here's how you can achieve it step by step:


How to use Web Components

1. Understand Web Components as Micro-Frontend Shells

Web components provide:

Each micro-frontend can be developed as a web component and integrated into a host application.


2. Plan the Architecture


3. Steps to Build Micro-Frontends

Step 1: Create a Web Component in Each Technology

React
  1. Install dependencies:
npm install react react-dom
  1. Create a web component wrapper:
import React from 'react';
import ReactDOM from 'react-dom';
import MyReactApp from './MyReactApp';

class ReactWebComponent extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('div');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
    ReactDOM.render(<MyReactApp />, mountPoint);
  }

  disconnectedCallback() {
    ReactDOM.unmountComponentAtNode(this.shadowRoot.firstChild);
  }
}

customElements.define('react-web-component', ReactWebComponent);
Vue
  1. Install dependencies:
npm install vue
  1. Create a Vue web component:
import { defineCustomElement } from 'vue';
import MyVueApp from './MyVueApp.vue';

const VueWebComponent = defineCustomElement(MyVueApp);
customElements.define('vue-web-component', VueWebComponent);
Angular
  1. Use Angular's @angular/elements package:
ng add @angular/elements
  1. Create a custom element:
import { createCustomElement } from '@angular/elements';
import { Injector } from '@angular/core';
import { MyAngularComponent } from './my-angular.component';

const MyAngularElement = createCustomElement(MyAngularComponent, { injector });
customElements.define('angular-web-component', MyAngularElement);

Step 2: Deploy Micro-Frontends

Deploy each micro-frontend to its own domain, subdomain, or path.

Examples:


Step 3: Integrate into the Host Application

The host application can dynamically load web components from these deployments.

  1. Load Web Components: Add a script tag for each component:
<script src="https://example.com/react/main.js"></script>
<script src="https://example.com/vue/main.js"></script>
<script src="https://example.com/angular/main.js"></script>
  1. Use the Components: Include the custom elements in the host app:
<react-web-component></react-web-component>
<vue-web-component></vue-web-component>
<angular-web-component></angular-web-component>

4. Handle Communication Between Components

Use a shared event bus or custom events for communication:

// Dispatching an event
document.dispatchEvent(new CustomEvent('some-event', { detail: { data: 'example' } }));

// Listening to an event
document.addEventListener('some-event', (e) => {
  console.log(e.detail.data);
});

5. Benefits of Using Web Components


By following this approach, you can create a flexible, scalable micro-frontend architecture where each team can use their preferred technology stack while maintaining seamless integration.

Conclusion

Web components are lightweight because they use native browser APIs and avoid framework-specific runtime overhead. They are framework-agnostic because they are based on standardized web technologies, making them compatible with any framework or standalone application. These characteristics make web components ideal for building reusable and independent UI elements.

Using iFrames as micro-frontend

Using iFrames to implement a micro-frontend architecture is one of the oldest and simplest approaches. iFrames provide strong isolation between different applications, making them suitable for use cases where strict separation of code and styles is required. Here's how you can implement a micro-frontend architecture using iFrames:


1. When to Use iFrames for Micro-Frontends

Advantages:

Disadvantages:


2. Architecture Overview

  1. Host Application: The main application containing the iFrames.
  2. Micro-Frontends: Separate applications loaded within the iFrames.

3. Steps to Create Micro-Frontends Using iFrames

Step 1: Build Individual Micro-Frontend Applications

Develop each micro-frontend as a standalone application. These applications should be deployed independently.

For example:


Step 2: Host Application with iFrames

The host application is responsible for loading and displaying the iFrames.

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Micro-Frontends with iFrames</title>
  <style>
    iframe {
      width: 100%;
      height: 500px;
      border: none;
    }
  </style>
</head>
<body>
  <h1>Micro-Frontend Example with iFrames</h1>

  <!-- iFrames for Micro-Frontends -->
  <iframe src="https://cart.example.com" id="cart-app"></iframe>
  <iframe src="https://profile.example.com" id="profile-app"></iframe>
  <iframe src="https://search.example.com" id="search-app"></iframe>

</body>
</html>

Step 3: Communication Between Host and Micro-Frontends

Since iFrames are isolated, communication happens via postMessage API or query parameters in the iFrame's URL.

Using postMessage for Communication
  1. Host to iFrame:
const cartIframe = document.getElementById('cart-app');
cartIframe.contentWindow.postMessage({ action: 'updateCart', data: { items: 5 } }, 'https://cart.example.com');
  1. iFrame to Host: In the micro-frontend application:
window.parent.postMessage({ action: 'cartUpdated', data: { total: 200 } }, '*');
  1. Listening for Messages: Host application listens for events:
window.addEventListener('message', (event) => {
  if (event.origin === 'https://cart.example.com') {
    console.log('Message from Cart App:', event.data);
  }
});

Step 4: Deployment

Deploy each micro-frontend to its respective domain or subdomain. Ensure that:


4. Enhancing the iFrame Micro-Frontend Experience

  1. Dynamic Loading of iFrames: Load iFrames dynamically based on user interaction to improve initial load performance.
function loadIframe(id, url) {
  const iframe = document.getElementById(id);
  if (!iframe.src) {
    iframe.src = url;
  }
}

loadIframe('cart-app', 'https://cart.example.com');
  1. Shared Styles and Branding: Use shared CSS or a centralized design system to ensure consistency across micro-frontends.
  2. Handling Navigation: Synchronize navigation between the host and iFrames using postMessage.

5. Use Cases for iFrame-Based Micro-Frontends


6. Alternatives to iFrames

If iFrames do not meet your needs, consider using:


Using iFrames for micro-frontends is a straightforward and robust method, especially for systems requiring strong isolation or integration of legacy apps.

Challenges and Considerations

While iFrames are useful, there are some challenges:

  1. Performance Issues: iFrames may slow down the host application if the legacy system is resource-intensive.
  2. Security Concerns: Cross-site scripting (XSS) or improper CORS configurations can pose risks.
  3. Communication Limitations: Parent and iFrame applications need mechanisms like the postMessage API for inter-application communication.

Conclusion

iFrames are a powerful tool for integrating legacy systems because they provide an isolated, framework-independent, and quick solution that requires no major changes to the legacy application. Despite their limitations, they remain a practical choice for embedding old systems into modern applications while maintaining functionality.

Micro-Frontend Architectures with Qiankun and Piral

Micro-frontends are an architectural style that allows multiple applications to work together seamlessly, often by integrating smaller, independently developed frontend applications into a cohesive user experience. Two popular frameworks for achieving this are Qiankun and Piral. Here's how you can use these tools for micro-frontend development:


1. Qiankun for Micro-Frontends

Qiankun is a micro-frontend framework based on Single-SPA. It simplifies the process of integrating multiple applications with features like sandboxing, isolation, and lifecycle management.

Key Features

Architecture Overview

Steps to Implement with Qiankun

Step 1: Install Qiankun

npm install qiankun

Step 2: Create the Main Application

import { registerMicroApps, start } from 'qiankun';

// Register micro-frontends
registerMicroApps([
  {
    name: 'cart-app',
    entry: 'https://cart.example.com',
    container: '#container',
    activeRule: '/cart',
  },
  {
    name: 'profile-app',
    entry: 'https://profile.example.com',
    container: '#container',
    activeRule: '/profile',
  },
]);

// Start Qiankun
start();

Step 3: Setup Sub Applications Each micro-frontend should export lifecycle methods (bootstrap, mount, unmount):

export async function bootstrap() {
  console.log('Micro-frontend bootstrapped');
}

export async function mount() {
  console.log('Micro-frontend mounted');
}

export async function unmount() {
  console.log('Micro-frontend unmounted');
}

Step 4: Communication Between Apps Qiankun provides an event bus for communication:

// Emit an event in one app
window.dispatchEvent(new CustomEvent('cartUpdated', { detail: { items: 5 } }));

// Listen for events in another app
window.addEventListener('cartUpdated', (event) => {
  console.log('Cart updated:', event.detail);
});

Step 5: Deployment Deploy each micro-frontend independently, ensuring proper CORS and routing configurations.


2. Piral for Micro-Frontends

Piral is another powerful framework for building micro-frontends. It provides a modular architecture using Pilets (individual micro-frontends).

Key Features

Architecture Overview

Steps to Implement with Piral

Step 1: Install Piral

npm install piral-cli

Step 2: Create the Shell Application

piral new my-app-shell

Step 3: Add Pilets

pilet new --source my-app-shell

Step 4: Configure Communication Use the global state container or events for communication:

// Shell app: provide shared state
export function createSharedState() {
  const state = createState({
    cartItems: 0,
  });
  return state;
}

// Pilet: access shared state
const { cartItems } = useGlobalState();

Step 5: Deployment Deploy the shell and Pilets to a hosting service. The shell dynamically fetches and loads Pilets based on configuration.


Comparison: Qiankun vs Piral

Feature Qiankun Piral
Framework Support Framework-agnostic Framework-agnostic
Integration Approach Centralized registration Decentralized pluggable modules
Communication Custom events or props Shared state or events
Dynamic Loading Built-in Built-in
Use Cases Large-scale apps with strong isolation Modular apps with shared resources

Alternatives


Conclusion

Both Qiankun and Piral offer robust solutions for implementing micro-frontends, each catering to different architectural needs. Qiankun is better suited for strict isolation and sandboxing, while Piral excels in modularity and shared resources. Choose the one that aligns with your application's requirements and team's expertise.

Final Conclusion

1. Web Components

Web Components are a native browser feature for creating reusable components that work across different frameworks and technologies.

When to Use Web Components:

Pros:

Cons:


2. Single SPA

Single SPA is a framework for integrating multiple microfrontends into a single page, allowing you to combine independent applications into one unified user experience.

When to Use Single SPA:

Pros:

Cons:


3. Piral

Piral is a higher-level microfrontend framework built on top of single-spa. It provides a more structured approach for integrating microfrontends (called "pilets") into an application shell.

When to Use Piral:

Pros:

Cons:


4. Module Federation (Webpack 5)

Module Federation allows sharing code dynamically between different frontend applications, essentially enabling the sharing of components, libraries, or other resources.

When to Use Module Federation:

Pros:

Cons:


5. Qiankun

Qiankun is a microfrontend framework that builds on top of single-spa, simplifying microfrontend integration by handling routing, shared dependencies, and lifecycle management.

When to Use Qiankun:

Pros:

Cons:


Decision Matrix

Use Case Web Component Single SPA Piral Module Federation Qiankun
Simple, reusable components ✔ ❌ ❌ ❌ ❌
Integration of independent apps ❌ ✔ ✔ ✔ ✔
Quick setup with opinionated tooling ❌ ❌ ✔ ❌ ✔
Full control over routing & lifecycle ❌ ✔ ✔ ❌ ✔
Sharing code between microfrontends ❌ ❌ ❌ ✔ ✔
Integration with multiple frameworks ✔ ✔ ✔ ✔ ✔
Gradual migration (from monolithic to micro) ❌ ✔ ✔ ✔ ✔
State management & communication ❌ ⚙ ✔ ❌ ✔

✔: The option meets the criteria completely or is the best choice without further adjustments.
X: The option does not meet the criteria or is not a viable choice for this criterion.
⚙: The option is partially suitable and requires additional setup, adjustments, or considerations to meet the criteria.


Recommendations

  1. Use Web Components if:
    • You need reusable, independent components that can be used across different apps and frameworks.
    • You have simple microfrontend needs without complex routing, lifecycle, or communication requirements.
  2. Use Single SPA if:
    • You need flexibility and control over the integration of microfrontends and are comfortable configuring routing, lifecycle, and communication.
    • You have a set of independent apps that need to be gradually integrated into a larger app.
  3. Use Piral if:
    • You want a more structured solution with built-in state management, lifecycle management, and routing for large-scale microfrontends.
    • You need to quickly set up microfrontends in a modular, collaborative environment.
  4. Use Module Federation if:
    • Your microfrontends need to share dependencies or code (e.g., React components) dynamically and without duplication.
    • You are working in a tightly coupled environment or with a monorepo.
  5. Use Qiankun if:
    • You need a high-level, opinionated solution for integrating microfrontends with automatic handling of routing, lifecycle, and shared dependencies.
    • You have multiple apps that need to be integrated smoothly, especially when those apps are built using different technologies.

Ultimately, the choice depends on project requirements, team experience, and how much control we need over the microfrontend architecture. If we want more structure and less configuration, Piral or Qiankun could be ideal. If we prefer full flexibility, Single SPA or Module Federation might be better suited for your needs. Or if it’s only simple component then Web Component will be better.


The explanation provides guidance on choosing the appropriate microfrontend tool:

1. "If we want more structure and less configuration, Piral or Qiankun could be ideal."


2. "If we prefer full flexibility, Single SPA or Module Federation might be better suited for your needs."


3. "Or if it’s only a simple component, then Web Component will be better."


Note: