As a product grows, so does the organization building it. Different business domains emerge, teams split up, and each team develops its own pace and priorities. But if the codebase stays in one piece, every small change requires coordination and every deployment affects the entire organization.
That is where Micro Frontend comes in.
In this article I will explain micro frontend architecture from the ground up. Instead of handing you a ready-made library list, I wanted to explain the underlying logic. The examples are written in React, but the architectural principles I cover (isolation, event bus, integration approaches) can be applied with any framework. I chose React to show the full picture in one consistent language.
I have a personal connection to this topic. At one of the companies I worked at, I had the chance to be a maintainer on an open-source micro frontend framework called VoltranJS. That experience let me understand this architecture deeply, both in theory and in practice.
1. What Is Micro Frontend? π€
Micro Frontend is an architecture that breaks a large and complex frontend application into small pieces that can be developed independently, deployed independently, and updated without blocking each other. Each of these independent pieces is called a micro app.
π― In short: what microservices do for the backend, Micro Frontend does for the frontend.
Unlike backend microservices, however, micro apps share the same browser environment (same DOM, same window, same localStorage). Isolation does not come for free here; it must be provided deliberately.
Consider an e-commerce site. Home page, product listing, product detail, cart, checkout -- these are all areas with different business logic, different requirements, and different rates of change. Micro frontend makes it possible for each of these areas to:
β Live in its own repository
β Have its own CI/CD pipeline
β Be deployed independently
β Use different technology stacks if desired
β οΈ The freedom to use different technology stacks is theoretical. In practice, most successful micro frontend implementations standardize around a single main framework. I cover this in detail in Section 3 -- Disadvantages.
2. Monolith vs Micro Frontend π
βββββββββββββββββββββββββββββββββββββββββββββββ
β Single Large Frontend App β
β ββββββββββ ββββββββββββ βββββββββββββββ β
β β Header β β Products β β Checkout β β
β ββββββββββ ββββββββββββ βββββββββββββββ β
β One Repo -- One Deploy -- All Teams β
βββββββββββββββββββββββββββββββββββββββββββββββ
Problems that arise as a monolith grows:
β Every small change requires deploying the entire application
β Multiple teams working on the same codebase leads to constant merge conflicts
β Different teams' different speed requirements get squeezed into a single pipeline
β Modernizing legacy code forces an all-or-nothing decision
β One team's bug can bring down the entire application
Micro frontend addresses these problems like this:
ββββββββββββββββ βββββββββββββββββ ββββββββββββββ
β Header β β Products β β Cart β
β Team A β β Team B β β Team C β
β Independent β β Independent β βIndependent β
β Deploy β β Deploy β β Deploy β
ββββββββββββββββ βββββββββββββββββ ββββββββββββββ
β β β
ββββββββββββββββββ΄βββββββββββββββββββ
β
ββββββββββββββββββββ
β Main App β
ββββββββββββββββββββ
3. Advantages and Disadvantages βοΈ
3.1 Advantages
β
Independent Deployment
Each team can ship their own micro app without waiting for other teams. A payment team's deployment does not affect the cart team's release schedule.
β
Team Autonomy
Each team can make its own technology decisions. Onboarding gets easier; a new developer only needs to learn their own micro app, not the entire application.
β
Fault Isolation
If a micro app crashes, error boundaries keep the others running. In a monolith, a single bug can bring down the whole page.
β
Incremental Modernization
Instead of rewriting a legacy application all at once, you can migrate piece by piece to modern technology. Old pages keep working while new micro apps are added one by one.
β
Parallel Development
Multiple teams can work on different micro apps at the same time, completing their sprints without blocking each other.
β
Easy A/B Testing
You can run region-based or user-based tests simply by deploying a different version of the relevant micro app.
3.2 Disadvantages
β
Increased Operational Complexity
Each micro app means a separate service: separate repo, separate CI/CD, separate monitoring. This overhead can be heavy for small teams.
β
Initial Load Performance
Each micro app means a separate JavaScript bundle. HTTP/2 multiplexing reduces the multi-request problem, but total JavaScript parse and execution time still increases. A poorly configured dependency strategy can bloat the page.
β
Design Inconsistency
If each team manages its own CSS, visual inconsistencies build up over time. A shared design system becomes mandatory.
β
Test Complexity
Integration and end-to-end tests require keeping multiple services running at the same time.
β
Dependency Conflicts
If different micro apps use incompatible library versions, two instances of the same library end up running in the browser simultaneously.
β
Multiple Framework Cost
The freedom for each team to use its own framework is theoretical. In practice, each additional technology stack loads its own runtime (framework core ~40-50 kb + router ~15-20 kb + state library ~10-30 kb gzipped), developers cannot move between teams, the UI kit has to be written separately for each framework, and the test matrix multiplies. Most successful implementations standardize around a single main framework.
β
Drawing Wrong Boundaries
If domain boundaries are drawn based on technology preferences rather than business requirements, unnecessary network requests and data duplication become inevitable. This is the most common structural mistake.
4. Core Concepts π§±
4.1 Main App
This is the shell of the micro frontend architecture -- the central structure that brings all other micro apps together. Routing, navigation, and shared layout live in this layer. Authentication and global state are typically managed here as well. The main app also takes on the orchestration role, deciding how and where micro apps are composed. That composition can happen client-side, server-side, or at the edge.
Some micro frontend resources also call this the "shell" or "container." I used "Main App" throughout this article to avoid confusion with other uses of those words in software. You will see "shell" in the code examples because that is the common directory naming convention.
4.2 Micro App
A small frontend application responsible for a specific business domain, deployed independently. In Module Federation terminology it is also called a remote.
π‘ The word "remote" is heavily overloaded in software, so outside the Module Federation section I use "Micro App" throughout the article.
4.3 Routing
The main app is responsible for unmounting the current micro app and mounting the new one when the route changes. There are two common patterns:
β
Main-app-side routing: All URL navigation is defined in the main app. The micro app only knows its mount point; it has no idea which URL it will be shown on. This provides centralized control and keeps micro apps fully isolated.
// shell/src/App.js -- main app
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';
// Micro apps are lazy-loaded (integration details in Section 7)
const Home = lazy(() => import('homeApp/Home'));
const Catalog = lazy(() => import('catalogApp/Catalog'));
const Cart = lazy(() => import('cartApp/Cart'));
const Fallback = ({ name }) => (
<div className="mfe-skeleton">{name} loading...</div>
);
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={
<Suspense fallback={<Fallback name="Home" />}>
<Home />
</Suspense>
} />
<Route path="/products/*" element={
<Suspense fallback={<Fallback name="Products" />}>
<Catalog />
</Suspense>
} />
<Route path="/cart" element={
<Suspense fallback={<Fallback name="Cart" />}>
<Cart />
</Suspense>
} />
</Routes>
</BrowserRouter>
);
}
β
Micro-app-side routing: The micro app manages its own internal navigation (for example, moving from a product list to a product detail page). The main app only knows which root URL to mount the micro app at; everything inside is the micro app's responsibility.
// catalog-micro-app/src/CatalogApp.js
import { Routes, Route } from 'react-router-dom';
import ProductList from './ProductList';
import ProductDetail from './ProductDetail';
// The main app mounts this micro app under "/products/*".
// The micro app manages its own nested routes.
export default function CatalogApp() {
return (
<Routes>
<Route index element={<ProductList />} />
<Route path=":id" element={<ProductDetail />} />
</Routes>
);
}
π‘ The two patterns are not mutually exclusive. The main app handles top-level routing (which micro app to show) while the micro app handles its own nested routes. Together, the flow looks like this:
URL: /products/abc-123
Main app routing (App.js):
"/products/*" β mount <Catalog /> micro app
Micro app routing (CatalogApp.js):
"/products/" β <ProductList /> (index route)
"/products/:id" β <ProductDetail /> β abc-123 matches here
The "/products/*" wildcard in the main app lets the micro app recognize its own nested paths. The main app answers "which micro app," the micro app answers "which sub-page."
5. How Is Isolation Achieved? π
One of the most critical challenges in micro frontend is isolation: how do you prevent one micro app's CSS or JavaScript from overriding another's?
5.1 CSS Isolation
The browser's global CSS scope is shared by all micro apps. If the same class name is used in two different micro apps, one will override the other.
.container { background: blue; } /* Micro App A */
.container { background: red; } /* Micro App B -- overrides the rule above! */
The most practical and widespread solution is CSS Modules. With CSS Modules you give the file a .module.css extension, and at build time an automatic hash is added to every class name. So instead of .container you get something like .ProductList_container__3xMq2, making collisions impossible. SCSS Modules use the same isolation mechanism (.module.scss extension) and add SCSS features like variables, nesting, and mixins on top. I used SCSS Modules in the examples throughout this article.
/* ProductList.module.scss */
.container {
padding: 1rem;
border-radius: 8px;
.title {
font-size: 1.5rem;
color: #111827;
}
&:hover { background: #f9fafb; }
$primary: #4f46e5;
.badge {
background: $primary;
color: white;
padding: 2px 8px;
border-radius: 999px;
}
}
// ProductList.js
import styles from './ProductList.module.scss';
function ProductList() {
return (
<div className={styles.container}>
<h2 className={styles.title}>Products</h2>
<span className={styles.badge}>New</span>
</div>
);
}
// Rendered to DOM as: <div class="ProductList_container__3xMq2">
π‘ Alternative approaches: CSS-in-JS libraries (styled-components, Emotion) or Shadow DOM also provide isolation. In a micro frontend context, though, SCSS Modules offer the lowest runtime cost and the broadest tooling compatibility.
5.2 JavaScript Isolation
Global variables, properties on window, and localStorage are the most common sources of collisions.
// Micro App A
window.currentUser = { id: 1, name: 'Alice' };
// Micro App B overwrites Micro App A's data
window.currentUser = { id: 2, name: 'Bob' };
The correct approach:
window.__MFE__ = window.__MFE__ || {};
window.__MFE__.header = { currentUser: { id: 1, name: 'Alice' }, version: '1.2.3' };
window.__MFE__.cart = { items: [], total: 0 };
β
For localStorage, use a prefix:
const PREFIX = 'mfe_header_';
localStorage.setItem(`${PREFIX}user`, JSON.stringify(user));
localStorage.getItem(`${PREFIX}user`);
β οΈ A namespace is only a naming convention; it is not enforced at runtime. Any micro app can still read or overwrite
window.__MFE__.cart. This approach works only when every team respects the convention.
6. Communication with Event Bus π‘
Micro apps should not import each other directly; that creates coupling and breaks independent deployment. Communication should be event-based. The CustomEvent API on window is all you need for this:
const bus = {
emit(event, data) {
window.dispatchEvent(new CustomEvent(event, { detail: data }));
},
on(event, handler) {
/*
wrapper:
(1) extracts e.detail
(2) stores the reference for cleanup
*/
const wrapper = (e) => handler(e.detail);
window.addEventListener(event, wrapper);
return () => window.removeEventListener(event, wrapper); // cleanup
},
};
export { bus };
You publish this package as @org/event-bus in your private npm registry (GitHub Packages, npm org scope, Verdaccio, etc.). How the package is distributed to micro apps (Module Federation shared config, vendor CDN, etc.) depends on the integration approach you choose; I will cover those in Section 7.
β
Why CustomEvent?
Because it lives on window, events emitted from different bundles naturally see each other on the same page. No extra instance-sharing is needed. When any micro app calls bus.emit() from its own bundle, the bus.on() listeners in other micro apps will catch it. Zero dependencies, native browser API.
6.1 Usage
π‘ Naming convention: The format
mfe:{microapp}:{action}is recommended. Themfe:prefix prevents collisions (and typically matches the name of the micro frontend framework in use), the middle segment identifies the source, and the trailing segment describes the action.
// ProductsApp.jsx
import React from "react";
import { bus } from "@org/event-bus";
const products = [
{ id: 1, name: "Keyboard", price: 500 },
{ id: 2, name: "Mouse", price: 300 },
];
export default function ProductsApp() {
const addToCart = (product) => {
// β
We are sending the product info
bus.emit("mfe:cart:add", {
productId: product.id,
price: product.price,
quantity: 1,
});
};
return (
<div>
<h2>Products</h2>
{products.map((product) => (
<div key={product.id}>
<span>
{product.name} - ${product.price}
</span>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
);
}
// CartApp.jsx
import React, { useEffect, useState } from "react";
import { bus } from "@org/event-bus";
export default function CartApp() {
const [cartItems, setCartItems] = useState([]);
useEffect(() => {
const handleAddToCart = (item) => {
setCartItems((prev) => {
const existing = prev.find(p => p.productId === item.productId);
if (existing) {
return prev.map(p =>
p.productId === item.productId
? { ...p, quantity: p.quantity + 1 }
: p
);
}
return [...prev, item];
});
};
// β
We are receiving the product info sent from the other component and running cleanup
/* This way:
β Listener is removed when the component unmounts
β No memory leaks
β Multiple mount/unmount cycles are safe
*/
const unsubscribe = bus.on("mfe:cart:add", handleAddToCart);
return unsubscribe;
}, []);
return (
<div>
<h2>Cart</h2>
{cartItems.map((item) => (
<div key={item.productId}>
Product: {item.productId} |
Quantity: {item.quantity} |
Price: ${item.price}
</div>
))}
</div>
);
}
6.2 Communication Scenarios
** β
Client to Client: Two micro apps on the same page communicate**
import { bus } from '@org/event-bus';
function ProductCard({ product }) {
return (
<button onClick={() =>
bus.emit('mfe:cart:add', { productId: product.id, price: product.price })
}>
Add to Cart
</button>
);
}
** β Client to Server to Notification: Notifying other micro apps after a backend action**
Sending a request to the server does not require the event bus; a standard fetch is enough. But when the result needs to be broadcast to other micro apps, that is where the event bus comes in:
// checkout micro app
import { bus } from '@org/event-bus';
async function initiatePayment(cartId, paymentMethod) {
// Standard fetch request to the server
const res = await fetch('/api/checkout/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cartId, paymentMethod }),
});
const { orderId } = await res.json();
// Broadcast the result to other micro apps via event bus
bus.emit('mfe:order:created', { orderId });
}
7. Integration Approaches π
There are multiple ways to compose micro apps together. Each has different strengths and weaknesses; choosing the right one depends on your project's needs.
7.1 Build-Time Integration
Micro apps are published as npm packages, and the main app includes them in a single bundle at build time. The package being published here is the micro app itself -- all business logic, state, and API calls are embedded into the main app's build.
Micro App A β npm publish β @org/header@1.2.3
Main App β npm install @org/header β build β deploy
β Simple to set up
β The micro app cannot be updated without rebuilding the main app -- true independent deployment is not possible
7.2 Run-Time Client-Side Integration
Micro apps are loaded dynamically at runtime in the browser. This can be done in two ways: the manual approach and Module Federation.
7.2.1 Manual Approach
In its simplest form, each micro app's bundle is fetched from a CDN and mounted into the page:
async function bootstrapApp() {
const [HeaderModule, ProductsModule] = await Promise.all([
import('https://header.cdn.example.com/header.esm.js'),
import('https://products.cdn.example.com/products.esm.js'),
]);
HeaderModule.default.mount(document.getElementById('header'));
ProductsModule.default.mount(document.getElementById('products'));
}
β Each micro app can be deployed completely independently
β Initial load time increases
β Dependency sharing (avoiding duplicate loading of common libraries like React) must be managed manually by the developer
That manual management problem leads us to the shared dependency problem and Module Federation.
7.2.2 The Shared Dependency Problem
If every micro app bundles its own copy of React and similar libraries, the user downloads the same code multiple times. This wastes bandwidth and causes two separate instances of the same library to run on the page at the same time.
header.bundle.js β React (~42kb gzipped) + own code (8kb)
products.bundle.js β React (~42kb gzipped) + own code (15kb)
cart.bundle.js β React (~42kb gzipped) + own code (6kb)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3 copies loading, 2 of them unnecessary β 84kb wasted
** β Vendor Bundle Approach (Pre-Module Federation)**
Before Module Federation, the common solution was to serve shared libraries as a single vendor bundle (vendor.js) from a CDN, and have micro apps use webpack's externals config to exclude those libraries from their own bundles. The externals setting tells webpack not to include a library in the bundle and instead read it from the global scope at runtime (for example window.React):
// Micro app webpack.config.js (pre-Module Federation approach)
module.exports = {
externals: {
react: 'React', // import React β taken from window.React
'react-dom': 'ReactDOM', // import ReactDOM β taken from window.ReactDOM
},
};
<!-- Main app layout -- loads vendor libraries into global scope -->
<script src="https://cdn.example.com/vendor/react.18.2.0.min.js"></script>
<script src="https://cdn.example.com/vendor/react-dom.18.2.0.min.js"></script>
<!-- Micro app bundles no longer include React; they use window.React -->
<script src="https://header.cdn.example.com/header.bundle.js"></script>
<script src="https://products.cdn.example.com/products.bundle.js"></script>
This works, but management is manual: you have to centrally coordinate which library is used at which version. Module Federation automates this process.
7.2.3 Module Federation
Module Federation, introduced with webpack 5, was the most game-changing innovation in the micro frontend world. It solves the dependency-sharing problem at runtime, at the webpack level.
Main App (Host): The central application that consumes modules from other applications. Called the "host" in Module Federation terminology.
Remote (Micro App): An independent application that exposes its own modules. Each remote produces a remoteEntry.js file. This file works like a manifest, declaring which modules the remote exposes and where their JavaScript files are located. The main app downloads this manifest first on page load, then requests the actual code for the needed modules on demand.
Shared: Common dependencies. When marked with singleton: true, only a single instance of a library like React is guaranteed to load.
Remote Configuration:
// products-app/webpack.config.js (micro app)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: {
publicPath: 'https://products.cdn.example.com/',
},
plugins: [
new ModuleFederationPlugin({
name: 'productsApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Main App (Host) Configuration:
// shell-app/webpack.config.js (main app)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
productsApp: 'productsApp@https://products.cdn.example.com/remoteEntry.js',
cartApp: 'cartApp@https://cart.cdn.example.com/remoteEntry.js',
headerApp: 'headerApp@https://header.cdn.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Using a Remote Module in the Main App:
// shell-app/src/App.js (main app)
import React, { Suspense, lazy } from 'react';
// Remote modules are loaded with lazy import
const ProductList = lazy(() => import('productsApp/ProductList'));
const CartBadge = lazy(() => import('cartApp/CartBadge'));
const Header = lazy(() => import('headerApp/Header'));
const Fallback = ({ name }) => (
<div className="mfe-skeleton">{name} loading...</div>
);
export default function App() {
return (
<>
<Suspense fallback={<Fallback name="Header" />}>
<Header />
</Suspense>
<main>
<Suspense fallback={<Fallback name="Products" />}>
<ProductList />
</Suspense>
</main>
<Suspense fallback={<Fallback name="Cart" />}>
<CartBadge />
</Suspense>
</>
);
}
When remote modules are wrapped with lazy, the Suspense fallback is shown while loading. Once the remote's JavaScript is downloaded, the actual component renders.
As of 2024, Module Federation has evolved into an independent project (module-federation/core) that can be used with bundlers beyond webpack.
7.3 Which Approach Should You Choose?
β Build-time integration is the simplest to set up and type safety comes naturally, but it cannot provide independent deployment. The main app has to be rebuilt with every update. It can work for small teams or rarely-changing micro apps, but it becomes a bottleneck as you scale.
β
Manual client-side integration offers independent deployment; each micro app is served from its own CDN and loaded at runtime. However, dependency sharing is entirely the developer's responsibility -- you have to manually coordinate externals and CDN settings to prevent duplicate loading of libraries like React. Initial load time also increases.
β
Module Federation offers the same runtime independence while automating dependency sharing. singleton: true guarantees that shared libraries load only once. It works with webpack and, increasingly, with other bundlers.
8. Design System Management π¨
When multiple teams work independently, buttons end up different colors, spacing becomes inconsistent, and typography standards drift.
The solution is straightforward: a shared design system.
8.1 CSS Variables (Custom Properties): Single Source of Truth
Defining design decisions (color, spacing, typography, border radius) as CSS custom properties is the simplest way to maintain consistency across all micro apps. Instead of hardcoding values in CSS, micro apps read from these variables:
/* @org/design-system/variables.css */
:root {
--color-primary: #4F46E5;
--color-primary-hover: #4338CA;
--color-success: #10B981;
--color-danger: #EF4444;
--color-text: #111827;
--color-text-muted: #6B7280;
--color-bg: #FFFFFF;
--color-bg-secondary: #F9FAFB;
--color-border: #E5E7EB;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--font-family: 'Inter', system-ui, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.5rem;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 999px;
--color-skeleton: #F0F0F0;
--color-skeleton-wave: #E0E0E0;
}
/* products-micro-app/ProductCard.module.scss */
.card {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-md);
.title { font-size: var(--font-size-lg); color: var(--color-text); }
.price { color: var(--color-primary); font-size: var(--font-size-xl); }
}
The advantage of this approach: when a brand color changes, you update only the variables file, and all micro apps automatically pick up the new values.
8.2 Shared UI Kit: npm Package
Beyond variables, shared components (Button, Input, Modal, Card) are published as an npm package. Micro apps add it as a dependency. What is shared here as an npm package is strictly presentational components with no business logic; the micro apps themselves continue to be deployed independently.
// products micro app
import { Button, Card } from '@org/ui-kit';
function ProductCard({ product }) {
return (
<Card padding="md" bordered>
<h3>{product.name}</h3>
<Button variant="primary" onClick={() => addToCart(product)}>
Add to Cart
</Button>
</Card>
);
}
The UI kit is managed with semantic versioning. For minor updates (adding a new component), micro apps can upgrade at their own pace. For major updates (breaking changes), all teams should be given a reasonable migration window.
π‘ The approach that works best in practice: load CSS variables in the layout, distribute the UI kit as an npm package, and give all teams 2-3 sprints for major version migrations.
9. When to Use It, When Not To π€
The core question that determines whether to adopt micro frontend is: Are your teams' business domains sufficiently separated?
Based on my experience and common industry patterns, micro frontend starts delivering value when there are multiple independent teams with clear domain separation. There is no hard number, but for single-team or very small organizations, the operational overhead typically outweighs the benefits.
9.1 When Micro Frontend Is a Good Fit
β There are clear boundaries between different business domains (checkout, catalog, search, account) managed by separate teams
β Teams have started blocking each other's deployments
β Different areas have significantly different rates of change
β A large legacy application needs to be modernized piece by piece
β A/B tests need to run frequently and independently in a specific area of the page
β Teams want full ownership of their own release schedule
9.2 When Micro Frontend Is Not a Good Fit
β The application is still in a growth phase and domain boundaries are not yet clear; wrongly drawn boundaries will create large refactoring costs later
β Teams are still making decisions together on every feature; architecture alone does not solve organizational problems
β The goal is shipping a fast MVP; product first, architecture second
β Performance and bundle size are the primary constraints; every additional micro app adds overhead
β Developer experience and local setup complexity cannot be tolerated
What Is Next? πΊοΈ
This article covered the core concepts of micro frontend architecture, integration approaches, and the key things to watch out for. If you want to keep exploring, here are some topics worth looking into:
Server-Side Integration and Hydration
Rendering micro apps on the server, composing the layout server-side, and making it interactive on the client (hydration). SSR and CSR micro apps coexisting on the same page, graceful degradation strategies, and resolving hydration mismatch issues.
Performance and Monitoring
Per-micro-app Core Web Vitals measurement and reporting. Error boundaries to keep a crashing micro app from affecting others. Skeleton placeholders to prevent Cumulative Layout Shift. Above-the-fold/below-the-fold lazy loading. Critical CSS inlining. INP (Interaction to Next Paint) measurement and optimization per micro app.
Local Development Experience
Day-to-day development in a micro frontend setup: running a micro app in standalone mode, testing main app integration with mock micro apps, full environment setup with Docker Compose. The answer to: "Do I really have to spin up 10 services just to work on my own micro app?"
Micro Frontend with Next.js
How Next.js's App Router, Server Components, and built-in SSR capabilities combine with micro frontend architecture. Module Federation's Next.js integration (@module-federation/nextjs-mf), page-based micro app separation, and using Next.js's own middleware layer as the main app.
Feedback π¬
While writing this article, I used Claude Opus 4.6 Thinking for proofreading, my own notes from my time as a VoltranJS maintainer combined with ChatGPT 5.2's deep research feature for sourcing and research, and Gemini 3 Pro Image Preview 2k (Nano Banana Pro) for generating the diagrams.
Feedback, suggestions, and corrections are always welcome. You can reach me through the social media links on my website or on LinkedIn.
Take care, Yasin π€
Resources π
Open Source Repos
- https://github.com/verdaccio/verdaccio
- https://github.com/hepsiburada/VoltranJS
- https://github.com/puzzle-js/puzzle-js
- https://github.com/single-spa/single-spa
- https://github.com/umijs/qiankun
- https://github.com/modern-js-dev/garfish
- https://github.com/namecheap/ilc
- https://github.com/smapiot/piral
- https://github.com/SAP/luigi
- https://github.com/zalando/tailor
- https://github.com/americanexpress/one-app
- https://github.com/opencomponents/oc
- https://github.com/module-federation/core
- https://github.com/module-federation/module-federation-examples
- https://github.com/systemjs/systemjs
- https://github.com/chrisdavies/eev
- https://github.com/developit/mitt
- https://github.com/originjs/vite-plugin-federation
- https://github.com/react-microfrontends
- https://github.com/vue-microfrontends
- https://github.com/neuland/micro-frontends
- https://github.com/rajasegar/awesome-micro-frontends
Articles and Docs
- https://martinfowler.com/articles/micro-frontends.html
- https://micro-frontends.org/
- https://www.thoughtworks.com/radar/techniques/micro-frontends
- https://medium.com/swlh/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669
- https://medium.com/hepsiburadatech/hepsiburada-micro-frontend-d%C3%B6n%C3%BC%C5%9F%C3%BCm%C3%BC-4c2f26b8dcae
- https://medium.com/frontend-development-with-js/scalable-frontend-geli%C5%9Ftirmelerinde-micro-frontend-yakla%C5%9F%C4%B1mlar%C4%B1-framework-ve-pluginler-8d0e5773c34f
- https://www.slideshare.net/slideshow/hepsiburada-micro-frontends-dnm/229602398
- https://single-spa.js.org/docs/getting-started-overview
- https://webpack.js.org/concepts/module-federation/
- https://blog.scottlogic.com/2021/02/17/probably-dont-need-microfrontends.html
- https://newsletter.systemdesign.one/p/micro-frontends
- https://allegro.tech/2016/03/Managing-Frontend-in-the-microservices-architecture.html
- https://engineering.hellofresh.com/front-end-microservices-at-hellofresh-23978a611b87
- https://www.infoq.com/news/2018/08/experiences-micro-frontends/
- https://www.upwork.com/blog/2017/05/modernizing-upwork-micro-frontends/
Books
- Micro Frontends in Action -- https://www.manning.com/books/micro-frontends-in-action
- Building Micro-Frontends -- https://www.buildingmicrofrontends.com/
- The Art of Micro Frontends -- https://www.packtpub.com/product/the-art-of-micro-frontends/9781800563568
- Practical Module Federation -- https://module-federation.myshopify.com/products/practical-module-federation
Courses
- https://www.udemy.com/course/microfrontend-course/
- https://www.pluralsight.com/courses/micro-frontends-architecture
- https://www.freecodecamp.org/news/learn-all-about-micro-frontends/








Top comments (0)