Micro Frontend #
Micro frontend adalah ekstensi dari prinsip microservices ke sisi frontend — memecah aplikasi web monolitik menjadi bagian-bagian yang lebih kecil, independen, dan bisa di-deploy secara terpisah oleh tim yang berbeda. Alih-alih satu codebase React/Vue/Angular besar yang di-maintain oleh seluruh tim frontend, setiap tim memiliki dan men-deploy bagian UI mereka sendiri.
Motivasi utamanya sama dengan microservices di backend: ketika organisasi tumbuh dan jumlah tim bertambah, koordinasi untuk mengubah dan men-deploy satu codebase besar menjadi bottleneck. Tim Checkout tidak bisa release fitur mereka tanpa menunggu giliran deploy bersama tim Product, tim Search, dan tim Account.
Tapi seperti microservices, micro frontend bukan solusi untuk semua masalah. Ia memperkenalkan kompleksitas yang signifikan — duplicated dependencies, inkonsistensi UI, debugging yang lebih sulit, dan overhead koordinasi yang berbeda. Memahami trade-off ini sebelum memilih arsitektur ini adalah kunci.
Motivasi: Kapan Micro Frontend Relevan #
Masalah yang diselesaikan micro frontend:
Masalah 1: Deployment coupling
Tim A ingin release fitur baru, tapi harus menunggu Tim B selesai
karena semua ada di satu codebase yang di-deploy bersama.
→ Micro frontend: setiap tim deploy kapan saja secara independen
Masalah 2: Technology lock-in
Seluruh frontend harus pakai satu framework yang sama
Tim baru tidak bisa coba teknologi yang lebih cocok untuk use case mereka.
→ Micro frontend: setiap tim bisa pilih stack yang tepat (dalam batas wajar)
Masalah 3: Codebase yang terlalu besar untuk satu tim
Satu PR butuh review dari banyak tim yang tidak familiar dengan area tersebut
Build time yang terus bertambah
→ Micro frontend: setiap tim punya codebase yang lebih kecil dan bisa di-manage
Tapi ingat: micro frontend adalah solusi untuk masalah ORGANISASI, bukan masalah teknis.
Jika tim masih kecil, satu codebase yang terstruktur baik jauh lebih sederhana.
graph TB
subgraph Shell["Shell Application (Host)"]
Nav[Navigation MFE\nTim Platform]
Content[Content Area]
end
subgraph MFEs["Micro Frontends (Remotes)"]
Product[Product MFE\nTim Catalog]
Cart[Cart MFE\nTim Commerce]
Account[Account MFE\nTim User]
Search[Search MFE\nTim Discovery]
end
Nav --> Content
Content --> Product
Content --> Cart
Content --> Account
Content --> Search
style Shell fill:#e8f4f8
style Product fill:#e8f8e8
style Cart fill:#e8f8e8
style Account fill:#e8f8e8
style Search fill:#e8f8e8Empat Pendekatan Implementasi #
1. Build-time Integration (npm packages) #
Setiap micro frontend di-publish sebagai npm package. Shell menginstall semua dan build bersama.
// package.json di shell/host application
{
"dependencies": {
"@company/product-mfe": "^2.1.0",
"@company/cart-mfe": "^1.5.0",
"@company/account-mfe": "^3.0.0"
}
}
// Di shell application
import ProductMFE from '@company/product-mfe'
import CartMFE from '@company/cart-mfe'
function App() {
return (
<div>
<ProductMFE />
<CartMFE />
</div>
)
}
Build-time integration:
✓ Keuntungan:
→ Paling sederhana secara teknis
→ Type safety penuh antar MFE
→ Build optimization (tree shaking, code splitting) bisa optimal
→ Debugging lebih mudah (semua dalam satu build)
✗ Kekurangan:
→ Tidak benar-benar independen: shell harus di-rebuild jika ada update MFE
→ Bukan deployment yang benar-benar terpisah
→ Masih ada coupling di dependency version
→ Tidak menyelesaikan masalah koordinasi deployment
Kapan cocok:
→ Tim yang ingin manfaat code separation tapi tidak butuh independent deployment
→ Sebagai langkah awal sebelum migrasi ke runtime integration
2. Runtime Integration via iFrame #
MFE di-load dalam iframe terpisah.
<!-- Shell application -->
<html>
<body>
<nav id="nav-mfe"></nav>
<!-- Product MFE dalam iframe -->
<iframe
src="https://product-mfe.company.com/products"
style="width: 100%; border: none;"
sandbox="allow-scripts allow-same-origin"
title="Product Catalog"
></iframe>
<!-- Cart MFE dalam iframe -->
<iframe
src="https://cart-mfe.company.com/cart"
style="width: 300px; border: none;"
title="Shopping Cart"
></iframe>
</body>
</html>
// Komunikasi antar iframe via postMessage
// Di dalam iframe Product MFE
function addToCart(product) {
window.parent.postMessage(
{ type: 'ADD_TO_CART', product },
'https://shell.company.com' // specify origin untuk keamanan!
)
}
// Di shell — mendengarkan pesan dari iframe
window.addEventListener('message', (event) => {
// SELALU validasi origin sebelum memproses
if (event.origin !== 'https://product-mfe.company.com') return
if (event.data.type === 'ADD_TO_CART') {
updateCartState(event.data.product)
// Forward ke Cart MFE
document.querySelector('iframe[title="Shopping Cart"]')
.contentWindow
.postMessage({ type: 'CART_UPDATED', ... }, 'https://cart-mfe.company.com')
}
})
iFrame integration:
✓ Keuntungan:
→ Isolasi penuh: CSS tidak bisa bocor, JS tidak bisa konflik
→ Security sandbox yang kuat
→ Benar-benar independen: setiap MFE URL sendiri, deploy sendiri
✗ Kekurangan:
→ UX terbatas: scroll, modal, tooltip tidak bisa keluar dari iframe boundary
→ SEO sangat buruk: search engine sulit mengindex konten dalam iframe
→ Komunikasi antar MFE hanya via postMessage yang verbose
→ Performance: setiap iframe adalah browsing context baru (JS engine baru)
Kapan cocok:
→ Third-party widget yang perlu isolasi ketat (payment form, chat widget)
→ Ketika isolasi security lebih penting dari UX
→ Bukan untuk sebagian besar aplikasi consumer-facing
3. Module Federation (Webpack 5) #
Module Federation adalah pendekatan paling populer untuk runtime integration. Bundle JavaScript di-load secara dinamis pada runtime dari server yang berbeda.
// webpack.config.js — Product MFE (Remote)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'productMFE', // nama unik untuk MFE ini
filename: 'remoteEntry.js', // file manifest yang di-load oleh shell
exposes: {
// Komponen yang di-expose ke shell dan MFE lain
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
shared: {
// Deklarasikan dependency yang di-share
// Mencegah React di-load dua kali (satu dari shell, satu dari MFE)
react: {
singleton: true, // hanya satu instance
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
}),
],
}
// webpack.config.js — Shell Application (Host)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// Daftar MFE yang akan di-load secara remote
productMFE: 'productMFE@https://product-mfe.company.com/remoteEntry.js',
cartMFE: 'cartMFE@https://cart-mfe.company.com/remoteEntry.js',
accountMFE: 'accountMFE@https://account-mfe.company.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}
// App.jsx — Shell menggunakan komponen dari remote MFE
import React, { Suspense, lazy } from 'react'
// Lazy load komponen dari remote MFE
// Di-download dari server product-mfe saat dibutuhkan
const ProductList = lazy(() => import('productMFE/ProductList'))
const CartWidget = lazy(() => import('cartMFE/CartWidget'))
function App() {
return (
<div className="app">
<Suspense fallback={<div>Loading products...</div>}>
<ProductList onAddToCart={handleAddToCart} />
</Suspense>
<Suspense fallback={<div>Loading cart...</div>}>
<CartWidget />
</Suspense>
</div>
)
}
Module Federation:
✓ Keuntungan:
→ Independent deployment yang sesungguhnya
→ Shared dependencies (React tidak di-load dua kali)
→ UX yang seamless (tidak ada iframe boundary)
→ Lazy loading otomatis
✗ Kekurangan:
→ Kompleksitas konfigurasi webpack yang tinggi
→ Versi dependency harus kompatibel antar MFE
→ Runtime error jika remote MFE tidak tersedia
→ Debugging lebih sulit
Kapan cocok:
→ Tim besar yang butuh deployment independen sesungguhnya
→ Aplikasi React/Vue/Angular yang sudah ada dan ingin dipecah
→ Ketika UX yang seamless adalah prioritas
4. Web Components #
Web Components adalah standar browser yang memungkinkan pembuatan custom HTML element yang bisa digunakan di framework apapun.
// product-mfe/src/ProductWidget.js
class ProductWidget extends HTMLElement {
constructor() {
super()
// Shadow DOM untuk CSS isolation
this.attachShadow({ mode: 'open' })
}
// Observed attributes — seperti props di React
static get observedAttributes() {
return ['product-id', 'show-price']
}
connectedCallback() {
// Dipanggil saat element masuk ke DOM
this.render()
this.loadProduct(this.getAttribute('product-id'))
}
attributeChangedCallback(name, oldValue, newValue) {
// Dipanggil saat attribute berubah
if (name === 'product-id' && oldValue !== newValue) {
this.loadProduct(newValue)
}
}
async loadProduct(productId) {
const product = await fetchProduct(productId)
this.render(product)
}
render(product = null) {
this.shadowRoot.innerHTML = `
<style>
/* CSS terisolasi di dalam shadow DOM */
:host { display: block; padding: 16px; }
.product-name { font-size: 1.2em; font-weight: bold; }
.product-price { color: #e00; }
</style>
<div class="product-widget">
${product
? `<div class="product-name">${product.name}</div>
<div class="product-price">Rp ${product.price.toLocaleString()}</div>`
: '<div>Loading...</div>'
}
</div>
`
}
// Custom event untuk komunikasi ke luar
dispatchAddToCart(product) {
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { product },
bubbles: true, // event bisa di-capture oleh ancestor
composed: true // event menembus shadow DOM boundary
}))
}
}
// Register sebagai custom HTML element
customElements.define('product-widget', ProductWidget)
<!-- Penggunaan di shell atau framework manapun -->
<product-widget
product-id="123"
show-price="true">
</product-widget>
<!-- Event handler -->
<script>
document.querySelector('product-widget')
.addEventListener('add-to-cart', (event) => {
console.log('Add to cart:', event.detail.product)
})
</script>
Shared State: Tantangan Terbesar #
Salah satu tantangan teknis terbesar di micro frontend adalah bagaimana state di-share antar MFE yang independen.
// Pendekatan 1: Custom Events (paling sederhana, cocok untuk event sederhana)
// MFE A: publish event
function userLoggedIn(user) {
window.dispatchEvent(new CustomEvent('user:logged-in', {
detail: { user }
}))
}
// MFE B: subscribe ke event
window.addEventListener('user:logged-in', (event) => {
const { user } = event.detail
updateUIForUser(user)
})
// Pendekatan 2: Shared Store via Window (hati-hati — mudah abuse)
// Di shell application
window.__mfeStore = createGlobalStore({
user: null,
cart: { items: [] },
})
// Di MFE — akses shared store
const store = window.__mfeStore
store.subscribe('cart', (cart) => updateCartBadge(cart.items.length))
store.dispatch({ type: 'ADD_TO_CART', product })
// Pendekatan 3: URL sebagai shared state
// State yang perlu di-share bisa ada di URL
// MFE product: navigate ke /products/123
// MFE breadcrumb: baca dari URL untuk tampilkan breadcrumb yang benar
// Keuntungan: shareable URL, browser history, tidak ada coupling
// Pendekatan 4: Backend sebagai source of truth
// Daripada sync state antar MFE, semua baca dari API
// MFE Cart: POST /api/cart/add → response berisi cart terbaru
// MFE CartBadge: GET /api/cart/summary → tampilkan jumlah item
// Coupling di backend, bukan di frontend
Design System: Fondasi Konsistensi #
Micro frontend yang berbeda-beda team akan menghasilkan UI yang inkonsisten jika tidak ada design system yang dipakai bersama.
// @company/design-system — package npm yang di-share ke semua MFE
// Button.jsx
export function Button({ variant = 'primary', size = 'medium', children, ...props }) {
return (
<button
className={`btn btn--${variant} btn--${size}`}
{...props}
>
{children}
</button>
)
}
// Typography.jsx
export function Heading({ level = 1, children }) {
const Tag = `h${level}`
return <Tag className={`heading heading--${level}`}>{children}</Tag>
}
// Design tokens — CSS variables yang konsisten
export const tokens = {
colors: {
primary: '#0066cc',
danger: '#dc2626',
success: '#16a34a',
},
spacing: {
xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px'
},
typography: {
fontFamily: "'Inter', sans-serif",
fontSize: { sm: '14px', md: '16px', lg: '18px' }
}
}
Design system di micro frontend:
Apa yang perlu di-standardisasi:
→ Typography (font family, size, weight)
→ Color palette dan semantic colors (primary, danger, success)
→ Spacing scale (4px, 8px, 16px, 24px, ...)
→ Komponen UI yang umum (Button, Input, Modal, Toast)
→ Icons (satu set yang konsisten)
Cara distribusi:
→ npm package (@company/design-system) — paling umum
→ CSS variables di global stylesheet yang di-load shell
→ Storybook sebagai living documentation
Governance:
→ Siapa yang bertanggung jawab atas design system?
→ Bagaimana MFE bisa request komponen baru?
→ Bagaimana versioning dan breaking changes?
→ Dedicated platform team atau rotasi tim?
Kapan Micro Frontend vs Monolith Frontend #
Decision framework:
Tetap dengan monolith frontend jika:
✓ Tim masih kecil (< 5-10 engineer frontend)
✓ Tidak ada masalah deployment coupling yang nyata
✓ Product masih dalam tahap eksplorasi dan iterasi cepat
✓ UX seamless adalah prioritas utama
✓ Tim tidak memiliki pengalaman dengan distributed frontend
Pertimbangkan micro frontend jika:
→ 3+ tim yang sering konflik dalam satu codebase frontend
→ Tim yang berbeda butuh release schedule yang berbeda
→ Domain bisnis yang benar-benar berbeda (e-commerce + CMS + admin panel)
→ Sudah ada independent backend teams yang punya API stabil
→ Tim memiliki pengalaman atau siap investasi dalam kompleksitas ini
Alternatif sebelum micro frontend:
→ Modularisasi kode yang lebih baik (clear module boundaries)
→ Monorepo dengan packages per domain
→ Feature flags untuk independent feature release
→ Paket npm untuk shared UI tanpa full micro frontend
Anti-Pattern yang Harus Dihindari #
// ✗ Anti-pattern 1: MFE yang terlalu granular
// Button sebagai MFE, Form sebagai MFE, Table sebagai MFE
// Setiap page butuh puluhan request untuk load semua MFE kecil
// ✓ Solusi: MFE sesuai domain bisnis, bukan komponen UI
// ✗ Anti-pattern 2: Shared mutable state via window object
window.globalState = {
user: currentUser,
cart: cartItems
}
// Tidak ada kontrol, mudah terjadi race condition, sulit debug
// ✓ Solusi: event bus yang terdefinisi, URL sebagai state, atau backend
// ✗ Anti-pattern 3: Setiap MFE bawa versi React yang berbeda
// productMFE: React 16, cartMFE: React 17, accountMFE: React 18
// User download React 3 kali, performa buruk, React Context tidak bisa di-share
// ✓ Solusi: Module Federation dengan singleton dependency, align major version
// ✗ Anti-pattern 4: MFE yang tight-coupled ke routing shell
// Cart MFE langsung akses window.history, mengasumsikan path tertentu
// Tidak bisa di-embed di konteks berbeda
// ✓ Solusi: MFE stateless terhadap routing, terima config via props/attributes
// ✗ Anti-pattern 5: Tidak ada error boundary antar MFE
// Satu MFE crash → seluruh halaman crash
// ✓ Solusi: setiap MFE dibungkus error boundary, fallback UI jika MFE gagal load
// Di shell:
function MFEWrapper({ children, name }) {
return (
<ErrorBoundary fallback={<div>Gagal memuat {name}</div>}>
<Suspense fallback={<div>Memuat {name}...</div>}>
{children}
</Suspense>
</ErrorBoundary>
)
}
Checklist Micro Frontend #
ARSITEKTUR:
□ Setiap MFE sesuai dengan domain bisnis yang jelas (bukan komponen UI)
□ MFE bisa di-deploy secara independen tanpa koordinasi
□ Setiap MFE punya tim yang memilikinya (clear ownership)
□ MFE tidak tightly coupled ke routing atau state shell
INTEGRASI:
□ Metode integrasi dipilih berdasarkan kebutuhan (iframe/Module Fed/Web Components)
□ Shared dependencies di-declare sebagai singleton (mencegah duplicate React)
□ Error boundary ada di setiap MFE untuk mencegah crash yang menyebar
□ Loading state ada untuk setiap MFE yang di-load secara async
STATE MANAGEMENT:
□ Contract komunikasi antar MFE terdefinisi dengan jelas
□ Tidak ada shared mutable global state yang tidak terkontrol
□ Event contract terdokumentasi (nama event, payload schema)
DESIGN SYSTEM:
□ Design system tersedia sebagai shared package
□ Semua MFE menggunakan design system yang sama untuk komponen umum
□ Design tokens (colors, spacing, typography) konsisten
PERFORMANCE:
□ Bundle size setiap MFE dimonitor dan ada budget
□ Shared dependencies tidak di-duplicate (Module Federation shared config)
□ Lazy loading dikonfigurasi untuk MFE yang tidak dibutuhkan saat initial load
□ Prefetching untuk MFE yang kemungkinan besar dibutuhkan berikutnya
OPERASIONAL:
□ Setiap MFE punya CI/CD pipeline sendiri
□ Versioning strategy yang jelas (semver atau immutable URL dengan content hash)
□ Rollback bisa dilakukan per MFE tanpa deploy ulang shell
□ Monitoring per MFE (error rate, load time, availability)
Ringkasan #
- Micro frontend menyelesaikan masalah organisasi, bukan masalah teknis — jika bottleneck adalah koordinasi tim yang besar untuk deploy bersama, micro frontend relevan. Jika masalahnya teknis (performa, code quality), ada solusi yang lebih sederhana.
- Mulai dengan monolith yang terstruktur baik — seperti microservices di backend, monolith yang terstruktur dengan baik lebih mudah di-manage dan bisa di-evolusi menjadi micro frontend ketika kebutuhan nyata muncul.
- Module Federation adalah pendekatan terpopuler untuk runtime integration — memungkinkan independent deployment dengan UX yang seamless dan shared dependencies. Tapi kompleksitas konfigurasi webpack perlu diperhitungkan.
- iFrame cocok untuk widget yang butuh isolasi ketat — payment form, chat widget — bukan untuk aplikasi consumer-facing secara umum karena keterbatasan UX dan SEO.
- Web Components memberikan interoperabilitas antar framework — komponen yang ditulis sebagai Web Component bisa digunakan di React, Vue, Angular, atau vanilla JS tanpa binding khusus.
- Shared dependencies harus di-declare sebagai singleton — React yang di-load dua kali menyebabkan bug yang sulit di-debug dan performa yang buruk. Module Federation
singleton: truemenyelesaikan ini.- Design system adalah fondasi konsistensi — tanpa design system yang dipakai bersama, UI akan inkonsisten antar MFE dari tim yang berbeda. Investasi ini tidak bisa ditunda.
- Error boundary antar MFE adalah wajib — satu MFE yang crash tidak boleh menghancurkan seluruh halaman. Setiap MFE harus dibungkus error boundary dengan fallback UI.
- State management antar MFE perlu kontrak yang jelas — custom events, URL sebagai state, atau backend sebagai single source of truth lebih baik dari shared mutable global object.
- Kompleksitas operasional meningkat signifikan — lebih banyak deployment unit, lebih banyak yang perlu dimonitor, dependency matrix yang lebih kompleks. Pastikan tim siap sebelum memilih arsitektur ini.