Microfrontend Architecture: A Scalable Solution for Enterprise Applications â
What Are Microfrontends? â
Microfrontends is an architectural pattern that decomposes a single frontend application into multiple small, independent, and separately developable, testable, and deployable sub-applications. Each sub-application can be developed by different teams using different technology stacks, ultimately combining to deliver a unified application experience to users.
Imagine you're working on a large e-commerce platform with homepage, product search, shopping cart, user center, payment, and other functional modules. What problems would you encounter if all features were developed in a single massive frontend project?
<!-- Problems with Traditional Monolithic Frontend Applications -->
<!DOCTYPE html>
<html>
<head>
<title>Large E-commerce Platform</title>
<!-- All pages need to load the same huge CSS file -->
<link rel="stylesheet" href="app.12345678.css" />
</head>
<body>
<div id="app"></div>
<!-- All pages need to load the same huge JavaScript file -->
<script src="app.12345678.js"></script>
<!-- A small feature change requires rebuilding the entire application -->
</body>
</html>Problems with this monolithic architecture:
- Difficult Team Collaboration: Multiple teams modifying the same codebase leads to frequent conflicts
- Technology Stack Lock-in: The entire application must use the same technology stack, with high costs for technology upgrades
- Low Deployment Efficiency: Any small change requires rebuilding and deploying the entire application
- Performance Bottlenecks: Initial load requires downloading all code, affecting first-screen speed
- High Maintenance Cost: High code coupling, where modifying one feature may affect others
Core Value of Microfrontends â
1. Technology Stack Agnosticism â
Each microfrontend application can choose the most suitable technology stack:
// Main App - Using React
const MainApp = () => {
return (
<div>
<Header />
<MicroApp name="products" url="/products/" />
<MicroApp name="cart" url="/cart/" />
<MicroApp name="user" url="/user/" />
<Footer />
</div>
);
};
// Products Micro App - Using Vue
// products/app.js
const ProductsApp = Vue.createApp({
template: `
<div>
<h1>Product List</h1>
<ProductList />
</div>
`,
}).mount("#products-app");
// Cart Micro App - Using Angular
// cart/app.js
angular.module("cartApp", []).component("cartAppComponent", {
template: `
<div>
<h1>Shopping Cart</h1>
<CartItems />
</div>
`,
});2. Independent Development and Deployment â
Each microfrontend can be developed, tested, and deployed independently:
# Team A - Products Page Micro App
name: products-micro-app
version: 1.0.0
framework: Vue 3
build_command: npm run build
deploy_command: npm run deploy
repository: https://github.com/company/products-micro-app
team: frontend-team-A
# Team B - Shopping Cart Micro App
name: cart-micro-app
version: 2.1.0
framework: React 18
build_command: npm run build
deploy_command: npm run deploy
repository: https://github.com/company/cart-micro-app
team: frontend-team-B
# Team C - User Center Micro App
name: user-micro-app
version: 1.5.0
framework: Angular 15
build_command: ng build
deploy_command: ng deploy
repository: https://github.com/company/user-micro-app
team: frontend-team-C3. Progressive Refactoring and Upgrades â
Microfrontends support progressive refactoring of legacy systems:
// Progressive Refactoring Strategy
const LegacyPage = () => {
const [useNewHeader, setUseNewHeader] = useState(false);
const [useNewSearch, setUseNewSearch] = useState(false);
return (
<div>
{/* Independently toggle new features */}
{useNewHeader ? <NewHeader /> : <OldHeader />}
<div className="main-content">
{useNewSearch ? <NewSearch /> : <OldSearch />}
{/* Keep core functionality unchanged */}
<LegacyContent />
</div>
{/* Gradually migrate to micro apps */}
{useNewHeader && <NewHeaderMicroApp />}
{useNewSearch && <NewSearchMicroApp />}
</div>
);
};
// Feature Toggles
const FeatureToggle = {
newHeaderEnabled: process.env.ENABLE_NEW_HEADER === "true",
newSearchEnabled: process.env.ENABLE_NEW_SEARCH === "true",
microAppsEnabled: process.env.ENABLE_MICRO_APPS === "true",
};Microfrontend Implementation Approaches â
1. Routing-Based Distribution â
// Main App Routing Configuration
const routes = [
{
path: "/",
component: MainLayout,
children: [
{ path: "", component: HomeApp },
{ path: "products/*", component: () => import("microapps/products") },
{ path: "cart/*", component: () => import("microapps/cart") },
{ path: "user/*", component: () => import("microapps/user") },
],
},
];
// Micro App Loading Strategy
const loadMicroApp = (appName) => {
const appLoader = {
products: () => loadVueApp("products"),
cart: () => loadReactApp("cart"),
user: () => loadAngularApp("user"),
};
return appLoader[appName]();
};2. Module Federation â
Webpack 5's Module Federation feature allows applications to share dependencies at runtime:
// Main App webpack.config.js
const ModuleFederationPlugin = require("@module-federation/webpack");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "main_app",
remotes: {
products_app: "products_app@http://localhost:3001/remoteEntry.js",
cart_app: "cart_app@http://localhost:3002/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
vue: { singleton: true, requiredVersion: "^3.0.0" },
},
}),
],
};
// Micro App webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "products_app",
filename: "remoteEntry.js",
exposes: {
"./ProductsList": "./src/components/ProductsList",
"./ProductDetail": "./src/components/ProductDetail",
},
shared: {
vue: { singleton: true, requiredVersion: "^3.0.0" },
"vue-router": { singleton: true, requiredVersion: "^4.0.0" },
},
}),
],
};3. qiankun Framework â
A microfrontend framework based on single-spa:
// Main App Registering Micro Apps
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "products", // Sub-app name
entry: "//localhost:7101", // Sub-app entry
container: "#products-container", // Sub-app mount node
activeRule: "/products", // Activation rule
},
{
name: "cart",
entry: "//localhost:7102",
container: "#cart-container",
activeRule: "/cart",
},
{
name: "user",
entry: "//localhost:7103",
container: "#user-container",
activeRule: "/user",
},
]);
// Start Main App
start();4. Web Components Approach â
Implementing microfrontends using native Web Components:
// Products Micro App - Define Web Component
class ProductCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<style>
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 8px;
}
</style>
<div class="product-card">
<h3>${this.getAttribute("title")}</h3>
<p>${this.getAttribute("price")}</p>
</div>
`;
}
}
customElements.define("product-card", ProductCard);
// Main App Using Web Component
const MainApp = () => {
return (
<div>
<h1>E-commerce Platform</h1>
{/* Using Web Components developed with different frameworks */}
<product-card
title="iPhone 15"
price="$999"
onclick={() => console.log("Product clicked")}
/>
{/* React Component */}
<ReactProductCard title="MacBook Pro" price="$1999" />
</div>
);
};Core Challenges and Solutions for Microfrontends â
1. Application Isolation â
Each microfrontend needs an independent runtime environment to avoid style and JavaScript conflicts:
// CSS Isolation Strategy
const createIsolatedStyles = (appName) => {
const styleSheet = new CSSStyleSheet();
// Add specific prefix for each component
const prefixCSS = `
.${appName}__button {
background-color: #1890ff;
border: none;
color: white;
padding: 8px 16px;
}
.${appName}__container {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
}
`;
styleSheet.replaceSync(prefixCSS);
return styleSheet;
};
// JavaScript Sandbox Isolation
const createSandbox = (appCode) => {
const sandbox = document.createElement("iframe");
sandbox.srcdoc = `
<!DOCTYPE html>
<html>
<head>
<style>${getAppStyles("micro-app")}</style>
</head>
<body>
<script>${appCode}</script>
</body>
</html>
`;
document.body.appendChild(sandbox);
return sandbox;
};2. State Management â
Microfrontends need effective state sharing mechanisms:
// Global State Manager
class GlobalStateManager {
constructor() {
this.state = {
user: null,
cart: [],
theme: "light",
};
this.listeners = new Map();
}
setState(updates) {
const oldState = { ...this.state };
this.state = { ...this.state, ...updates };
// Notify all subscribed micro apps
this.listeners.forEach((listener, key) => {
if (updates[key] !== undefined) {
listener(this.state[key], oldState[key]);
}
});
}
subscribe(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(callback);
// Return current value
return this.state[key];
}
unsubscribe(key, callback) {
const listeners = this.listeners.get(key);
if (listeners) {
listeners.delete(callback);
}
}
}
// Usage in Micro Apps
const globalState = new GlobalStateManager();
// React Micro App
function useGlobalState(key) {
const [state, setState] = useState(globalState.subscribe(key, setState));
useEffect(() => {
return () => globalState.unsubscribe(key, setState);
}, [key]);
return [state, (newState) => globalState.setState({ [key]: newState })];
}
// Vue Micro App
export default {
data() {
return {
userName: globalState.subscribe("user", (user) => {
this.userName = user?.name || "";
}),
};
},
};3. Route Coordination â
Microfrontends need unified route management to avoid conflicts:
// Main App Route Manager
class MicroRouter {
constructor() {
this.routes = new Map();
this.currentApp = null;
}
registerRoute(path, app) {
this.routes.set(path, app);
}
navigate(path) {
const [appName] = this.findAppForPath(path);
if (appName !== this.currentApp) {
this.switchApp(appName, path);
} else {
this.updateAppPath(appName, path);
}
}
findAppForPath(path) {
for (const [routePath, appName] of this.routes) {
if (path.startsWith(routePath)) {
return appName;
}
}
return null;
}
async switchApp(appName, path) {
// Unmount current app
if (this.currentApp) {
await this.unmountApp(this.currentApp);
}
// Load new app
await this.loadApp(appName);
// Update route
this.updateAppPath(appName, path);
this.currentApp = appName;
}
}
// Usage Example
const router = new MicroRouter();
router.registerRoute("/products", "products-app");
router.registerRoute("/cart", "cart-app");
router.registerRoute("/user", "user-app");
// Handle browser route changes
window.addEventListener("popstate", (event) => {
router.navigate(window.location.pathname);
});4. Performance Optimization â
Microfrontend architecture requires special attention to performance optimization:
// Preload Strategy
class MicroAppLoader {
constructor() {
this.loadedApps = new Set();
this.loadingApps = new Set();
}
async preloadApp(appName) {
if (this.loadedApps.has(appName) || this.loadingApps.has(appName)) {
return;
}
this.loadingApps.add(appName);
try {
// Preload app code
await this.loadAppCode(appName);
// Preload app data
await this.preloadAppData(appName);
this.loadedApps.add(appName);
} finally {
this.loadingApps.delete(appName);
}
}
async loadAppCode(appName) {
const appConfig = this.getAppConfig(appName);
// Dynamically import app code
const module = await import(appConfig.entry);
return module;
}
async preloadAppData(appName) {
// Preload static data needed by the app
const cacheKey = `app-data-${appName}`;
if (!sessionStorage.getItem(cacheKey)) {
const data = await fetch(`/api/apps/${appName}/data`).then((res) =>
res.json()
);
sessionStorage.setItem(cacheKey, JSON.stringify(data));
}
}
}
// Lazy Loading Implementation
const LazyMicroApp = ({ appName, ...props }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [app, setApp] = useState(null);
useEffect(() => {
const loadApp = async () => {
const module = await import(`./microapps/${appName}`);
const App = module.default;
setApp(() => <App {...props} />);
setIsLoaded(true);
};
// Preload during idle time
if ("requestIdleCallback" in window) {
requestIdleCallback(loadApp);
} else {
setTimeout(loadApp, 100);
}
}, [appName]);
if (!isLoaded) {
return <div>Loading...</div>;
}
return app;
};Practical Application Scenarios for Microfrontends â
1. Large Enterprise Platforms â
// E-commerce Platform Microfrontend Architecture
const ECommercePlatform = () => {
return (
<div className="ecommerce-platform">
{/* Common Header - Managed by Main App */}
<Header user={globalState.user} />
<div className="main-content">
{/* Product Display - Vue Micro App */}
<MicroAppContainer
name="products"
entry="/products/"
container="#products-app"
/>
{/* Shopping Cart - React Micro App */}
<MicroAppContainer name="cart" entry="/cart/" container="#cart-app" />
{/* Search - Angular Micro App */}
<MicroAppContainer
name="search"
entry="/search/"
container="#search-app"
/>
</div>
{/* Common Footer - Managed by Main App */}
<Footer />
</div>
);
};2. Admin Dashboard Systems â
// Admin Dashboard Microfrontend Architecture
const AdminDashboard = () => {
const [currentModule, setCurrentModule] = useState("dashboard");
const modules = [
{ name: "dashboard", icon: "đ", title: "Dashboard" },
{ name: "users", icon: "đĨ", title: "User Management" },
{ name: "products", icon: "đĻ", title: "Product Management" },
{ name: "orders", icon: "đ", title: "Order Management" },
{ name: "settings", icon: "âī¸", title: "System Settings" },
];
return (
<div className="admin-dashboard">
<Sidebar
modules={modules}
current={currentModule}
onSelect={setCurrentModule}
/>
<div className="content-area">
<MicroAppContainer
name={currentModule}
entry={`/admin/${currentModule}/`}
container={`#${currentModule}-app`}
/>
</div>
</div>
);
};Microfrontend Best Practices â
1. Design Principles â
Independence: Each microfrontend should be able to run independently
// Independent Running Check
const validateMicroApp = (app) => {
const checks = [
app.canMountIndependently,
app.hasIsolatedStyles,
app.hasIsolatedState,
app.canHandleErrorsIndependently,
];
return checks.every((check) => check);
};
// Micro App Independence Configuration
const microAppConfig = {
name: "user-profile",
independent: true,
dependencies: {
shared: ["react", "react-dom"], // Shared dependencies
isolated: ["axios", "lodash"], // Isolated dependencies
},
features: {
offlineSupport: true,
errorBoundary: true,
lazyLoading: true,
},
};Communication: Microfrontends need effective communication mechanisms
// Event Bus Communication
class MicroAppEventBus {
constructor() {
this.events = new Map();
}
emit(eventName, data) {
const handlers = this.events.get(eventName) || [];
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Event handling error: ${eventName}`, error);
}
});
}
on(eventName, handler) {
if (!this.events.has(eventName)) {
this.events.set(eventName, []);
}
this.events.get(eventName).push(handler);
}
off(eventName, handler) {
const handlers = this.events.get(eventName);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
}
// Cross-App Communication Example
const eventBus = new MicroAppEventBus();
// Products App Emits Event
const emitAddToCartEvent = (product) => {
eventBus.emit("add-to-cart", {
productId: product.id,
productName: product.name,
price: product.price,
timestamp: Date.now(),
});
};
// Cart App Listens to Event
const listenToCartEvents = () => {
eventBus.on("add-to-cart", (cartItem) => {
console.log("Product added to cart:", cartItem);
// Update cart state
updateCartState(cartItem);
});
};2. Development Workflow â
# Microfrontend Development Workflow
stages:
- name: "Local Development"
description: "Each team develops micro apps independently"
tools:
- "Independent development server"
- "Hot module reload"
- "Local state management"
- name: "Integration Testing"
description: "Integrate all micro apps in local environment"
tools:
- "Local proxy server"
- "Module Federation development environment"
- "Cross-app communication testing"
- name: "Pre-release Testing"
description: "Test integration in production-like environment"
tools:
- "Pre-release environment"
- "End-to-end testing"
- "Performance monitoring"
- name: "Production Release"
description: "Independently release each micro app"
tools:
- "CI/CD pipeline"
- "Automated testing"
- "Progressive deployment"
# Team Collaboration Standards
collaboration:
code_owners:
"products-app": "frontend-team-A"
"cart-app": "frontend-team-B"
"user-app": "frontend-team-C"
version_management:
strategy: "semantic_versioning"
compatibility_check: "api_compatibility"
deployment_strategy:
approach: "independent_deployment"
rollback_capability: "per_app_rollback"Summary â
Microfrontend architecture is an effective solution to address the complexity issues of large frontend applications. It provides a scalable solution for enterprise-level applications through the following core values:
Core Advantages:
- Technology Stack Agnosticism: Different teams can choose the most suitable technology stack
- Independent Development and Deployment: Improves development efficiency and deployment flexibility
- Progressive Refactoring: Supports gradual migration and upgrade of legacy systems
- Team Autonomy: Reduces code conflicts and dependencies between teams
- Maintainability: Reduces application complexity and improves code quality
Applicable Scenarios:
- Large enterprise application platforms
- Frontend projects requiring multi-team collaboration
- Progressive refactoring of legacy systems
- Technology stack upgrades or migrations
- Applications with high independence requirements for different business modules
Implementation Key Points:
- Choose appropriate microfrontend approach (routing distribution, module federation, qiankun, etc.)
- Establish comprehensive application isolation mechanisms
- Design efficient communication strategies between micro apps
- Establish unified development workflow and standards
- Consider performance optimization and user experience
Microfrontends are not a silver bullet and require choosing the appropriate implementation approach based on specific business scenarios, team size, and technical requirements. Correctly implementing microfrontend architecture can significantly improve the maintainability, scalability, and team development efficiency of large frontend applications.