Welcome back, intrepid architect! In the previous chapters, we laid the groundwork for building robust React applications, understanding rendering strategies, and scaling our components. But what happens when your application grows so large that a single team can no longer manage it effectively, or when different parts of your UI need to be developed and deployed completely independently?
This is where Microfrontends come into play. Just as microservices revolutionized backend development by breaking down monolithic applications into smaller, manageable services, microfrontends apply a similar philosophy to the frontend. In this chapter, we’ll dive deep into this powerful architectural pattern, focusing on how Webpack Module Federation enables us to build truly decoupled and scalable React user interfaces.
You’ll learn:
- What microfrontends are, their benefits, and their challenges.
- How Webpack Module Federation works under the hood to enable runtime module sharing.
- Practical steps to build a host application that consumes multiple independent microfrontend “remotes.”
- Strategies for managing shared dependencies and avoiding common pitfalls.
Before we begin, a solid understanding of React component lifecycles, Webpack fundamentals (from Chapter 2), and basic routing concepts will be helpful. Ready to deconstruct and reconstruct your frontend like never before? Let’s get started!
What are Microfrontends? A Frontend Microservices Approach
Imagine a massive, sprawling enterprise application โ a dashboard with many widgets, a complex user profile section, an administration panel, and several other distinct features. If all these features are built within a single React application, often called a “monolith,” you might encounter several issues:
- Slow Development Cycles: A single codebase means more coordination, longer build times, and increased risk for every deployment.
- Team Bottlenecks: Multiple teams working on the same codebase can lead to merge conflicts, dependency hell, and stepping on each other’s toes.
- Technology Lock-in: Choosing a specific React version or library for the entire monolith makes it hard to experiment with newer technologies for individual features.
- Deployment Risks: A bug in one small feature can potentially bring down the entire application.
Microfrontends offer a solution by breaking down the monolithic frontend into smaller, independently deployable applications. Each microfrontend represents a distinct business domain or feature, owned by a small, autonomous team.
Think of it like building a house:
- Monolith: One large construction crew builds the entire house from foundation to roof, all at once. If the plumbing team is delayed, the whole house is delayed.
- Microfrontends: Different specialist teams build specific rooms (kitchen, living room, bedroom) independently. The kitchen team can finish and move on while the living room team is still working. Finally, everything is integrated into a cohesive home.
Benefits of Microfrontends:
- Independent Development & Deployment: Teams can build, test, and deploy their microfrontends without affecting or waiting for other teams. This accelerates delivery.
- Team Autonomy: Each team can choose its own tech stack (within reasonable boundaries), allowing for innovation and better ownership.
- Scalability: Easier to scale teams and features independently.
- Resilience: A failure in one microfrontend might not bring down the entire application.
- Incremental Upgrades: Easier to upgrade specific parts of the application or even rewrite small sections without a full-scale migration.
Challenges of Microfrontends:
- Increased Operational Complexity: More repositories, more build pipelines, more deployments to manage.
- Shared State Management: How do different microfrontends communicate or share data (e.g., user authentication status)?
- Consistent User Experience (UX): Ensuring a unified look and feel, shared design systems, and seamless navigation across disparate applications.
- Performance Overhead: Careful management of shared dependencies is crucial to avoid downloading the same libraries multiple times.
Webpack Module Federation: The Game Changer
In the past, implementing microfrontends often involved complex iframe setups, client-side composition, or server-side composition. While these methods worked, they often came with significant drawbacks in terms of performance, development experience, or complexity.
Enter Webpack Module Federation, introduced in Webpack 5. This powerful feature provides a native, robust, and elegant solution for building microfrontends. It allows multiple Webpack builds to expose and consume JavaScript modules from each other at runtime.
How it Works (The Mental Model):
Imagine you have two separate React applications, App A and App B. App A wants to use a component from App B. With Module Federation:
App Bis configured to “expose” its component. It becomes a Remote application.App Ais configured to “consume” that component. It becomes a Host application.- At runtime, when
App Aneeds the component fromApp B, Webpack handles loadingApp B’s exposed module.
This loading happens dynamically, meaning the host application doesn’t need to know about the remote’s code at build time. It simply knows where to find it.
Let’s visualize this interaction:
Key Concepts in Module Federation:
ModuleFederationPlugin: The core Webpack plugin that enables this functionality.name: A unique name for your application (remote or host) to be referenced by others.filename: The name of the remote entry file (e.g.,remoteEntry.js) that will be loaded by consuming applications.exposes: An object mapping a local module path to an exposed module name. This is how a remote application makes its components available.remotes: An object mapping a desired remote name to its remote entry URL. This is how a host application defines which remotes it can consume.shared: A crucial configuration for defining dependencies that should be shared between applications. This prevents multiple copies of libraries like React from being downloaded, which is vital for performance.
A Real Production Failure Story: The Monolith’s Demise
Consider “GlobalCorp,” a fictional company with a massive internal analytics dashboard. Initially, a single team built it as a React monolith. As the company grew, multiple product teams needed to add their specific analytics widgets.
The Problem: Every new widget, every bug fix, and every dependency update required a full build and deployment of the entire monolithic dashboard.
- Deployment Freeze: When Team Alpha needed to deploy a critical security patch, Team Beta’s new feature was held hostage, waiting for Alpha’s release train.
- Dependency Collisions: Team Gamma updated a common UI library, inadvertently introducing a breaking change for Team Delta’s widget, causing a P0 production outage that took down the entire dashboard.
- Slow Builds: The CI/CD pipeline for the monolith took over an hour, leading to developer frustration and slow feedback loops.
- Fear of Change: The codebase became so large and intertwined that engineers were hesitant to refactor or upgrade core libraries, fearing unknown side effects.
This led to missed deadlines, increased operational costs, and a significant drop in developer morale. The solution? A strategic shift to microfrontends using Webpack Module Federation, allowing each product team to own, build, and deploy their widgets independently, transforming the monolithic beast into a flexible, scalable ecosystem.
Step-by-Step Implementation: Building a Microfrontend Enterprise Suite
Let’s build a simple microfrontend suite. We’ll create:
- An
app-shell(host application) that provides the main layout and navigation. - A
dashboard-widget(remote application) that exposes a simple data display widget. - A
user-profile(remote application) that exposes a user information component.
We’ll use Vite for scaffolding our React projects due to its speed, but critically, we will integrate Webpack Module Federation using the @module-federation/webpack plugin, as Vite’s native module resolution doesn’t directly support the Webpack-specific federation protocol.
Setup: Project Structure
First, create a new directory for our project and navigate into it:
mkdir microfrontend-suite
cd microfrontend-suite
1. Create the Host Application (app-shell)
This will be our main application that pulls in other microfrontends.
# Create React app with Vite
npm create vite@latest app-shell -- --template react-ts
cd app-shell
npm install
Now, we need to configure Webpack Module Federation. We’ll install Webpack and the necessary plugins.
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader
Explanation:
webpack,webpack-cli,webpack-dev-server: Standard Webpack tools.html-webpack-plugin: Generates ourindex.htmlfile.@module-federation/webpack: The crucial plugin for Module Federation.babel-loader,@babel/preset-react,@babel/preset-typescript: For transpiling React and TypeScript.css-loader,style-loader: For handling CSS.
Next, create a webpack.config.js file in the app-shell directory.
// app-shell/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');
module.exports = {
entry: './src/index.tsx', // Our main entry point
mode: 'development',
devServer: {
port: 8080, // Host app will run on port 8080
historyApiFallback: true, // For client-side routing
},
output: {
publicPath: 'auto', // Important for Module Federation to resolve paths
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app_shell', // Unique name for our host app
remotes: {
// Here we define the remotes we want to consume
// The format is: 'remoteName': 'remoteName@http://localhost:port/remoteEntry.js'
dashboard: 'dashboard_widget@http://localhost:8081/remoteEntry.js',
profile: 'user_profile@http://localhost:8082/remoteEntry.js',
},
shared: {
// Crucial for performance: share common dependencies
// This ensures React and ReactDOM are loaded only once
react: {
singleton: true, // Only a single version of React should be loaded
requiredVersion: '^18.0.0', // Specify compatible React version (adjust for 2026, e.g., '^19.0.0' or '^20.0.0')
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0', // Same for ReactDOM
},
// Add other shared dependencies like react-router-dom, etc.
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
Explanation of app-shell/webpack.config.js:
name: 'app_shell': Identifies this application in the federation network.remotes: This is where the host declares which remote applications it will consume. We specifydashboardandprofileand their entry points.shared: This is extremely important. By markingreactandreact-domassingleton: trueand specifyingrequiredVersion, we instruct Webpack to ensure that only one instance of these libraries is loaded into the browser, even if multiple microfrontends depend on them. This prevents duplicate bundles and potential runtime issues. For 2026, React^18.0.0is a safe baseline, but newer major versions like^19.0.0or^20.0.0would be common.
Update app-shell/public/index.html (if it exists, or create it):
<!-- app-shell/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Microfrontend App Shell</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Update app-shell/src/index.tsx:
// app-shell/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Update app-shell/src/App.tsx to include placeholder content for now:
// app-shell/src/App.tsx
import React from 'react';
const App: React.FC = () => {
return (
<div>
<h1>Welcome to the App Shell!</h1>
<p>Loading microfrontends soon...</p>
</div>
);
};
export default App;
Modify app-shell/package.json to add a start script:
// app-shell/package.json (add this script)
"scripts": {
"start": "webpack serve --open",
"build": "webpack --mode production"
},
2. Create Remote Application 1 (dashboard-widget)
This microfrontend will expose a simple widget.
# Go back to the root directory
cd ..
npm create vite@latest dashboard-widget -- --template react-ts
cd dashboard-widget
npm install
Install Webpack and Module Federation plugins, similar to the host:
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader
Create dashboard-widget/webpack.config.js:
// dashboard-widget/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
port: 8081, // This remote app will run on port 8081
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'dashboard_widget', // Unique name for this remote
filename: 'remoteEntry.js', // The file the host will load
exposes: {
// What we expose from this remote
'./DashboardWidget': './src/DashboardWidget.tsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0', // Match host's required version
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
Explanation of dashboard-widget/webpack.config.js:
name: 'dashboard_widget': Unique identifier for this remote.filename: 'remoteEntry.js': The name of the JavaScript bundle that contains the exposed modules and the federation runtime logic.exposes: This is where we declare which modules (components in our case) from this application are made available to others../DashboardWidgetmaps to the component located at./src/DashboardWidget.tsx.shared: Again, we sharereactandreact-domto ensure singletons.
Create dashboard-widget/public/index.html:
<!-- dashboard-widget/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Widget Remote</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Update dashboard-widget/src/index.tsx:
// dashboard-widget/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import DashboardWidget from './DashboardWidget';
// This remote app can also be run independently for development
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<h2>Running Dashboard Widget Independently</h2>
<DashboardWidget />
</React.StrictMode>
);
Create dashboard-widget/src/DashboardWidget.tsx (the component we’ll expose):
// dashboard-widget/src/DashboardWidget.tsx
import React from 'react';
const DashboardWidget: React.FC = () => {
const data = {
users: 12345,
revenue: '$56,789',
activeSessions: 789,
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px', backgroundColor: '#f9f9f9' }}>
<h3>๐ Sales Dashboard Widget</h3>
<p>Total Users: <strong>{data.users}</strong></p>
<p>Monthly Revenue: <strong>{data.revenue}</strong></p>
<p>Active Sessions: <strong>{data.activeSessions}</strong></p>
<button onClick={() => alert('Refreshing data from Dashboard Widget!')}>Refresh Data</button>
</div>
);
};
export default DashboardWidget;
Modify dashboard-widget/package.json to add a start script:
// dashboard-widget/package.json (add this script)
"scripts": {
"start": "webpack serve --open",
"build": "webpack --mode production"
},
3. Create Remote Application 2 (user-profile)
This will expose a user profile component.
# Go back to the root directory
cd ..
npm create vite@latest user-profile -- --template react-ts
cd user-profile
npm install
Install Webpack and Module Federation plugins:
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin @module-federation/webpack @babel/core babel-loader @babel/preset-react @babel/preset-typescript css-loader style-loader
Create user-profile/webpack.config.js:
// user-profile/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('@module-federation/webpack').default;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
mode: 'development',
devServer: {
port: 8082, // This remote app will run on port 8082
historyApiFallback: true,
},
output: {
publicPath: 'auto',
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'user_profile',
filename: 'remoteEntry.js',
exposes: {
'./UserProfile': './src/UserProfile.tsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
Create user-profile/public/index.html:
<!-- user-profile/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Profile Remote</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Update user-profile/src/index.tsx:
// user-profile/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import UserProfile from './UserProfile';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<h2>Running User Profile Independently</h2>
<UserProfile />
</React.StrictMode>
);
Create user-profile/src/UserProfile.tsx:
// user-profile/src/UserProfile.tsx
import React from 'react';
const UserProfile: React.FC = () => {
const user = {
name: 'Alice Wonderland',
email: '[email protected]',
role: 'Administrator',
lastLogin: '2026-02-14 10:30 AM',
};
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px', backgroundColor: '#eef' }}>
<h3>๐ค User Profile</h3>
<p>Name: <strong>{user.name}</strong></p>
<p>Email: <strong>{user.email}</strong></p>
<p>Role: <strong>{user.role}</strong></p>
<p>Last Login: <strong>{user.lastLogin}</strong></p>
</div>
);
};
export default UserProfile;
Modify user-profile/package.json to add a start script:
// user-profile/package.json (add this script)
"scripts": {
"start": "webpack serve --open",
"build": "webpack --mode production"
},
4. Integrate Remotes into the Host (app-shell)
Now that our remotes are set up, let’s update app-shell/src/App.tsx to dynamically load and render them.
First, we need to declare the types for our dynamically loaded remotes so TypeScript knows what to expect. Create a file app-shell/src/declarations.d.ts:
// app-shell/src/declarations.d.ts
declare module 'dashboard/DashboardWidget' {
const DashboardWidget: React.ComponentType;
export default DashboardWidget;
}
declare module 'profile/UserProfile' {
const UserProfile: React.ComponentType;
export default UserProfile;
}
Now, modify app-shell/src/App.tsx to use these remote components:
// app-shell/src/App.tsx
import React, { Suspense } from 'react';
// Dynamically import the remote components
// The syntax 'remoteName/exposedModuleName' corresponds to
// the 'remotes' config in webpack.config.js and 'exposes' config in remote's webpack.config.js
const DashboardWidget = React.lazy(() => import('dashboard/DashboardWidget'));
const UserProfile = React.lazy(() => import('profile/UserProfile'));
const App: React.FC = () => {
return (
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
<h1 style={{ color: '#333' }}>Welcome to the Microfrontend App Shell!</h1>
<p>This is where we integrate all our independent components.</p>
<div style={{ display: 'flex', gap: '20px', marginTop: '30px' }}>
{/* Use Suspense to handle loading states for dynamically imported components */}
<Suspense fallback={<div>Loading Dashboard Widget...</div>}>
<DashboardWidget />
</Suspense>
<Suspense fallback={<div>Loading User Profile...</div>}>
<UserProfile />
</Suspense>
</div>
<footer style={{ marginTop: '50px', paddingTop: '20px', borderTop: '1px solid #eee', fontSize: '0.9em', color: '#666' }}>
Powered by Webpack Module Federation
</footer>
</div>
);
};
export default App;
Explanation of app-shell/src/App.tsx:
React.lazy(): This function lets you render a dynamic import as a regular component. It’s built for code-splitting and works perfectly with Module Federation.Suspense: A React component that lets you “wait” for some code to load and specify a loading indicator (fallback) while it’s happening. If the remote module fails to load, you might want to wrap this in anError Boundaryfor a more robust solution (as discussed in Chapter 6: Error Handling).
Running the Microfrontend Suite
To see this in action, you need to start all three applications simultaneously. Open three separate terminal windows:
Terminal 1 (Dashboard Widget):
cd microfrontend-suite/dashboard-widget npm startThis will start the
dashboard-widgetonhttp://localhost:8081.Terminal 2 (User Profile):
cd microfrontend-suite/user-profile npm startThis will start the
user-profileonhttp://localhost:8082.Terminal 3 (App Shell):
cd microfrontend-suite/app-shell npm startThis will start the
app-shellonhttp://localhost:8080.
Now, open your browser and navigate to http://localhost:8080. You should see the app-shell loading and displaying both the “Sales Dashboard Widget” and the “User Profile” components, seamlessly integrated from their independent applications! If you stop one of the remote applications, you’ll see the Suspense fallback message appear, demonstrating the dynamic loading.
Mini-Challenge: Add a New Settings Microfrontend
Your turn! Let’s reinforce your understanding by adding another microfrontend.
Challenge: Create a new microfrontend called app-settings that exposes a simple “Settings” component. Then, integrate this Settings component into your app-shell alongside the existing dashboard and profile widgets.
Steps:
- Create a new React project named
app-settings. - Install Webpack and Module Federation dependencies.
- Configure
app-settings/webpack.config.jsto expose aSettingscomponent (e.g.,./SettingsPage). Remember to set itsport(e.g., 8083) and configurename,filename,exposes, andshared. - Create a simple
app-settings/src/SettingsPage.tsxcomponent. - Update
app-shell/webpack.config.jsto includeapp-settingsin itsremotesconfiguration. - Add a type declaration for the new
settingsmodule inapp-shell/src/declarations.d.ts. - Modify
app-shell/src/App.tsxto dynamically import and render theSettingsPagecomponent, usingReact.lazyandSuspense. - Start all four applications and verify the integration.
Hint: Follow the exact pattern used for dashboard-widget and user-profile. Pay close attention to unique port numbers, name properties, and the remotes/exposes mapping.
What to Observe/Learn:
- You’ll solidify your understanding of the host-remote relationship and the configuration required for each.
- You’ll appreciate how easily new features (as microfrontends) can be added to the existing shell without touching the other remote applications.
Common Pitfalls & Troubleshooting
Working with microfrontends and Module Federation can introduce new complexities. Here are some common issues and how to tackle them:
Shared Dependency Version Mismatches:
- Problem: If the host and a remote both try to load different major versions of a shared library (e.g., React 17 and React 18), you can get runtime errors, duplicate bundles, or unexpected behavior.
- Solution: Use the
sharedconfiguration diligently.singleton: true: Ensures only one instance of the dependency is loaded.requiredVersion: '^18.0.0': Specifies a compatible semantic version range. If a remote requires a version outside this range, Webpack will either warn or error, depending on thestrictVersionflag.strictVersion: true: (Defaultfalse) If set totrue, Webpack will throw an error if therequiredVersiondoesn’t strictly match the available shared version. This is safer for critical dependencies.
- Debugging: Check your browser’s network tab. If you see multiple
react.jsorreact-dom.jsbundles, you likely have a shared dependency issue. Also, inspect the console for Webpack Module Federation warnings about version conflicts.
Loading Errors / Network Issues:
- Problem: A remote application might not be running, its
remoteEntry.jsfile might be inaccessible, or there could be a network issue. This leads to broken UI in the host. - Solution:
Suspensefallback: As demonstrated,Suspenseprovides a basic loading state.- Error Boundaries: Wrap your dynamically loaded remote components in React
Error Boundaries. This allows you to gracefully catch loading failures or runtime errors originating from the remote and display a user-friendly message or fallback UI, preventing the entire host application from crashing. - Network Tab: Always check the network tab in your browser’s developer tools to see if
remoteEntry.jsfiles are being loaded correctly and without HTTP errors. - CORS: Ensure your
webpack-dev-serverfor each remote is configured to allow CORS requests from the host’s origin if they are on different domains/ports. In our example,publicPath: 'auto'and default dev server settings usually handle this for local development.
- Problem: A remote application might not be running, its
Routing Conflicts and History Management:
- Problem: If both the host and remotes use client-side routing (e.g.,
react-router-dom), they can interfere with each other’s history stack or render the wrong component. - Solution:
- Shared Router: Often, the host application owns the primary router. Remotes can then register their routes with the host’s router or use relative paths within their isolated context.
- Abstracted Routing: Create a shared routing utility or context that the host provides, allowing remotes to navigate or define sub-routes without directly managing the browser history.
- Event-Driven Navigation: Remotes can emit events (e.g., using a custom event bus or shared state management) that the host listens to for navigation instructions.
- Problem: If both the host and remotes use client-side routing (e.g.,
Performance Overheads:
- Problem: If not managed carefully, microfrontends can lead to larger total bundle sizes due to duplicated dependencies or inefficient code splitting.
- Solution:
- Aggressive
sharedconfiguration: Share as many common libraries as possible. - Lazy Loading: Use
React.lazy()andSuspensefor all remote components to ensure they are only loaded when needed. - Code Splitting within Remotes: Remotes should also implement their own code splitting to reduce their initial bundle size.
- Caching: Leverage browser caching and CDN delivery for
remoteEntry.jsfiles.
- Aggressive
Summary
Congratulations! You’ve successfully navigated the exciting world of microfrontends with Webpack Module Federation. Let’s recap the key takeaways:
- Microfrontends break down large frontend monoliths into smaller, independently deployable applications, fostering team autonomy and accelerating development.
- Webpack Module Federation provides a native, powerful mechanism to achieve runtime composition of these independent applications.
- The Host application consumes modules exposed by Remote applications.
- The
ModuleFederationPluginwithname,filename,exposes,remotes, and crucially,sharedconfigurations are central to this pattern. - Shared dependencies (like React and ReactDOM) are vital for performance and stability, preventing duplicate downloads and runtime conflicts.
React.lazyandSuspenseare essential for dynamically loading remote components and providing a smooth user experience.- While offering immense benefits, microfrontends introduce complexities in areas like shared state, routing, and operational management, requiring careful architectural consideration.
You’ve now got a foundational understanding of how to build truly scalable and decoupled frontend systems. This architectural pattern is increasingly becoming a standard for large-scale enterprise applications in 2026, enabling teams to move faster and with greater confidence.
In the next chapter, we’ll delve into managing large-scale routing and state boundaries within such complex applications, ensuring that even with many independent parts, your users experience a cohesive and predictable flow.
References
- Webpack Module Federation Official Documentation
- React Docs: Code Splitting
- MDN Web Docs: Webpack
- Module Federation with React: A practical guide
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.