İçeriğe atla

Micro Frontend'ler: İleri Düzey Pattern'ler, Performans ve Production Dersleri

Micro frontend'lerde ileri düzey kalıplar: event sourcing ile paylaşımlı state, bundle bütçeleri, cross-app hata takibi, imzalı mesajlaşma ve strangler-fig geçişi.

Ayhan Sipahi Ayhan Sipahi

Micro Frontend Serisi Navigasyonu#

  • Kısım 1: Mimari temelleri ve uygulama türleri
  • Kısım 2: Module Federation, iletişim kalıpları ve entegrasyon stratejileri
  • Kısım 3 (Burada bulunuyorsunuz): İleri düzey kalıplar, performans optimizasyonu ve production hata ayıklama

Ön Koşullar: Kısım 1 temelleri ve Kısım 2 uygulama kalıpları.


Production’daki micro frontend’ler, mimari diyagramların nadiren gösterdiği zorlu problemler ortaya çıkarır: bağımsız olarak deploy edilen uygulamalar arasında ayrışan paylaşımlı state, yükleme sürelerini şişiren bundle tekrarları ve takım ile deployment sınırlarını aşan hatalar. Bu yükün büyük kısmını üç kalıp taşır. Event sourcing, bağımsız olarak deploy edilen uygulamalar arasında paylaşımlı state’i tutarlı tutar; performans bütçelemesi bundle tekrarlarını sınırlar; uygulama genelinde hata takibi ise dağıtık frontend hatalarını ayıklanabilir kılar.

İleri Düzey State Management Kalıpları#

Micro frontend mimarisinin en zor kısmı, bağımsız olarak deploy edilen uygulamalar arasında paylaşılan state’tir. Aşağıdaki iki kalıp, deploy’ları birbirine bağlamadan bu state’i tutarlı tutar.

1. Event Sourcing ile Dağıtık State Management#

// @company/shared-state - Micro frontend'ler için ileri düzey state management
interface StateEvent {
  id: string;
  type: string;
  payload: any;
  timestamp: number;
  version: number;
  microfrontend: string;
}

interface StateSnapshot {
  version: number;
  data: any;
  timestamp: number;
}

class DistributedStateManager {
  private eventStore: StateEvent[] = [];
  private snapshots: Map<string, StateSnapshot> = new Map();
  private subscribers: Map<string, Set<(state: any) => void>> = new Map();
  private maxEventHistory = 1000;
  private snapshotInterval = 100; // Her 100 event'te snapshot oluştur

  constructor(private persistenceAdapter?: PersistenceAdapter) {
    // Persistence katmanından başlangıç state'ini yükle
    this.loadInitialState();
  }

  // Sadece ekleme yapılan event store
  dispatch(event: Omit<StateEvent, 'id' | 'timestamp' | 'version'>) {
    const stateEvent: StateEvent = {
      ...event,
      id: generateId(),
      timestamp: Date.now(),
      version: this.eventStore.length + 1,
    };

    this.eventStore.push(stateEvent);

    // Performans için periyodik snapshot'lar oluştur
    if (stateEvent.version % this.snapshotInterval === 0) {
      this.createSnapshot(event.type.split(':')[0]); // event type'dan namespace
    }

    // Harici storage'a kaydet
    this.persistenceAdapter?.persistEvent(stateEvent);

    // Subscriber'ları bilgilendir
    this.notifySubscribers(event.type, this.getState(event.type.split(':')[0]));

    // Event geçmişi boyutunu koru
    if (this.eventStore.length > this.maxEventHistory) {
      this.eventStore = this.eventStore.slice(-this.maxEventHistory);
    }
  }

  getState(namespace: string): any {
    // Önce en son snapshot'ı kullanmaya çalış
    const snapshot = this.snapshots.get(namespace);
    const eventsAfterSnapshot = snapshot
      ? this.eventStore.filter(e => e.version > snapshot.version && e.type.startsWith(namespace))
      : this.eventStore.filter(e => e.type.startsWith(namespace));

    let state = snapshot?.data || {};

    // Snapshot'tan sonraki event'leri uygula
    eventsAfterSnapshot.forEach(event => {
      state = this.applyEvent(state, event);
    });

    return state;
  }

  subscribe(eventType: string, callback: (state: any) => void): () => void {
    if (!this.subscribers.has(eventType)) {
      this.subscribers.set(eventType, new Set());
    }

    this.subscribers.get(eventType)!.add(callback);

    // Mevcut state ile hemen çağır
    const namespace = eventType.split(':')[0];
    callback(this.getState(namespace));

    // Unsubscribe fonksiyonu döndür
    return () => {
      this.subscribers.get(eventType)?.delete(callback);
    };
  }

  private applyEvent(state: any, event: StateEvent): any {
    switch (event.type) {
      case 'user:login':
        return { ...state, user: event.payload, isAuthenticated: true };

      case 'user:logout':
        return { ...state, user: null, isAuthenticated: false };

      case 'cart:add-item':
        const items = state.items || [];
        return {
          ...state,
          items: [...items, event.payload],
          total: calculateTotal([...items, event.payload])
        };

      case 'cart:remove-item':
        const filteredItems = (state.items || []).filter(
          (item: any) => item.id !== event.payload.id
        );
        return {
          ...state,
          items: filteredItems,
          total: calculateTotal(filteredItems)
        };

      default:
        return state;
    }
  }

  private createSnapshot(namespace: string) {
    const state = this.getState(namespace);
    const snapshot: StateSnapshot = {
      version: this.eventStore.length,
      data: state,
      timestamp: Date.now(),
    };

    this.snapshots.set(namespace, snapshot);
    this.persistenceAdapter?.persistSnapshot(namespace, snapshot);
  }

  private notifySubscribers(eventType: string, state: any) {
    // Tam eşleşen aboneler ve "cart:*" gibi namespace joker abonelikleri
    const namespace = eventType.split(':')[0];
    const callbacks = new Set([
      ...(this.subscribers.get(eventType) ?? []),
      ...(this.subscribers.get(`${namespace}:*`) ?? []),
    ]);

    callbacks.forEach(callback => {
      try {
        callback(state);
      } catch (error) {
        console.error(`Error in state subscriber for ${eventType}:`, error);
      }
    });
  }

  private async loadInitialState() {
    if (this.persistenceAdapter) {
      try {
        const { events, snapshots } = await this.persistenceAdapter.loadInitialState();
        this.eventStore = events;
        snapshots.forEach((snapshot, namespace) => {
          this.snapshots.set(namespace, snapshot);
        });
      } catch (error) {
        console.error('Failed to load initial state:', error);
      }
    }
  }

  // Debug yardımcıları
  getEventHistory(namespace?: string): StateEvent[] {
    if (namespace) {
      return this.eventStore.filter(e => e.type.startsWith(namespace));
    }
    return [...this.eventStore];
  }

  replayEventsFrom(version: number): void {
    const eventsToReplay = this.eventStore.filter(e => e.version >= version);
    console.log(`${version} versiyonundan ${eventsToReplay.length} event'i yeniden oynatılıyor`);

    eventsToReplay.forEach(event => {
      console.log(`Yeniden oynatılıyor: ${event.type}`, event.payload);
    });
  }
}

// Persistence adapter arayüzü
interface PersistenceAdapter {
  persistEvent(event: StateEvent): Promise<void>;
  persistSnapshot(namespace: string, snapshot: StateSnapshot): Promise<void>;
  loadInitialState(): Promise<{
    events: StateEvent[];
    snapshots: Map<string, StateSnapshot>;
  }>;
}

// Daha kolay kullanım için React hook'ları
export const useDistributedState = <T>(namespace: string): [T, (event: any) => void] => {
  const [state, setState] = useState<T>({} as T);
  const stateManager = useContext(StateManagerContext);

  useEffect(() => {
    const unsubscribe = stateManager.subscribe(`${namespace}:*`, setState);
    return unsubscribe;
  }, [namespace, stateManager]);

  const dispatch = useCallback((event: any) => {
    stateManager.dispatch({
      type: `${namespace}:${event.type}`,
      payload: event.payload,
      microfrontend: event.source || 'unknown',
    });
  }, [namespace, stateManager]);

  return [state, dispatch];
};

2. Çakışma Çözümü ile Optimistic Update’ler#

// Micro frontend'ler için gelişmiş optimistic update kalıbı
interface OptimisticUpdate {
  id: string;
  type: string;
  payload: any;
  timestamp: number;
  microfrontend: string;
  status: 'pending' | 'confirmed' | 'failed';
}

class OptimisticStateManager {
  private pendingUpdates: Map<string, OptimisticUpdate> = new Map();
  private baseState: any = {};
  private stateManager: DistributedStateManager;

  constructor(stateManager: DistributedStateManager) {
    this.stateManager = stateManager;
  }

  optimisticDispatch(event: any): string {
    const updateId = generateId();
    const optimisticUpdate: OptimisticUpdate = {
      id: updateId,
      type: event.type,
      payload: event.payload,
      timestamp: Date.now(),
      microfrontend: event.source,
      status: 'pending',
    };

    this.pendingUpdates.set(updateId, optimisticUpdate);

    // Optimistic update'i hemen uygula
    this.applyOptimisticUpdate(optimisticUpdate);

    // Sunucuya gönder
    this.sendToServer(event)
      .then(() => {
        // Update'i onayla
        const update = this.pendingUpdates.get(updateId);
        if (update) {
          update.status = 'confirmed';
          // Onaylanmış state'i uygula
          this.stateManager.dispatch(event);
        }
      })
      .catch((error) => {
        // Optimistic update'i geri al
        const update = this.pendingUpdates.get(updateId);
        if (update) {
          update.status = 'failed';
          this.revertOptimisticUpdate(updateId);

          // Kullanıcıya hata göster
          this.showConflictResolution(error, event);
        }
      })
      .finally(() => {
        this.pendingUpdates.delete(updateId);
      });

    return updateId;
  }

  private applyOptimisticUpdate(update: OptimisticUpdate) {
    // Update'i UI'ya optimistic olarak uygula
    const event = {
      type: update.type,
      payload: { ...update.payload, _optimistic: true },
      microfrontend: update.microfrontend,
    };

    this.stateManager.dispatch(event);
  }

  private revertOptimisticUpdate(updateId: string) {
    const update = this.pendingUpdates.get(updateId);
    if (!update) return;

    // Geri alma event'ini gönder
    this.stateManager.dispatch({
      type: `${update.type}:revert`,
      payload: { originalPayload: update.payload },
      microfrontend: update.microfrontend,
    });
  }

  private async sendToServer(event: any): Promise<any> {
    const response = await fetch('/api/state/update', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event),
    });

    if (!response.ok) {
      throw new Error(`Sunucu update'i reddetti: ${response.statusText}`);
    }

    return response.json();
  }

  private showConflictResolution(error: Error, originalEvent: any) {
    // Çakışma çözümü için UI göster
    this.stateManager.dispatch({
      type: 'ui:show-conflict-resolution',
      payload: {
        error: error.message,
        originalEvent,
        timestamp: Date.now(),
      },
      microfrontend: 'system',
    });
  }
}

Performans Optimizasyon Stratejileri#

1. İleri Düzey Bundle Analizi ve Optimizasyon#

// webpack-bundle-analyzer-reporter.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

class MicroFrontendBundleReporter {
  constructor(options = {}) {
    this.options = {
      reportDir: './bundle-reports',
      threshold: 50000, // 50KB
      ...options,
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('MicroFrontendBundleReporter', (compilation, callback) => {
      const stats = compilation.getStats().toJson();
      const analysis = this.analyzeBundle(stats);

      // Rapor oluştur
      this.generateReport(analysis);

      // Büyük bundle'lar hakkında uyarı ver
      this.checkBundleSize(analysis);

      callback();
    });
  }

  analyzeBundle(stats) {
    const analysis = {
      totalSize: 0,
      sharedDependencies: {},
      duplicatedDependencies: [],
      largeDependencies: [],
      unusedExports: [],
    };

    stats.chunks.forEach(chunk => {
      chunk.modules.forEach(module => {
        const size = module.size || 0;
        analysis.totalSize += size;

        // Paylaşılan bağımlılıkları tanımla
        if (module.name && module.name.includes('node_modules')) {
          const dep = this.extractDependencyName(module.name);
          if (!analysis.sharedDependencies[dep]) {
            analysis.sharedDependencies[dep] = { size: 0, count: 0 };
          }
          analysis.sharedDependencies[dep].size += size;
          analysis.sharedDependencies[dep].count++;
        }

        // Büyük bağımlılıkları işaretle
        if (size > this.options.threshold) {
          analysis.largeDependencies.push({
            name: module.name,
            size,
            reasons: module.reasons || [],
          });
        }
      });
    });

    // Duplike bağımlılıkları bul
    Object.entries(analysis.sharedDependencies).forEach(([dep, info]) => {
      if (info.count > 1) {
        analysis.duplicatedDependencies.push({ name: dep, ...info });
      }
    });

    return analysis;
  }

  generateReport(analysis) {
    const report = {
      timestamp: new Date().toISOString(),
      microfrontend: process.env.MICRO_FRONTEND_NAME || 'unknown',
      ...analysis,
    };

    const fs = require('fs');
    const path = require('path');

    if (!fs.existsSync(this.options.reportDir)) {
      fs.mkdirSync(this.options.reportDir, { recursive: true });
    }

    const reportPath = path.join(
      this.options.reportDir,
      `bundle-report-${Date.now()}.json`
    );

    fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
    console.log(`Bundle raporu oluşturuldu: ${reportPath}`);
  }

  checkBundleSize(analysis) {
    const maxSize = 500000; // 500KB uyarı eşiği

    if (analysis.totalSize > maxSize) {
      console.warn(`[WARN] Bundle boyutu (${analysis.totalSize} byte) önerilen eşiği (${maxSize} byte) aşıyor`);
    }

    if (analysis.duplicatedDependencies.length > 0) {
      console.warn('[WARN] Duplike bağımlılıklar bulundu:');
      analysis.duplicatedDependencies.forEach(dep => {
        console.warn(`  - ${dep.name}: ${dep.size} byte (${dep.count} kopya)`);
      });
    }
  }

  extractDependencyName(moduleName) {
    const match = moduleName.match(/node_modules[\/\\](@[^\/\\]+[\/\\][^\/\\]+|[^\/\\]+)/);
    return match ? match[1] : moduleName;
  }
}

module.exports = MicroFrontendBundleReporter;

2. Performans İzleme ve Optimizasyon#

// @company/performance-monitor
interface PerformanceMetrics {
  microfrontend: string;
  loadTime: number;
  renderTime: number;
  bundleSize: number;
  memoryUsage: number;
  errorCount: number;
}

class MicroFrontendPerformanceMonitor {
  private metrics: Map<string, PerformanceMetrics> = new Map();
  private observers: PerformanceObserver[] = [];

  constructor(private reportingEndpoint?: string) {
    this.setupPerformanceObservers();
  }

  startMeasurement(microfrontendName: string) {
    const startTime = performance.now();

    return {
      recordLoadComplete: () => {
        const loadTime = performance.now() - startTime;
        this.updateMetrics(microfrontendName, { loadTime });
      },

      recordRenderComplete: () => {
        const renderTime = performance.now() - startTime;
        this.updateMetrics(microfrontendName, { renderTime });
      },

      recordError: () => {
        const current = this.metrics.get(microfrontendName);
        this.updateMetrics(microfrontendName, {
          errorCount: (current?.errorCount || 0) + 1
        });
      }
    };
  }

  private setupPerformanceObservers() {
    // Resource yüklemeyi izle
    const resourceObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.name.includes('remoteEntry.js')) {
          const microfrontendName = this.extractMicrofrontendName(entry.name);
          this.updateMetrics(microfrontendName, {
            bundleSize: entry.transferSize || 0,
          });
        }
      });
    });
    resourceObserver.observe({ entryTypes: ['resource'] });
    this.observers.push(resourceObserver);

    // Uzun task'ları izle
    const longTaskObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.duration > 16) { // 16ms'den uzun task'lar (60fps'de 1 frame)
          console.warn(`Uzun task algılandı: ${entry.duration}ms`);
          this.reportLongTask(entry);
        }
      });
    });
    longTaskObserver.observe({ entryTypes: ['longtask'] });
    this.observers.push(longTaskObserver);

    // Bellek kullanımını izle
    this.startMemoryMonitoring();
  }

  private startMemoryMonitoring() {
    setInterval(() => {
      if ('memory' in performance) {
        const memInfo = (performance as any).memory;
        this.metrics.forEach((_, microfrontendName) => {
          this.updateMetrics(microfrontendName, {
            memoryUsage: memInfo.usedJSHeapSize,
          });
        });
      }
    }, 10000); // Her 10 saniyede
  }

  private updateMetrics(microfrontendName: string, updates: Partial<PerformanceMetrics>) {
    const current = this.metrics.get(microfrontendName) || {
      microfrontend: microfrontendName,
      loadTime: 0,
      renderTime: 0,
      bundleSize: 0,
      memoryUsage: 0,
      errorCount: 0,
    };

    const updated = { ...current, ...updates };
    this.metrics.set(microfrontendName, updated);

    // İzleme servisine rapor et
    this.reportMetrics(updated);
  }

  private reportMetrics(metrics: PerformanceMetrics) {
    if (this.reportingEndpoint) {
      fetch(this.reportingEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(metrics),
      }).catch(error => {
        console.error('Metrik raporlama başarısız:', error);
      });
    }

    // Geliştirme ortamında console'a da logla
    if (process.env.NODE_ENV === 'development') {
      console.log(`[Performance] ${metrics.microfrontend}:`, metrics);
    }
  }

  private reportLongTask(entry: PerformanceEntry) {
    if (this.reportingEndpoint) {
      fetch(`${this.reportingEndpoint}/long-tasks`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          duration: entry.duration,
          startTime: entry.startTime,
          timestamp: Date.now(),
        }),
      }).catch(error => {
        console.error('Uzun task raporlama başarısız:', error);
      });
    }
  }

  getMetrics(microfrontendName?: string): PerformanceMetrics | Map<string, PerformanceMetrics> {
    if (microfrontendName) {
      return this.metrics.get(microfrontendName)!;
    }
    return new Map(this.metrics);
  }

  cleanup() {
    this.observers.forEach(observer => observer.disconnect());
  }

  private extractMicrofrontendName(url: string): string {
    const match = url.match(/\/\/([^.]+)\./);
    return match ? match[1] : 'unknown';
  }
}

// Performans izleme için React hook
export const usePerformanceMonitor = (microfrontendName: string) => {
  const monitor = useContext(PerformanceMonitorContext);

  useEffect(() => {
    const measurement = monitor.startMeasurement(microfrontendName);

    // Component mount olduğunda kaydet (render tamamlandı)
    measurement.recordRenderComplete();

    return () => {
      // Gerekirse temizlik
    };
  }, [microfrontendName, monitor]);

  const recordError = useCallback(() => {
    const measurement = monitor.startMeasurement(microfrontendName);
    measurement.recordError();
  }, [microfrontendName, monitor]);

  return { recordError };
};

Dağıtık Frontend’lerde Hata Ayıklama Kalıpları#

Event Bus Aboneliklerinden Doğan Bellek Sızıntıları#

Yalnızca birkaç saatlik kesintisiz kullanımdan sonra görünen sızıntı, paylaşımlı event bus’ın klasik arıza biçimidir. Uygulama yavaş yavaş ağırlaşır, sonunda bazı micro frontend’ler yanıt vermeyi bırakır. Nedeni genellikle, kendisini kaydeden component’ten daha uzun yaşayan ve tüm component scope’unu ayakta tutan bir aboneliktir:

// Bellek sızıntısına neden olan problemli kod
const ProductList: React.FC = () => {
  const eventBus = useContext(EventBusContext);
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    // Bu, tüm component'i referans olarak tutan bir closure oluşturuyordu
    const handler = (event: any) => {
      // Handler closure tüm component scope'unu yakalar
      setProducts(prevProducts => {
        // Eski state'i tutan karmaşık state güncellemeleri
        return updateProductsWithComplexLogic(prevProducts, event);
      });
    };

    eventBus.subscribe('cart:updated', handler);

    // Bazı kod yollarında erken return'ler nedeniyle unsubscribe hiç çağrılmıyordu
    return () => eventBus.unsubscribe('cart:updated', handler);
  }, []); // Eksik bağımlılıklar stale closure'lara neden oldu

  return <div className="product-list">{/* ... */}</div>;
};

Çözüm, sabit bir handler referansı ve her koşulda çalışan bir temizlik yolu:

// Uygun bellek yönetimi ile düzeltilmiş versiyon
const ProductList: React.FC = () => {
  const eventBus = useContext(EventBusContext);
  const [products, setProducts] = useState<Product[]>([]);

  // Gereksiz yeniden oluşturmaları önlemek için useCallback kullan
  const handleCartUpdate = useCallback((event: CartUpdateEvent) => {
    setProducts(prevProducts => {
      // Referans döngülerini önlemek için immutable güncellemeler kullan
      return prevProducts.map(product =>
        product.id === event.productId
          ? { ...product, inCart: event.inCart }
          : product
      );
    });
  }, []); // Fonksiyonel güncellemeler kullandığımız için bağımlılık gerekmez

  useEffect(() => {
    const unsubscribe = eventBus.subscribe('cart:updated', handleCartUpdate);

    // Her zaman temizliğin gerçekleşmesini sağla
    return () => {
      unsubscribe();
    };
  }, [eventBus, handleCartUpdate]);

  // Geliştirme ortamında bellek kullanımını izle
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      const interval = setInterval(() => {
        if ('memory' in performance) {
          const memInfo = (performance as any).memory;
          console.log(`ProductList bellek kullanımı: ${memInfo.usedJSHeapSize / 1024 / 1024} MB`);
        }
      }, 5000);

      return () => clearInterval(interval);
    }
  }, []);

  return (
    <div className="product-list">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

// Bellek sızıntısı tespit yardımcısı
class MemoryLeakDetector {
  private snapshots: any[] = [];
  private interval: NodeJS.Timeout;

  constructor(private intervalMs: number = 30000) {
    this.interval = setInterval(() => {
      this.takeSnapshot();
    }, intervalMs);
  }

  private takeSnapshot() {
    if ('memory' in performance) {
      const memInfo = (performance as any).memory;
      const snapshot = {
        timestamp: Date.now(),
        usedJSHeapSize: memInfo.usedJSHeapSize,
        totalJSHeapSize: memInfo.totalJSHeapSize,
        jsHeapSizeLimit: memInfo.jsHeapSizeLimit,
      };

      this.snapshots.push(snapshot);

      // Son 20 snapshot'ı tut
      if (this.snapshots.length > 20) {
        this.snapshots.shift();
      }

      this.analyzeMemoryTrend();
    }
  }

  private analyzeMemoryTrend() {
    if (this.snapshots.length < 5) return;

    const recent = this.snapshots.slice(-5);

    // Bellek büyüme hızını ve desenini hesapla
    let consecutiveIncreases = 0;
    let totalGrowth = 0;

    for (let i = 1; i < recent.length; i++) {
      const growth = recent[i].usedJSHeapSize - recent[i - 1].usedJSHeapSize;
      if (growth > 0) {
        consecutiveIncreases++;
        totalGrowth += growth;
      } else {
        consecutiveIncreases = 0; // Düşüşte sayacı sıfırla
      }
    }

    // Şüpheli desenleri tespit et
    const growthMB = totalGrowth / 1024 / 1024;
    const hasConsistentGrowth = consecutiveIncreases >= 3;
    const hasSignificantGrowth = growthMB > 5; // Pencere boyunca 5 MB

    if (hasConsistentGrowth && hasSignificantGrowth) {
      console.warn(`Potansiyel bellek sızıntısı algılandı. Büyüme: ${recent.length - 1} aralıkta ${growthMB.toFixed(2)} MB`);
      this.reportMemoryLeak(recent);
    }
  }

  private reportMemoryLeak(snapshots: any[]) {
    // İzleme servisine rapor et
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.track('Memory Leak Detected', {
        snapshots,
        userAgent: navigator.userAgent,
        timestamp: Date.now(),
      });
    }
  }

  cleanup() {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }
}

Aralıklı Cross-Origin Mesaj Kayıpları#

Farklı subdomain’lerde deploy edilen micro frontend’ler mesajları aralıklı olarak kaybeder. postMessage, hedef frame yüklüyse mesajı iletir; yüklü değilse sessizce hiçbir yere ulaşmaz. Ortada bir hata fırlatılmadığı için arıza rastgele görünür. Sınırlı sayıda yeniden deneme yapan bir kuyruk zamanlama bağımlılığını ortadan kaldırır; sıkı origin kontrolü de kanalın bir giriş noktasına dönüşmesini engeller:

// Çözüm: Sağlam cross-origin iletişim
class CrossOriginMessenger {
  private trustedOrigins: Set<string> = new Set();
  private messageQueue: Array<{ data: any; targetOrigin: string; retry: number }> = [];
  private maxRetries = 3;

  constructor(trustedOrigins: string[]) {
    trustedOrigins.forEach(origin => this.trustedOrigins.add(origin));
    this.setupMessageListener();
    this.startRetryProcessor();
  }

  private setupMessageListener() {
    window.addEventListener('message', (event) => {
      // Sıkı origin kontrolü
      if (!this.trustedOrigins.has(event.origin)) {
        console.warn(`Güvenilmeyen origin'den mesaj reddedildi: ${event.origin}`);
        return;
      }

      try {
        const message = JSON.parse(event.data);
        this.handleMessage(message, event.origin);
      } catch (error) {
        console.error('Cross-origin mesajı ayrıştırılamadı:', error);
      }
    });
  }

  sendMessage(data: any, targetOrigin: string, retries: number = 0): boolean {
    if (!this.trustedOrigins.has(targetOrigin)) {
      console.error(`Güvenilmeyen origin'e mesaj gönderme denemesi: ${targetOrigin}`);
      return false;
    }

    try {
      const serializedData = JSON.stringify({
        ...data,
        timestamp: Date.now(),
        sender: window.location.origin,
        messageId: generateId(),
      });

      // Hedef window'u bulmaya çalış
      const targetWindow = this.findTargetWindow(targetOrigin);

      if (targetWindow) {
        targetWindow.postMessage(serializedData, targetOrigin);
        return true;
      } else {
        // Daha sonra tekrar denemek için kuyruğa al
        this.messageQueue.push({ data, targetOrigin, retry: retries });
        return false;
      }
    } catch (error) {
      console.error('Cross-origin mesaj gönderme başarısız:', error);
      return false;
    }
  }

  private findTargetWindow(targetOrigin: string): Window | null {
    // Tüm iframe'leri kontrol et
    const iframes = document.querySelectorAll('iframe');
    for (const iframe of iframes) {
      try {
        if (iframe.src.startsWith(targetOrigin)) {
          return iframe.contentWindow;
        }
      } catch (error) {
        // Erişim reddedildi - muhtemelen cross-origin
        continue;
      }
    }

    // Parent window olup olmadığını kontrol et
    if (window.parent !== window) {
      try {
        if (document.referrer.startsWith(targetOrigin)) {
          return window.parent;
        }
      } catch (error) {
        // Erişim reddedildi
      }
    }

    return null;
  }

  private startRetryProcessor() {
    setInterval(() => {
      const toRetry = this.messageQueue.splice(0); // Tüm kuyruktaki mesajları al

      toRetry.forEach(({ data, targetOrigin, retry }) => {
        if (retry < this.maxRetries) {
          const success = this.sendMessage(data, targetOrigin, retry + 1);
          if (!success) {
            // Artan retry sayısı ile yeniden kuyruğa al
            this.messageQueue.push({ data, targetOrigin, retry: retry + 1 });
          }
        } else {
          console.error(`${this.maxRetries} denemeden sonra mesaj gönderilemedi:`, data);
        }
      });
    }, 1000); // Her saniye tekrar dene
  }

  private handleMessage(message: any, origin: string) {
    // Farklı mesaj türlerini ele al
    switch (message.type) {
      case 'state-update':
        this.handleStateUpdate(message.payload);
        break;
      case 'navigation':
        this.handleNavigation(message.payload);
        break;
      case 'error':
        this.handleError(message.payload);
        break;
      default:
        console.warn(`Bilinmeyen mesaj türü: ${message.type}`);
    }
  }

  private handleStateUpdate(payload: any) {
    // Paylaşılan state'i güncelle
    if (typeof window !== 'undefined' && (window as any).stateManager) {
      (window as any).stateManager.dispatch({
        type: payload.type,
        payload: payload.data,
        source: 'cross-origin',
      });
    }
  }

  private handleNavigation(payload: any) {
    // Navigasyon isteklerini ele al
    if (payload.path && typeof window !== 'undefined') {
      window.history.pushState({}, '', payload.path);
    }
  }

  private handleError(payload: any) {
    console.error('Cross-origin hata alındı:', payload);
    // İzleme servisine rapor et
  }
}

Micro Frontend Güvenliği#

1. Dinamik Yükleme için Content Security Policy (CSP)#

// Micro frontend uygulamaları için CSP header üretici
class MicroFrontendCSPGenerator {
  constructor(
    private allowedOrigins: string[],
    private isDevelopment: boolean = false
  ) {}

  generateCSP(): string {
    const directives = [];

    // Script kaynakları - kendi origin ve micro frontend origin'lerini izin ver
    const scriptSrc = [
      "'self'",
      ...this.allowedOrigins,
    ];

    if (this.isDevelopment) {
      scriptSrc.push("'unsafe-eval'"); // Geliştirme araçları için
    }

    directives.push(`script-src ${scriptSrc.join(' ')}`);

    // API çağrıları için connect kaynakları
    const connectSrc = [
      "'self'",
      ...this.allowedOrigins,
      // API endpoint'leri ekle
      'https://api.company.com',
    ];

    directives.push(`connect-src ${connectSrc.join(' ')}`);

    // iframe tabanlı micro frontend'ler için frame kaynakları
    const frameSrc = [
      "'self'",
      ...this.allowedOrigins,
    ];

    directives.push(`frame-src ${frameSrc.join(' ')}`);

    // Image kaynakları
    directives.push(`img-src 'self' data: https:`);

    // Style kaynakları - unsafe-inline yerine nonce tabanlı yaklaşım
    const styleSrc = [
      "'self'",
      "'nonce-{NONCE_VALUE}'", // Production'da gerçek nonce ile değiştir
      ...this.allowedOrigins,
    ];

    // Sadece development'ta hot reloading için unsafe-inline'a izin ver
    if (this.isDevelopment) {
      styleSrc.push("'unsafe-inline'");
    }

    directives.push(`style-src ${styleSrc.join(' ')}`);

    return directives.join('; ');
  }

  // Express.js için middleware
  middleware() {
    return (req: any, res: any, next: any) => {
      res.setHeader('Content-Security-Policy', this.generateCSP());
      next();
    };
  }
}

// Kullanım
const cspGenerator = new MicroFrontendCSPGenerator([
  'https://products.company.com',
  'https://cart.company.com',
  'https://user.company.com',
], process.env.NODE_ENV === 'development');

app.use(cspGenerator.middleware());

2. Micro Frontend’ler Arası Güvenli İletişim#

// Güvenli iletişim kanalı
class SecureMicroFrontendCommunication {
  private secretKey: string;
  private trustedOrigins: Set<string>;

  constructor(secretKey: string, trustedOrigins: string[]) {
    this.secretKey = secretKey;
    this.trustedOrigins = new Set(trustedOrigins);
  }

  async sendSecureMessage(data: any, targetOrigin: string): Promise<boolean> {
    if (!this.trustedOrigins.has(targetOrigin)) {
      throw new Error(`Güvenilmeyen origin: ${targetOrigin}`);
    }

    try {
      // Timestamp ve nonce ile mesaj oluştur
      const message = {
        data,
        timestamp: Date.now(),
        nonce: this.generateNonce(),
      };

      // Mesajı imzala
      const signature = await this.signMessage(message);
      const secureMessage = { ...message, signature };

      // postMessage ile gönder
      const targetWindow = this.findTargetWindow(targetOrigin);
      if (targetWindow) {
        targetWindow.postMessage(JSON.stringify(secureMessage), targetOrigin);
        return true;
      }

      return false;
    } catch (error) {
      console.error('Güvenli mesaj gönderme başarısız:', error);
      return false;
    }
  }

  async verifyAndHandleMessage(event: MessageEvent): Promise<boolean> {
    if (!this.trustedOrigins.has(event.origin)) {
      console.warn(`Güvenilmeyen origin'den mesaj: ${event.origin}`);
      return false;
    }

    try {
      const message = JSON.parse(event.data);

      // Timestamp doğrula (replay saldırılarını önle)
      const age = Date.now() - message.timestamp;
      if (age > 60000) { // Maksimum 1 dakika yaş
        console.warn('Mesaj çok eski, potansiyel replay saldırısı');
        return false;
      }

      // İmzayı doğrula
      const isValid = await this.verifySignature(message);
      if (!isValid) {
        console.warn('Geçersiz mesaj imzası');
        return false;
      }

      // Mesajı işle
      this.handleVerifiedMessage(message.data);
      return true;
    } catch (error) {
      console.error('Mesaj doğrulama başarısız:', error);
      return false;
    }
  }

  private async signMessage(message: any): Promise<string> {
    const encoder = new TextEncoder();
    const data = encoder.encode(JSON.stringify(message));
    const keyData = encoder.encode(this.secretKey);

    const cryptoKey = await crypto.subtle.importKey(
      'raw',
      keyData,
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    );

    const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
    return Array.from(new Uint8Array(signature))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  }

  private async verifySignature(message: any): Promise<boolean> {
    const { signature, ...messageWithoutSignature } = message;
    const expectedSignature = await this.signMessage(messageWithoutSignature);
    return signature === expectedSignature;
  }

  private generateNonce(): string {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
  }

  private findTargetWindow(targetOrigin: string): Window | null {
    // Önceki örnekteki implementasyona benzer
    return null; // Kısalık için basitleştirildi
  }

  private handleVerifiedMessage(data: any) {
    // Doğrulanmış mesajı ele al
    console.log('Doğrulanmış mesaj alındı:', data);
  }
}

Migrasyon Stratejileri#

Strangler Fig Pattern Uygulaması#

// Monolith'ten micro frontend'lere kademeli migrasyon
class StranglerFigMigration {
  private routes: Map<string, 'monolith' | 'microfrontend'> = new Map();
  private featureFlags: Map<string, boolean> = new Map();

  constructor(private config: MigrationConfig) {
    this.initializeRoutes();
  }

  private initializeRoutes() {
    // Tüm route'lar monolith'e gidecek şekilde başla
    this.config.allRoutes.forEach(route => {
      this.routes.set(route, 'monolith');
    });

    // Feature flag'lara göre kademeli olarak micro frontend route'larını etkinleştir
    this.config.microfrontendRoutes.forEach(route => {
      const flagName = `enable_mf_${route.replace(/[^a-zA-Z0-9]/g, '_')}`;
      if (this.featureFlags.get(flagName)) {
        this.routes.set(route, 'microfrontend');
      }
    });
  }

  routeRequest(path: string): 'monolith' | 'microfrontend' {
    // Önce tam eşleşme kontrolü
    if (this.routes.has(path)) {
      return this.routes.get(path)!;
    }

    // Pattern eşleşmeleri kontrol et
    for (const [route, target] of this.routes.entries()) {
      if (this.matchesPattern(path, route)) {
        return target;
      }
    }

    // Bilinmeyen route'lar için monolith varsayılan
    return 'monolith';
  }

  migrateRoute(route: string) {
    console.log(`${route} route'u micro frontend'e migrate ediliyor`);
    this.routes.set(route, 'microfrontend');

    // İzleme için migrasyon event'ini logla
    this.logMigrationEvent(route);
  }

  rollbackRoute(route: string) {
    console.log(`${route} route'u monolith'e geri alınıyor`);
    this.routes.set(route, 'monolith');

    // Rollback event'ini logla
    this.logRollbackEvent(route);
  }

  private matchesPattern(path: string, pattern: string): boolean {
    // Basit wildcard eşleştirme
    const regex = new RegExp(
      '^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
    );
    return regex.test(path);
  }

  private logMigrationEvent(route: string) {
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.track('Route Migrated', {
        route,
        timestamp: Date.now(),
      });
    }
  }

  private logRollbackEvent(route: string) {
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.track('Route Rolled Back', {
        route,
        timestamp: Date.now(),
      });
    }
  }
}

interface MigrationConfig {
  allRoutes: string[];
  microfrontendRoutes: string[];
}

Ekosistem Görünümü#

Micro frontend ekosistemi hareket etmeye devam ediyor. Takip etmeye değer üç yön var:

  • Module Federation’ın yanında single-spa gibi framework-agnostic orkestrasyon
  • CDN sağlayıcılarının parçaları yanıt tarayıcıya ulaşmadan birleştirdiği edge-side composition
  • Her parça hazır olmadan kabuğu göndermeye başlayan streaming mimariler

Bu Kalıplar Nerede Geçerli#

Event sourcing, performans bütçelemesi ve uygulama genelinde hata takibi, birkaç takım paylaşımlı state üzerinde bağımsız deploy almaya başladığında karşılığını verir. Bu ölçeğin altında tutulan defter, ortadan kaldırdığı koordinasyondan pahalıya gelir: tek bundle gönderen tek takım için build-time integration ve sade bir store daha uygundur. Runtime yüklemeyi ancak bağımsız deploy’lar darboğaza dönüştüğünde ekleyin. İki maliyet her ölçekte sabit kalır. Her dinamik yükleme başarısız olabilir, bu yüzden nazik degradation ilk sürüme aittir ve sonraki bir sağlamlaştırma turuna bırakılamaz; her cross-origin mesaj sınırı da açıldığı günden itibaren origin kontrolü ve imza ister.

Geçişe yeni başlıyorsanız, monolith’teki tek bir route’u feature flag arkasında strangler-fig yönlendiricisine taşıyın; böylece geri dönüş bir konfigürasyon değişikliği olarak kalır.


Tam Seri Navigasyonu

  • Kısım 1: Mimari temelleri
  • Kısım 2: Uygulama kalıpları
  • Kısım 3 (Mevcut): İleri düzey kalıplar ve hata ayıklama

Kaynaklar#

Micro Frontend Mimari Rehberi

Micro frontend mimarisine dair 3 bölümlük kapsamlı rehber. Temel kavramlardan ileri düzey pattern'lere ve production debugging stratejilerine kadar.

İlerleme 3/3 yazı tamamlandı

Bu serideki tüm yazılar

Bölüm 3: Performance, Debugging ve Production Dersleri

İlgili yazılar

AWS CDK Link Kısaltıcı Bölüm 2: Temel Fonksiyonlar & API Geliştirme

Yönlendirme motoru, analytics toplama ve API Gateway yapılandırması: günlük milyonlarca yönlendirme için performans optimizasyonları ve debugging stratejileri.

aws-cdk · lambda · api-gateway +5

DynamoDB Throttling: Hot Partition Çözümleri, Write Sharding ve Retry Stratejileri

Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.

dynamodb · aws · reliability +4

TypeScript Formatlama ve Linting Araçları Karşılaştırması: Biome, Oxlint, ESLint ve Prettier

Modern TypeScript linting ve formatlama araçlarının karşılaştırması: ESLint, Prettier, Biome ve Oxlint için performans ölçümleri, konfigürasyon ve migration.

typescript · code-quality · developer-experience +2

AWS Lambda'da Bun ve Alternatif JavaScript Runtime'ları Çalıştırma

Bun ve Deno'yu AWS Lambda'da custom runtime ile çalıştırma: performans benchmark'ları, maliyet analizi ve production deployment pattern'leri.

lambda · javascript · serverless +2

TypeScript'te Davranışsal Tasarım Kalıpları: Observer, Strategy, Command, State

Observer, Strategy, Command, State ve Mediator TypeScript'te: hangisini dilin kendisi karşılıyor, RxJS, Redux ve XState ne zaman yerini hak ediyor.

typescript · design-patterns · functional-programming +1