Microfrontend Implementations Unpacked Module Federation iFrames and Web Components
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
The landscape of modern web development is continuously evolving, with a growing emphasis on building scalable, maintainable, and independently deployable applications. As front-end applications grow in complexity and team size, monoliths often become bottlenecks, hindering agility and developer productivity. This challenge has paved the way for the adoption of microfrontend architectures, a paradigm that extends the principles of microservices to the front end. By breaking down large front-end applications into smaller, autonomous units, microfrontends promise greater flexibility, faster development cycles, and improved team autonomy. However, choosing the right implementation strategy for microfrontends is crucial. This article will thoroughly explore three prominent technologies for building microfrontends: Module Federation, iFrames, and Web Components, comparing their approaches, use cases, and helping you navigate this critical decision.
Core Concepts in Microfrontends
Before diving into the specifics of each technology, let's establish a common understanding of some core concepts relevant to microfrontend architectures, which will be referenced throughout our discussion.
Microfrontend: An architectural style where a web application is composed of many independent front-end applications that can be developed, deployed, and managed autonomously.
Host Application: The main application that orchestrates and integrates various microfrontends. It provides the shell or layout for the entire user experience.
Remote Application (or Child Microfrontend): An independent front-end application that is loaded and displayed within the host application.
Isolation: The degree to which a microfrontend operates independently, without inadvertently affecting or being affected by other microfrontends or the host. This includes isolation of JavaScript, CSS, and global state.
Runtime Integration: The process of loading and displaying microfrontends within the host application while the application is running in the browser.
Build-Time Integration: The process where microfrontends are combined and bundled together during the build step before deployment.
Shared Dependencies: Common libraries or frameworks (e.g., React, Vue, Lodash) that multiple microfrontends might need, and which ideally should be loaded only once to avoid performance penalties.
Module Federation
Module Federation, introduced with Webpack 5, is a powerful and relatively new feature designed to address the challenges of sharing code and dependencies between independently built applications. It allows different Webpack builds to expose and consume modules from each other at runtime.
How it Works
At its core, Module Federation enables a "host" application to dynamically load code from "remote" applications. Both host and remote applications are essentially Webpack builds configured to act in these roles. A remote application exposes certain modules as "federated modules," which the host application can then consume as if they were local modules. Critical to this is the concept of "shared modules," where common dependencies (like React or a design system) can be singled out and loaded only once, even if multiple federated modules depend on them.
Implementation Example
Consider a scenario where our host application needs to load a ProductDetail
component from a remote ProductApp
.
Remote (ProductApp
's webpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8081/', // Public URL where this app is served }, devServer: { port: 8081, }, plugins: [ new ModuleFederationPlugin({ name: 'productApp', filename: 'remoteEntry.js', exposes: { './ProductDetail': './src/components/ProductDetail', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... other Webpack configurations };
Host (HostApp
's webpack.config.js
):
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:8080/', // Public URL where this app is served }, devServer: { port: 8080, }, plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { productApp: 'productApp@http://localhost:8081/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^17.0.2' }, 'react-dom': { singleton: true, requiredVersion: '^17.0.2' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], // ... other Webpack configurations };
Host (HostApp
's src/App.js
):
import React, { Suspense } from 'react'; const ProductDetail = React.lazy(() => import('productApp/ProductDetail')); const App = () => { return ( <div> <h1>Host Application</h1> <Suspense fallback={<div>Loading Product Detail...</div>}> <ProductDetail productId="123" /> </Suspense> </div> ); }; export default App;
In this example, the HostApp
dynamically loads the ProductDetail
component from the ProductApp
at runtime. Shared dependencies like React are optimized to load only once.
Application Scenarios
Module Federation excels in scenarios where:
- You need to share large, complex components or entire sub-applications.
- Optimized loading of shared libraries is crucial for performance.
- Teams require high levels of autonomy over their front-end developments and deployments.
- Frameworks are the same or compatible across different microfrontends (though Module Federation generally handles framework differences well, especially with wrappers).
- You are building a single-page application (SPA) that combines features from multiple teams.
iFrames
iFrames (inline frames) are a traditional HTML element that allows embedding another HTML document within the current HTML document. They provide a high degree of isolation, as each iFrame operates within its own browser context.
How it Works
An iFrame essentially creates a separate browsing context. This means the JavaScript, CSS, and local storage within an iFrame are completely isolated from the parent document and other iFrames. Communication between the parent and child (or child-to-child) typically relies on the postMessage
API, which securely sends messages between different origins.
Implementation Example
Host (index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with iFrame</title> </head> <body> <h1>Host Application</h1> <iframe id="productIframe" src="http://localhost:8082/product-app.html" style="width: 100%; height: 400px; border: 1px solid blue;" ></iframe> <script> const iframe = document.getElementById('productIframe'); // Listen for messages from the iframe window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8082') { // Verify origin for security return; } console.log('Message from iframe:', event.data); // Example: Update host UI based on iframe message }); // Send message to the iframe after it loads iframe.onload = () => { iframe.contentWindow.postMessage({ type: 'INIT_DATA', payload: { userId: 'abc' } }, 'http://localhost:8082'); }; </script> </body> </html>
Remote (product-app.html
running on http://localhost:8082
):
<!DOCTYPE html> <html> <head> <title>Product App (in iFrame)</title> </head> <body> <h2>Product Details</h2> <div id="product-info">Loading...</div> <script> const productInfoDiv = document.getElementById('product-info'); // Listen for messages from the parent window window.addEventListener('message', (event) => { if (event.origin !== 'http://localhost:8080') { // Verify origin return; } console.log('Message from host:', event.data); if (event.data.type === 'INIT_DATA') { productInfoDiv.innerHTML = ``; } }); // Example: Send a message to the host setTimeout(() => { window.parent.postMessage({ type: 'PRODUCT_LOADED', payload: { productId: '456' } }, 'http://localhost:8080'); }, 2000); </script> </body> </html>
Application Scenarios
iFrames are suitable when:
- Maximum isolation is required, for example, embedding third-party content or potentially insecure applications.
- Different components are built with vastly different technology stacks and need to operate independently without interference.
- You need to embed legacy applications into a modern shell.
- Security concerns demand strict separation of environments (e.g., payment gateways).
- SEO is not a primary concern for the embedded content (as search engines historically struggled with iFrame content, though this has improved).
- The performance overhead of multiple browser contexts is acceptable.
Web Components
Web Components are a set of W3C standards that allow developers to create custom, reusable, encapsulated HTML tags. They provide a native way to build modular components that can be used across different frameworks and in vanilla JavaScript.
How it Works
Web Components consist of four main technologies:
- Custom Elements: Allowing you to define new HTML tags.
- Shadow DOM: Providing an encapsulated DOM and styles for a component, isolating it from the main document's styles and scripts.
- HTML Templates: Allowing you to declare markup fragments that are not rendered until instantiated.
- ES Modules: For importing and exporting modules.
They offer strong encapsulation of both markup and styles, meaning a component's internal structure and styling won't bleed out or be affected by external CSS, and vice versa. Communication typically happens through standard DOM events and properties/attributes.
Implementation Example
Custom Element Definition (product-card.js
):
class ProductCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); // Create a shadow DOM const template = document.createElement('template'); template.innerHTML = ` <style> .card { border: 1px solid #ccc; padding: 16px; margin: 16px; border-radius: 8px; font-family: sans-serif; } h3 { color: #333; } .price { font-weight: bold; color: green; } </style> <div class="card"> <h3></h3> <p class="description"></p> <p class="price"></p> <button>Add to Cart</button> </div> `; this.shadowRoot.appendChild(template.content.cloneNode(true)); this.titleElement = this.shadowRoot.querySelector('h3'); this.descriptionElement = this.shadowRoot.querySelector('.description'); this.priceElement = this.shadowRoot.querySelector('.price'); this.button = this.shadowRoot.querySelector('button'); this.button.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('add-to-cart', { detail: { productId: this.getAttribute('product-id') }, bubbles: true, composed: true // Allows event to pass through shadow DOM boundary })); }); } // Define observed attributes static get observedAttributes() { return ['product-id', 'title', 'description', 'price']; } // Respond to attribute changes attributeChangedCallback(name, oldValue, newValue) { if (name === 'title') { this.titleElement.textContent = newValue; } else if (name === 'description') { this.descriptionElement.textContent = newValue; } else if (name === 'price') { this.priceElement.textContent = `$${parseFloat(newValue).toFixed(2)}`; } } } // Define the custom element customElements.define('product-card', ProductCard);
Host (index.html
):
<!DOCTYPE html> <html> <head> <title>Host App with Web Components</title> <script type="module" src="./product-card.js"></script> </head> <body> <h1>Host Application</h1> <div id="product-list"> <product-card product-id="P001" title="Super Widget" description="The best widget you'll ever own." price="29.99" ></product-card> <product-card product-id="P002" title="Mega Gadget" description="A revolutionary device for modern living." price="99.00" ></product-card> </div> <script> document.getElementById('product-list').addEventListener('add-to-cart', (event) => { console.log('Product added to cart:', event.detail.productId); // Handle cart logic in the host application }); </script> </body> </html>
Application Scenarios
Web Components are best suited for:
- Creating reusable UI components that can be shared across different projects, teams, and even frameworks.
- Achieving framework-agnostic component development.
- When strong UI and style encapsulation is desired without the overhead of iFrames.
- Building design systems where individual components need to be highly portable and maintainable.
- Hybrid applications where parts are built with different frameworks (e.g., React host with Vue Web Components).
Comparison of Microfrontend Implementations
Let's summarize the key characteristics, pros, and cons of each approach:
Feature/Criterion | Module Federation | iFrames | Web Components |
---|---|---|---|
Isolation Level | High (runtime), shared context for JS & CSS often | Very High (browser context) | High (Shadow DOM for CSS, native JS isolation) |
Runtime Performance | Good (shared dependencies, lazy loading) | Moderate to Poor (new browsing context per iFrame) | Good (native browser features) |
Development Complexity | Moderate (Webpack configuration can be complex) | Low to Moderate (standard HTML, postMessage ) | Moderate (native APIs, can be verbose) |
Sharing Dependencies | Excellent (native Webpack feature) | Poor (each iFrame loads its own deps) | Moderate (global scope, external mechanisms) |
Framework Agnosticism | Good (can wrap different frameworks) | Excellent (entirely separate apps) | Excellent (native standard) |
Routing | Highly flexible (integrates into host router) | Complex (each iFrame has its own history/URL) | Flexible (integrates into host router) |
Communication | Direct JS calls, shared state management | postMessage API | Custom Events, properties/attributes |
SEO Impact | Generally good (dynamic loading, unified DOM) | Potentially poor (content in separate contexts) | Generally good (unified DOM) |
Use Cases | SPAs with loosely coupled features, shared libs, team autonomy | Embedding third-party content, legacy apps, high security isolation | Reusable UI components, design systems, framework-agnostic libs |
Styling | Shared stylesheets, CSS-in-JS, CSS Modules | Completely isolated (separate stylesheets) | Encapsulated (Shadow DOM CSS) |
Bundle Size | Optimized by sharing common libraries | Can be large (each app bundles its own deps) | Relatively small (native APIs) |
Conclusion
The choice between Module Federation, iFrames, and Web Components for implementing microfrontends largely depends on your specific project requirements, existing infrastructure, budget, and desired level of isolation. Module Federation offers a sophisticated, Webpack-driven solution for achieving highly integrated and performant microfrontends, especially within a JavaScript ecosystem. iFrames provide unparalleled isolation, making them ideal for embedding disparate applications or untrusted content at the cost of potential performance and integration complexity. Web Components offer a native and framework-agnostic way to build reusable UI components with strong encapsulation, bridging the gap for shared design systems and cross-framework integration. Each approach brings unique strengths to the table, and a clear understanding of their respective trade-offs is paramount for building robust and scalable microfrontend architectures.