Micro-frontends: Quando e Por Que Adotar em Projetos Escaláveis
Em 2025, com aplicações web cada vez mais complexas, a arquitetura de micro-frontends tornou-se uma solução madura para escalar tanto o desenvolvimento quanto a manutenção de grandes aplicações. Vamos explorar quando e como implementar essa arquitetura de forma eficiente.
Fundamentos de Micro-frontends
1. Conceitos Básicos
// Exemplo de configuração de Module Federation
// webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
marketing: 'marketing@http://localhost:8081/remoteEntry.js',
dashboard: 'dashboard@http://localhost:8082/remoteEntry.js',
auth: 'auth@http://localhost:8083/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};
2. Arquitetura Típica
graph TD
A[Container Application] -->|Load| B[Marketing MFE]
A -->|Load| C[Dashboard MFE]
A -->|Load| D[Auth MFE]
B -->|Use| E[Shared Components]
C -->|Use| E
D -->|Use| E
Quando Adotar Micro-frontends
1. Indicadores de Necessidade
interface ProjectAssessment {
teamSize: number;
numberOfFeatures: number;
deploymentFrequency: number;
technicalDebt: {
monolithComplexity: number;
buildTime: number;
testCoverage: number;
};
teamAutonomy: {
independent: boolean;
technicalDecisions: boolean;
deploymentControl: boolean;
};
}
function shouldAdoptMicroFrontends(
assessment: ProjectAssessment
): RecommendationReport {
const score = calculateAdoptionScore(assessment);
return {
recommendation: score > 0.7 ? 'Adopt' : 'Evaluate Further',
reasons: analyzeFactors(assessment),
risks: identifyRisks(assessment),
nextSteps: generateRoadmap(assessment),
};
}
2. Análise de Trade-offs
Aspecto | Benefícios | Desafios |
---|---|---|
Escalabilidade | Times independentes | Complexidade inicial |
Performance | Carregamento sob demanda | Overhead de runtime |
Manutenção | Código isolado | Consistência entre apps |
Deploy | Independente | Coordenação necessária |
Desenvolvimento | Autonomia | Curva de aprendizado |
Implementação com Module Federation
1. Container Application
// src/App.tsx
import { lazy, Suspense } from 'react';
import { Router, Route, Switch } from 'react-router-dom';
const MarketingApp = lazy(() => import('./remotes/MarketingApp'));
const DashboardApp = lazy(() => import('./remotes/DashboardApp'));
const AuthApp = lazy(() => import('./remotes/AuthApp'));
export default function App() {
return (
<Router>
<Suspense fallback={<LoadingSpinner />}>
<Switch>
<Route path="/auth" component={AuthApp} />
<Route path="/dashboard" component={DashboardApp} />
<Route path="/" component={MarketingApp} />
</Switch>
</Suspense>
</Router>
);
}
// src/bootstrap.tsx
import { createRoot } from 'react-dom/client';
import App from './App';
const mount = (el: HTMLElement) => {
const root = createRoot(el);
root.render(<App />);
};
const devRoot = document.querySelector('#root');
if (devRoot) {
mount(devRoot);
}
export { mount };
2. Remote Applications
// marketing/src/bootstrap.tsx
import { createRoot } from 'react-dom/client';
import { createMemoryHistory, createBrowserHistory } from 'history';
import App from './App';
interface MountOptions {
defaultHistory?: typeof createMemoryHistory;
initialPath?: string;
onNavigate?: (location: any) => void;
}
const mount = (
el: HTMLElement,
{
defaultHistory,
initialPath,
onNavigate,
}: MountOptions = {}
) => {
const history = defaultHistory?.({
initialEntries: [initialPath || '/'],
}) || createBrowserHistory();
if (onNavigate) {
history.listen(onNavigate);
}
const root = createRoot(el);
root.render(<App history={history} />);
return {
onParentNavigate({ pathname: nextPathname }) {
const { pathname } = history.location;
if (pathname !== nextPathname) {
history.push(nextPathname);
}
},
};
};
if (process.env.NODE_ENV === 'development') {
const devRoot = document.querySelector('#_marketing-dev-root');
if (devRoot) {
mount(devRoot, { defaultHistory: createBrowserHistory });
}
}
export { mount };
3. Compartilhamento de Estado
// shared/store/index.ts
import create from 'zustand';
interface SharedState {
user: User | null;
theme: 'light' | 'dark';
setUser: (user: User | null) => void;
setTheme: (theme: 'light' | 'dark') => void;
}
const useSharedStore = create<SharedState>((set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
}));
export { useSharedStore };
// shared/events/index.ts
type EventCallback = (data: any) => void;
class EventBus {
private events: Map<string, Set<EventCallback>>;
constructor() {
this.events = new Map();
}
subscribe(event: string, callback: EventCallback) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(callback);
return () => {
this.events.get(event)?.delete(callback);
};
}
publish(event: string, data: any) {
this.events.get(event)?.forEach((callback) => {
callback(data);
});
}
}
export const eventBus = new EventBus();
Padrões de Design
1. Composição de Aplicações
// container/src/components/MicroFrontend.tsx
interface MicroFrontendProps {
name: string;
host: string;
history?: History;
}
function MicroFrontend({ name, host, history }: MicroFrontendProps) {
useEffect(() => {
const scriptId = `micro-frontend-script-${name}`;
const renderMicroFrontend = () => {
// @ts-ignore
window[`render${name}`](
`${name}-container`,
history
);
};
if (document.getElementById(scriptId)) {
renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then((res) => res.json())
.then((manifest) => {
const script = document.createElement('script');
script.id = scriptId;
script.crossOrigin = '';
script.src = `${host}${manifest.files['main.js']}`;
script.onload = () => {
renderMicroFrontend();
};
document.head.appendChild(script);
});
return () => {
// @ts-ignore
window[`unmount${name}`]?.(`${name}-container`);
};
}, []);
return <div id={`${name}-container`} />;
}
2. Comunicação Entre Apps
// shared/communication/index.ts
type MessageHandler = (data: any) => void;
class MessageBus {
private handlers: Map<string, Set<MessageHandler>>;
constructor() {
this.handlers = new Map();
window.addEventListener('message', (event) => {
const { type, data } = event.data;
this.handlers.get(type)?.forEach((handler) => {
handler(data);
});
});
}
subscribe(type: string, handler: MessageHandler) {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set());
}
this.handlers.get(type)!.add(handler);
return () => {
this.handlers.get(type)?.delete(handler);
};
}
publish(type: string, data: any) {
window.postMessage({ type, data }, '*');
}
}
export const messageBus = new MessageBus();
Melhores Práticas
1. Performance
// shared/performance/index.ts
interface PerformanceMetrics {
ttfb: number;
fcp: number;
lcp: number;
fid: number;
cls: number;
}
class PerformanceMonitor {
private metrics: Map<string, PerformanceMetrics>;
constructor() {
this.metrics = new Map();
this.setupObservers();
}
private setupObservers() {
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
this.recordMetric(entry);
}
}).observe({ entryTypes: ['web-vital'] });
}
private recordMetric(entry: PerformanceEntry) {
const appName = this.getAppName();
if (!this.metrics.has(appName)) {
this.metrics.set(appName, {
ttfb: 0,
fcp: 0,
lcp: 0,
fid: 0,
cls: 0,
});
}
const metrics = this.metrics.get(appName)!;
metrics[entry.name as keyof PerformanceMetrics] = entry.value;
}
}
2. Testes
// shared/testing/index.ts
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
interface TestConfig {
route?: string;
initialState?: any;
mocks?: any[];
}
function renderWithRouter(
ui: React.ReactElement,
{
route = '/',
initialState = {},
mocks = [],
}: TestConfig = {}
) {
const history = createMemoryHistory({
initialEntries: [route],
});
return {
...render(ui),
history,
};
}
export { renderWithRouter };
Desafios e Soluções
1. Consistência Visual
// shared/design-system/index.ts
import { createTheme, ThemeProvider } from '@mui/material/styles';
const baseTheme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
},
},
},
},
});
export { baseTheme, ThemeProvider };
2. Monitoramento
// shared/monitoring/index.ts
interface MonitoringConfig {
appName: string;
version: string;
environment: string;
}
class ApplicationMonitor {
constructor(private config: MonitoringConfig) {
this.setupErrorBoundary();
this.setupPerformanceMonitoring();
this.setupUserMonitoring();
}
private setupErrorBoundary() {
window.addEventListener('error', (event) => {
this.logError({
type: 'uncaught',
error: event.error,
location: event.filename,
line: event.lineno,
column: event.colno,
});
});
}
private logError(error: any) {
console.error('[MFE Error]', {
...error,
app: this.config.appName,
version: this.config.version,
environment: this.config.environment,
timestamp: new Date().toISOString(),
});
}
}
Conclusão
A adoção de micro-frontends oferece benefícios significativos para projetos escaláveis:
- Escalabilidade Organizacional: Times autônomos e independentes
- Flexibilidade Técnica: Liberdade de escolha de tecnologias
- Deploy Independente: Redução de riscos e maior velocidade
- Manutenibilidade: Código mais organizado e isolado
- Evolução Gradual: Possibilidade de migração incremental
Próximos Passos
- Avalie a necessidade real do seu projeto
- Comece com um piloto em área não crítica
- Estabeleça padrões e governança
- Invista em automação e ferramentas
- Monitore e ajuste continuamente
Está considerando adotar micro-frontends em seu projeto? Compartilhe suas dúvidas e experiências nos comentários abaixo!