Multi-Channel Micro Frontends: Produktionsoptimierung und plattformübergreifendes Rendering

Fortgeschrittene Patterns für die Bereitstellung von Micro Frontends auf Mobile, Web und Desktop. Performance-Optimierungsstrategien, Offline-Unterstützung und Produktionserfahrungen beim Skalieren auf 10M+ tägliche Interaktionen. Inklusive Rspack, Re.Pack und alternative Bundler-Ansätze.

Sechs Monate nach dem Launch unserer mobilen Micro-Frontend-Architektur standen wir vor einer neuen Herausforderung: Unser Kunde wollte dasselbe Erlebnis auf Web und Desktop. "Könnt ihr nicht einfach dieselben Micro Frontends wiederverwenden?", fragten sie. "Die sind doch bereits für das Web gebaut."

Diese harmlose Frage führte zu 4 Monaten Refactoring, Performance-Optimierung und einigen der kreativsten Engineering-Lösungen, die ich je implementiert habe. Am Ende hatten wir eine einzige Micro-Frontend-Codebasis, die mobile Apps, Webbrowser und Electron-Desktop-Anwendungen bediente und über 10 Millionen tägliche Interaktionen verarbeitete.

Hier ist, was wir über wirklich multi-channel Micro Frontends und die Produktionsoptimierungen gelernt haben, die es möglich machten.

Mobile Micro Frontend Serie#

Dies ist Teil 3 (final) unserer mobilen Micro-Frontend-Serie:

Neu einsteigen? Fang mit Teil 1 für die Grundlagen an.

Kommunikationsmuster gebraucht? Lies zuerst Teil 2.

Alternative Multi-Channel-Ansätze#

Bevor wir in unsere Multi-Channel-Lösung eintauchen, lass mich die alternativen Ansätze teilen, die wir evaluiert haben und warum wir unsere aktuelle Architektur gewählt haben.

Option 1: Re.Pack Multi-Channel-Architektur#

Re.Pack bietet einen einheitlichen Ansatz für React Native, Web und Desktop durch Module Federation:

Was wir experimentierten:

TypeScript
// Re.Pack Multi-Channel-Setup
// webpack.config.js (geteilte Konfiguration)
const { ModuleFederationPlugin } = require('@module-federation/nextjs-mf');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        payment: 'payment@http://localhost:3001/remoteEntry.js',
        booking: 'booking@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@shared/platform': { singleton: true }
      }
    })
  ]
};

// Plattform-Abstraktionsschicht
// @shared/platform/index.ts
export interface PlatformAPI {
  // Navigation
  navigate(screen: string, params?: any): void;
  goBack(): void;

  // Native Features
  camera: {
    takePhoto(): Promise<string>;
    selectFromGallery(): Promise<string>;
  };

  // Storage
  storage: {
    get(key: string): Promise<any>;
    set(key: string, value: any): Promise<void>;
  };

  // Network
  network: {
    isOnline(): boolean;
    onNetworkChange(callback: (online: boolean) => void): void;
  };
}

// Plattform-spezifische Implementierungen
export class ReactNativePlatform implements PlatformAPI {
  // React Native Implementierung
}

export class WebPlatform implements PlatformAPI {
  // Web Implementierung
}

export class ElectronPlatform implements PlatformAPI {
  // Electron Implementierung
}

Warum wir es nicht gewählt haben:

  • Komplexe Konfiguration: 3 verschiedene Webpack-Konfigurationen zu verwalten
  • Plattform-Abstraktionsschicht: Zusätzliche Komplexitätsebene für jedes Feature
  • Performance: Module Federation fügt Runtime-Overhead hinzu
  • Debugging: Schwierig zu debuggen bei plattformübergreifenden Problemen

Option 2: Rspack Universal Build#

Rspack bietet schnellere Builds, aber unsere Experimente zeigten Einschränkungen:

JavaScript
// rspack.config.js
const { defineConfig } = require('@rspack/cli');
const { RspackPlugin } = require('@rspack/core');

module.exports = defineConfig({
  target: ['web', 'node'], // Multi-target Build
  experiments: {
    rspackFuture: {
      newTreeshaking: true,
      newSplitChunks: true
    }
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
        }
      }
    }
  }
});

Rspack-Herausforderungen:

  • Ecosystem: Noch nicht so ausgereift wie Webpack
  • React Native: Begrenzte Unterstützung für React Native-spezifische Features
  • Plugin-Kompatibilität: Viele Webpack-Plugins funktionieren noch nicht

Option 3: Vite + Tauri (Desktop)#

Für Desktop-Apps experimentierten wir mit Vite + Tauri:

JavaScript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'esnext',
    rollupOptions: {
      external: ['@tauri-apps/api']
    }
  },
  define: {
    __TAURI__: JSON.stringify(process.env.TAURI_PLATFORM !== undefined)
  }
});

// src-tauri/tauri.conf.json
{
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "window": {
        "all": true
      },
      "filesystem": {
        "all": true
      }
    }
  }
}

Warum wir zu unserer Lösung wechselten:

  • Team-Expertise: Unser Team war bereits vertraut mit WebView-Architekturen
  • Bewährte Patterns: Wir hatten bereits gut funktionierende Kommunikationspatterns
  • Einfachheit: Eine Codebasis, drei Deployment-Ziele
  • Performance: Bessere Kontrolle über Optimierungen

Unsere Multi-Channel-Architektur: Die einheitliche Lösung#

Nach dem Evaluieren aller Optionen entschieden wir uns für eine einheitlichere Herangehensweise: Eine progressive Web App mit plattform-spezifischen Wrappers.

Loading diagram...

Der Platform Detection Layer#

Das Herzstück unserer Architektur ist ein intelligenter Platform Detection Layer:

TypeScript
// lib/platform-detector.ts
export enum PlatformType {
  REACT_NATIVE_IOS = 'react-native-ios',
  REACT_NATIVE_ANDROID = 'react-native-android',
  ELECTRON_WINDOWS = 'electron-windows',
  ELECTRON_MACOS = 'electron-macos',
  ELECTRON_LINUX = 'electron-linux',
  WEB_CHROME = 'web-chrome',
  WEB_SAFARI = 'web-safari',
  WEB_FIREFOX = 'web-firefox',
  WEB_EDGE = 'web-edge',
  WEB_UNKNOWN = 'web-unknown'
}

export class PlatformDetector {
  private static instance: PlatformDetector;
  private platform: PlatformType;
  private capabilities: PlatformCapabilities;

  private constructor() {
    this.detectPlatform();
    this.detectCapabilities();
  }

  static getInstance(): PlatformDetector {
    if (!PlatformDetector.instance) {
      PlatformDetector.instance = new PlatformDetector();
    }
    return PlatformDetector.instance;
  }

  private detectPlatform(): void {
    // React Native Detection
    if (window.ReactNativeWebView) {
      const userAgent = navigator.userAgent;
      this.platform = userAgent.includes('iPhone') || userAgent.includes('iPad')
        ? PlatformType.REACT_NATIVE_IOS
        : PlatformType.REACT_NATIVE_ANDROID;
      return;
    }

    // Electron Detection
    if (window.electron || navigator.userAgent.includes('Electron')) {
      const platform = navigator.platform.toLowerCase();
      if (platform.includes('win')) {
        this.platform = PlatformType.ELECTRON_WINDOWS;
      } else if (platform.includes('mac')) {
        this.platform = PlatformType.ELECTRON_MACOS;
      } else {
        this.platform = PlatformType.ELECTRON_LINUX;
      }
      return;
    }

    // Browser Detection
    const userAgent = navigator.userAgent.toLowerCase();
    if (userAgent.includes('chrome') && !userAgent.includes('edge')) {
      this.platform = PlatformType.WEB_CHROME;
    } else if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
      this.platform = PlatformType.WEB_SAFARI;
    } else if (userAgent.includes('firefox')) {
      this.platform = PlatformType.WEB_FIREFOX;
    } else if (userAgent.includes('edge')) {
      this.platform = PlatformType.WEB_EDGE;
    } else {
      this.platform = PlatformType.WEB_UNKNOWN;
    }
  }

  private detectCapabilities(): void {
    this.capabilities = {
      hasCamera: this.hasCamera(),
      hasPushNotifications: this.hasPushNotifications(),
      hasFileSystem: this.hasFileSystem(),
      hasBiometrics: this.hasBiometrics(),
      hasDeepLinking: this.hasDeepLinking(),
      hasSystemTray: this.hasSystemTray(),
      hasAutoUpdates: this.hasAutoUpdates(),
      supportsOffline: this.supportsOffline()
    };
  }

  // Getter-Methoden
  getPlatform(): PlatformType {
    return this.platform;
  }

  getCapabilities(): PlatformCapabilities {
    return this.capabilities;
  }

  isReactNative(): boolean {
    return this.platform.startsWith('react-native');
  }

  isElectron(): boolean {
    return this.platform.startsWith('electron');
  }

  isWeb(): boolean {
    return this.platform.startsWith('web');
  }

  isMobile(): boolean {
    return this.isReactNative();
  }

  isDesktop(): boolean {
    return this.isElectron();
  }

  // Capability Detection-Methoden
  private hasCamera(): boolean {
    if (this.isReactNative()) return true;
    if (this.isElectron()) return false;
    return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
  }

  private hasPushNotifications(): boolean {
    if (this.isReactNative()) return true;
    if (this.isElectron()) return true;
    return 'serviceWorker' in navigator && 'PushManager' in window;
  }

  private hasFileSystem(): boolean {
    if (this.isReactNative()) return true;
    if (this.isElectron()) return true;
    return 'showOpenFilePicker' in window;
  }

  private hasBiometrics(): boolean {
    return this.isReactNative();
  }

  private hasDeepLinking(): boolean {
    if (this.isReactNative()) return true;
    if (this.isElectron()) return true;
    return 'registerProtocolHandler' in navigator;
  }

  private hasSystemTray(): boolean {
    return this.isElectron();
  }

  private hasAutoUpdates(): boolean {
    return this.isElectron() || this.isReactNative();
  }

  private supportsOffline(): boolean {
    return 'serviceWorker' in navigator;
  }
}

export interface PlatformCapabilities {
  hasCamera: boolean;
  hasPushNotifications: boolean;
  hasFileSystem: boolean;
  hasBiometrics: boolean;
  hasDeepLinking: boolean;
  hasSystemTray: boolean;
  hasAutoUpdates: boolean;
  supportsOffline: boolean;
}

Feature Abstraction mit Capability-Based Design#

Anstatt plattform-spezifischen Code zu schreiben, verwenden wir ein capability-based Design:

TypeScript
// lib/feature-manager.ts
import { PlatformDetector } from './platform-detector';

export class FeatureManager {
  private detector = PlatformDetector.getInstance();

  async takePicture(): Promise<string | null> {
    const capabilities = this.detector.getCapabilities();

    if (!capabilities.hasCamera) {
      throw new Error('Camera nicht verfügbar auf dieser Plattform');
    }

    if (this.detector.isReactNative()) {
      return this.takePictureReactNative();
    }

    if (this.detector.isWeb()) {
      return this.takePictureWeb();
    }

    throw new Error('Kamera-Implementierung für diese Plattform nicht verfügbar');
  }

  private async takePictureReactNative(): Promise<string> {
    // React Native Implementierung
    const result = await window.ReactNativeWebView.postMessage(
      JSON.stringify({
        type: 'CAMERA_TAKE_PICTURE',
        options: { quality: 0.8, maxWidth: 1920, maxHeight: 1080 }
      })
    );
    return result.imageUri;
  }

  private async takePictureWeb(): Promise<string> {
    // Web Implementierung
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: 'environment' }
    });

    const video = document.createElement('video');
    video.srcObject = stream;
    video.play();

    return new Promise((resolve) => {
      video.addEventListener('canplay', () => {
        const canvas = document.createElement('canvas');
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;

        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(video, 0, 0);

        stream.getTracks().forEach(track => track.stop());
        resolve(canvas.toDataURL('image/jpeg', 0.8));
      });
    });
  }

  async sendPushNotification(message: string, title: string): Promise<void> {
    const capabilities = this.detector.getCapabilities();

    if (!capabilities.hasPushNotifications) {
      console.warn('Push Notifications nicht verfügbar auf dieser Plattform');
      return;
    }

    if (this.detector.isReactNative()) {
      return this.sendPushReactNative(message, title);
    }

    if (this.detector.isElectron()) {
      return this.sendPushElectron(message, title);
    }

    if (this.detector.isWeb()) {
      return this.sendPushWeb(message, title);
    }
  }

  private async sendPushReactNative(message: string, title: string): Promise<void> {
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        type: 'SEND_PUSH_NOTIFICATION',
        payload: { title, message }
      })
    );
  }

  private async sendPushElectron(message: string, title: string): Promise<void> {
    window.electron.notification.show({
      title,
      body: message,
      icon: '/assets/notification-icon.png'
    });
  }

  private async sendPushWeb(message: string, title: string): Promise<void> {
    if ('serviceWorker' in navigator && 'Notification' in window) {
      const permission = await Notification.requestPermission();
      if (permission === 'granted') {
        new Notification(title, { body: message });
      }
    }
  }

  async saveFile(content: string, filename: string): Promise<void> {
    const capabilities = this.detector.getCapabilities();

    if (!capabilities.hasFileSystem) {
      // Fallback zur Download-Funktionalität
      this.downloadFile(content, filename);
      return;
    }

    if (this.detector.isElectron()) {
      return this.saveFileElectron(content, filename);
    }

    if (this.detector.isWeb() && 'showSaveFilePicker' in window) {
      return this.saveFileWeb(content, filename);
    }

    // Fallback
    this.downloadFile(content, filename);
  }

  private async saveFileElectron(content: string, filename: string): Promise<void> {
    window.electron.fs.writeFile(filename, content);
  }

  private async saveFileWeb(content: string, filename: string): Promise<void> {
    const fileHandle = await (window as any).showSaveFilePicker({
      suggestedName: filename,
      types: [{
        description: 'Text files',
        accept: { 'text/plain': ['.txt'] }
      }]
    });

    const writable = await fileHandle.createWritable();
    await writable.write(content);
    await writable.close();
  }

  private downloadFile(content: string, filename: string): void {
    const blob = new Blob([content], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }
}

Produktionsoptimierungen: Für 10M+ tägliche Interaktionen#

Smart Bundle Splitting#

Mit drei Plattformen müssen wir intelligent über Bundle-Splitting nachdenken:

TypeScript
// webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    // Core Bundle - gemeinsam für alle Plattformen
    core: './src/core/index.ts',

    // Plattform-spezifische Bundles
    'platform-react-native': './src/platforms/react-native/index.ts',
    'platform-electron': './src/platforms/electron/index.ts',
    'platform-web': './src/platforms/web/index.ts',

    // Feature Bundles - lazy loaded
    camera: './src/features/camera/index.ts',
    payments: './src/features/payments/index.ts',
    notifications: './src/features/notifications/index.ts'
  },

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Vendor-Bibliotheken
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          maxSize: 250000 // 250KB Max
        },

        // Gemeinsame Utilities
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          maxSize: 150000 // 150KB Max
        },

        // React-spezifisch
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all'
        },

        // Plattform-spezifische Bundles
        'react-native-vendor': {
          test: /[\\/]node_modules[\\/].*react-native.*/,
          name: 'react-native-vendor',
          chunks: 'all'
        }
      }
    },

    // Code-Splitting für bessere Performance
    runtimeChunk: {
      name: 'runtime'
    }
  },

  // Plattform-spezifische Externals
  externals: function(context, request, callback) {
    // React Native Externals
    if (request.startsWith('@react-native-')) {
      return callback(null, 'umd ' + request);
    }

    // Electron Externals
    if (request === 'electron') {
      return callback(null, 'commonjs electron');
    }

    callback();
  }
};

Adaptive Loading Strategy#

Verschiedene Plattformen haben unterschiedliche Performance-Charakteristika:

TypeScript
// lib/adaptive-loader.ts
import { PlatformDetector } from './platform-detector';

export class AdaptiveLoader {
  private detector = PlatformDetector.getInstance();
  private loadedModules = new Map<string, Promise<any>>();

  async loadFeature(featureName: string): Promise<any> {
    // Prüfen, ob bereits geladen
    if (this.loadedModules.has(featureName)) {
      return this.loadedModules.get(featureName);
    }

    const loadPromise = this.loadFeatureByPlatform(featureName);
    this.loadedModules.set(featureName, loadPromise);

    return loadPromise;
  }

  private async loadFeatureByPlatform(featureName: string): Promise<any> {
    const platform = this.detector.getPlatform();
    const connectionType = this.getConnectionType();

    // Intelligente Loading-Strategie basierend auf Plattform und Verbindung
    const loadingStrategy = this.getLoadingStrategy(platform, connectionType);

    switch (loadingStrategy) {
      case 'immediate':
        return this.loadImmediate(featureName);

      case 'deferred':
        return this.loadDeferred(featureName);

      case 'lazy':
        return this.loadLazy(featureName);

      case 'preload':
        return this.preloadFeature(featureName);

      default:
        return this.loadImmediate(featureName);
    }
  }

  private getConnectionType(): 'fast' | 'slow' | 'offline' {
    if (!navigator.onLine) return 'offline';

    // Network Information API (falls verfügbar)
    if ('connection' in navigator) {
      const connection = (navigator as any).connection;
      const effectiveType = connection.effectiveType;

      if (effectiveType === '4g' || effectiveType === 'fast') {
        return 'fast';
      }
    }

    // Fallback basierend auf Plattform
    if (this.detector.isReactNative()) {
      return 'slow'; // Mobile Verbindungen sind oft langsamer
    }

    if (this.detector.isElectron()) {
      return 'fast'; // Desktop hat normalerweise gute Verbindung
    }

    return 'fast'; // Web-Standard
  }

  private getLoadingStrategy(
    platform: string,
    connectionType: 'fast' | 'slow' | 'offline'
  ): 'immediate' | 'deferred' | 'lazy' | 'preload' {
    // Offline: Lade nur kritische Features
    if (connectionType === 'offline') {
      return 'lazy';
    }

    // Mobile mit langsamer Verbindung: Deferred Loading
    if (platform.includes('react-native') && connectionType === 'slow') {
      return 'deferred';
    }

    // Desktop: Preload für bessere UX
    if (platform.includes('electron') && connectionType === 'fast') {
      return 'preload';
    }

    // Web: Immediate Loading
    if (platform.includes('web') && connectionType === 'fast') {
      return 'immediate';
    }

    return 'deferred';
  }

  private async loadImmediate(featureName: string): Promise<any> {
    console.log(`Loading ${featureName} immediately`);
    return import(`../features/${featureName}/index.ts`);
  }

  private async loadDeferred(featureName: string): Promise<any> {
    console.log(`Deferring load of ${featureName}`);

    // Warte auf nächsten Idle-Zeitraum
    return new Promise((resolve) => {
      if ('requestIdleCallback' in window) {
        requestIdleCallback(async () => {
          const module = await import(`../features/${featureName}/index.ts`);
          resolve(module);
        });
      } else {
        // Fallback für Browser ohne requestIdleCallback
        setTimeout(async () => {
          const module = await import(`../features/${featureName}/index.ts`);
          resolve(module);
        }, 100);
      }
    });
  }

  private async loadLazy(featureName: string): Promise<any> {
    console.log(`Lazy loading ${featureName} - will load on demand`);

    // Gibt einen Proxy zurück, der das Modul lädt, wenn darauf zugegriffen wird
    return new Proxy({}, {
      get: async (target, prop) => {
        const module = await import(`../features/${featureName}/index.ts`);
        return module[prop as string];
      }
    });
  }

  private async preloadFeature(featureName: string): Promise<any> {
    console.log(`Preloading ${featureName} in background`);

    // Preload mit niedrigster Priorität
    const link = document.createElement('link');
    link.rel = 'modulepreload';
    link.href = `/features/${featureName}/index.js`;
    document.head.appendChild(link);

    // Tatsächliches Laden
    return import(`../features/${featureName}/index.ts`);
  }

  // Preload kritischer Features beim App-Start
  async preloadCriticalFeatures(): Promise<void> {
    const criticalFeatures = ['authentication', 'navigation', 'storage'];
    const platform = this.detector.getPlatform();

    // Plattform-spezifische kritische Features
    const platformSpecificFeatures = {
      'react-native': ['camera', 'push-notifications'],
      'electron': ['file-system', 'system-tray'],
      'web': ['service-worker', 'web-share']
    };

    const allCriticalFeatures = [
      ...criticalFeatures,
      ...(platformSpecificFeatures[platform.split('-')[0] as keyof typeof platformSpecificFeatures] || [])
    ];

    // Parallel preloading für bessere Performance
    await Promise.allSettled(
      allCriticalFeatures.map(feature => this.preloadFeature(feature))
    );
  }
}

// Hook für React-Komponenten
export function useAdaptiveLoader() {
  const [loader] = useState(() => new AdaptiveLoader());

  return {
    loadFeature: (featureName: string) => loader.loadFeature(featureName),
    preloadCriticalFeatures: () => loader.preloadCriticalFeatures()
  };
}

Progressive Enhancement per Plattform#

Jede Plattform bekommt optimierte Features basierend auf ihren Stärken:

TypeScript
// components/adaptive-ui.tsx
import React, { useMemo } from 'react';
import { PlatformDetector } from '../lib/platform-detector';

interface AdaptiveUIProps {
  children: React.ReactNode;
  mobileOptimized?: React.ReactNode;
  desktopOptimized?: React.ReactNode;
  webOptimized?: React.ReactNode;
}

export function AdaptiveUI({
  children,
  mobileOptimized,
  desktopOptimized,
  webOptimized
}: AdaptiveUIProps) {
  const platform = useMemo(() => PlatformDetector.getInstance(), []);

  // Rendere plattform-spezifische Version, falls verfügbar
  if (platform.isMobile() && mobileOptimized) {
    return <>{mobileOptimized}</>;
  }

  if (platform.isDesktop() && desktopOptimized) {
    return <>{desktopOptimized}</>;
  }

  if (platform.isWeb() && webOptimized) {
    return <>{webOptimized}</>;
  }

  // Fallback zur Standard-Version
  return <>{children}</>;
}

// Beispiel-Verwendung
export function PaymentForm() {
  return (
    <AdaptiveUI
      // Standard UI für alle Plattformen
      children={
        <div className="payment-form">
          <input type="text" placeholder="Kartennummer" />
          <button>Zahlen</button>
        </div>
      }

      // Mobile: Touch-optimierte UI mit Biometrics
      mobileOptimized={
        <div className="payment-form-mobile">
          <input
            type="text"
            placeholder="Kartennummer"
            className="touch-optimized"
          />
          <BiometricButton />
          <button className="large-touch-target">Zahlen</button>
        </div>
      }

      // Desktop: Keyboard-Shortcuts und erweiterte Funktionen
      desktopOptimized={
        <div className="payment-form-desktop">
          <input
            type="text"
            placeholder="Kartennummer (Strg+V zum Einfügen)"
            className="keyboard-optimized"
          />
          <KeyboardShortcuts />
          <button>Zahlen (Enter)</button>
        </div>
      }

      // Web: PWA-Features und Web APIs
      webOptimized={
        <div className="payment-form-web">
          <input
            type="text"
            placeholder="Kartennummer"
            autoComplete="cc-number"
          />
          <WebAuthnButton />
          <button>Zahlen</button>
        </div>
      }
    />
  );
}

Offline-First Architektur#

Mit drei Plattformen ist eine einheitliche Offline-Strategie crucial:

TypeScript
// lib/offline-manager.ts
import { PlatformDetector } from './platform-detector';

interface OfflineData {
  id: string;
  type: 'user-action' | 'api-call' | 'file-upload';
  data: any;
  timestamp: number;
  retryCount: number;
  platform: string;
}

export class OfflineManager {
  private detector = PlatformDetector.getInstance();
  private pendingActions = new Map<string, OfflineData>();
  private storage: Storage;
  private maxRetries = 3;

  constructor() {
    this.initializeStorage();
    this.setupNetworkListeners();
    this.loadPendingActions();
  }

  private initializeStorage(): void {
    if (this.detector.isReactNative()) {
      // React Native: AsyncStorage über Bridge
      this.storage = {
        getItem: (key: string) =>
          window.ReactNativeWebView.postMessage(
            JSON.stringify({ type: 'STORAGE_GET', key })
          ),
        setItem: (key: string, value: string) =>
          window.ReactNativeWebView.postMessage(
            JSON.stringify({ type: 'STORAGE_SET', key, value })
          ),
        removeItem: (key: string) =>
          window.ReactNativeWebView.postMessage(
            JSON.stringify({ type: 'STORAGE_REMOVE', key })
          )
      } as Storage;
    } else if (this.detector.isElectron()) {
      // Electron: Verwende electron-store
      this.storage = window.electron.store;
    } else {
      // Web: localStorage mit IndexedDB fallback
      this.storage = this.createWebStorage();
    }
  }

  private createWebStorage(): Storage {
    // Implementierung einer erweiterten Storage für Web
    // mit IndexedDB für größere Datenmengen
    return {
      getItem: async (key: string) => {
        try {
          return localStorage.getItem(key);
        } catch {
          // Fallback zu IndexedDB
          return this.getFromIndexedDB(key);
        }
      },
      setItem: async (key: string, value: string) => {
        try {
          localStorage.setItem(key, value);
        } catch {
          // Fallback zu IndexedDB
          return this.setInIndexedDB(key, value);
        }
      },
      removeItem: async (key: string) => {
        try {
          localStorage.removeItem(key);
        } catch {
          return this.removeFromIndexedDB(key);
        }
      }
    } as Storage;
  }

  private async getFromIndexedDB(key: string): Promise<string | null> {
    return new Promise((resolve) => {
      const request = indexedDB.open('OfflineStorage', 1);
      request.onsuccess = () => {
        const db = request.result;
        const transaction = db.transaction(['data'], 'readonly');
        const store = transaction.objectStore('data');
        const getRequest = store.get(key);

        getRequest.onsuccess = () => {
          resolve(getRequest.result?.value || null);
        };
      };
    });
  }

  private async setInIndexedDB(key: string, value: string): Promise<void> {
    return new Promise((resolve) => {
      const request = indexedDB.open('OfflineStorage', 1);
      request.onsuccess = () => {
        const db = request.result;
        const transaction = db.transaction(['data'], 'readwrite');
        const store = transaction.objectStore('data');
        store.put({ key, value });
        transaction.oncomplete = () => resolve();
      };
    });
  }

  private async removeFromIndexedDB(key: string): Promise<void> {
    return new Promise((resolve) => {
      const request = indexedDB.open('OfflineStorage', 1);
      request.onsuccess = () => {
        const db = request.result;
        const transaction = db.transaction(['data'], 'readwrite');
        const store = transaction.objectStore('data');
        store.delete(key);
        transaction.oncomplete = () => resolve();
      };
    });
  }

  private setupNetworkListeners(): void {
    if (this.detector.isReactNative()) {
      // React Native: Netzwerk-Status über Bridge
      window.addEventListener('message', (event) => {
        const data = JSON.parse(event.data);
        if (data.type === 'NETWORK_STATUS_CHANGED') {
          this.handleNetworkChange(data.isOnline);
        }
      });
    } else {
      // Web und Electron: Standard Navigator API
      window.addEventListener('online', () => this.handleNetworkChange(true));
      window.addEventListener('offline', () => this.handleNetworkChange(false));
    }
  }

  private async handleNetworkChange(isOnline: boolean): void {
    if (isOnline) {
      console.log('Network back online - processing pending actions');
      await this.processPendingActions();
    } else {
      console.log('Network went offline - queuing actions');
    }
  }

  async queueAction(
    id: string,
    type: 'user-action' | 'api-call' | 'file-upload',
    data: any
  ): Promise<void> {
    const offlineData: OfflineData = {
      id,
      type,
      data,
      timestamp: Date.now(),
      retryCount: 0,
      platform: this.detector.getPlatform()
    };

    this.pendingActions.set(id, offlineData);
    await this.persistPendingActions();

    // Versuche sofort zu verarbeiten, falls online
    if (navigator.onLine) {
      await this.processAction(offlineData);
    }
  }

  private async processPendingActions(): Promise<void> {
    const actions = Array.from(this.pendingActions.values());

    // Sortiere nach Timestamp (FIFO)
    actions.sort((a, b) => a.timestamp - b.timestamp);

    for (const action of actions) {
      try {
        await this.processAction(action);
        this.pendingActions.delete(action.id);
      } catch (error) {
        console.error(`Failed to process action ${action.id}:`, error);

        action.retryCount++;
        if (action.retryCount >= this.maxRetries) {
          console.error(`Action ${action.id} exceeded max retries, removing`);
          this.pendingActions.delete(action.id);
        }
      }
    }

    await this.persistPendingActions();
  }

  private async processAction(action: OfflineData): Promise<void> {
    switch (action.type) {
      case 'api-call':
        await this.processAPICall(action);
        break;
      case 'user-action':
        await this.processUserAction(action);
        break;
      case 'file-upload':
        await this.processFileUpload(action);
        break;
    }
  }

  private async processAPICall(action: OfflineData): Promise<void> {
    const { url, method, data, headers } = action.data;

    const response = await fetch(url, {
      method,
      headers,
      body: data ? JSON.stringify(data) : undefined
    });

    if (!response.ok) {
      throw new Error(`API call failed: ${response.status}`);
    }
  }

  private async processUserAction(action: OfflineData): Promise<void> {
    // Verarbeite Benutzeraktionen basierend auf dem Typ
    const { actionType, payload } = action.data;

    switch (actionType) {
      case 'like-post':
        await fetch('/api/posts/like', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });
        break;
      case 'save-draft':
        await fetch('/api/drafts', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });
        break;
    }
  }

  private async processFileUpload(action: OfflineData): Promise<void> {
    const { file, uploadUrl, metadata } = action.data;

    const formData = new FormData();
    formData.append('file', file);
    formData.append('metadata', JSON.stringify(metadata));

    const response = await fetch(uploadUrl, {
      method: 'POST',
      body: formData
    });

    if (!response.ok) {
      throw new Error(`File upload failed: ${response.status}`);
    }
  }

  private async loadPendingActions(): Promise<void> {
    try {
      const stored = await this.storage.getItem('pendingActions');
      if (stored) {
        const actions = JSON.parse(stored) as OfflineData[];
        actions.forEach(action => {
          this.pendingActions.set(action.id, action);
        });
      }
    } catch (error) {
      console.error('Failed to load pending actions:', error);
    }
  }

  private async persistPendingActions(): Promise<void> {
    try {
      const actions = Array.from(this.pendingActions.values());
      await this.storage.setItem('pendingActions', JSON.stringify(actions));
    } catch (error) {
      console.error('Failed to persist pending actions:', error);
    }
  }

  // Public API für React-Komponenten
  getPendingActionsCount(): number {
    return this.pendingActions.size;
  }

  async clearAllPendingActions(): Promise<void> {
    this.pendingActions.clear();
    await this.storage.removeItem('pendingActions');
  }
}

// React Hook
export function useOfflineManager() {
  const [manager] = useState(() => new OfflineManager());
  const [pendingCount, setPendingCount] = useState(0);

  useEffect(() => {
    const updateCount = () => {
      setPendingCount(manager.getPendingActionsCount());
    };

    const interval = setInterval(updateCount, 1000);
    return () => clearInterval(interval);
  }, [manager]);

  return {
    queueAction: (id: string, type: any, data: any) =>
      manager.queueAction(id, type, data),
    pendingActionsCount: pendingCount,
    clearPending: () => manager.clearAllPendingActions()
  };
}

Performance Monitoring: Plattformübergreifende Insights#

Einheitliches Monitoring Setup#

TypeScript
// lib/performance-monitor.ts
import { PlatformDetector } from './platform-detector';

interface PerformanceMetric {
  name: string;
  value: number;
  timestamp: number;
  platform: string;
  context: Record<string, any>;
}

export class PerformanceMonitor {
  private detector = PlatformDetector.getInstance();
  private metrics: PerformanceMetric[] = [];
  private observers: PerformanceObserver[] = [];

  constructor() {
    this.setupPlatformSpecificMonitoring();
    this.setupCommonMonitoring();
  }

  private setupPlatformSpecificMonitoring(): void {
    const platform = this.detector.getPlatform();

    if (this.detector.isReactNative()) {
      this.setupReactNativeMonitoring();
    } else if (this.detector.isElectron()) {
      this.setupElectronMonitoring();
    } else {
      this.setupWebMonitoring();
    }
  }

  private setupReactNativeMonitoring(): void {
    // React Native Performance Tracking über Bridge
    window.addEventListener('message', (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'PERFORMANCE_METRIC') {
        this.recordMetric(data.name, data.value, data.context);
      }
    });

    // Request native performance metrics
    this.requestNativeMetrics();
  }

  private setupElectronMonitoring(): void {
    // Electron Main Process Metrics
    if (window.electron?.performance) {
      setInterval(() => {
        window.electron.performance.getCPUUsage().then((cpuUsage: number) => {
          this.recordMetric('cpu_usage', cpuUsage, { source: 'electron-main' });
        });

        window.electron.performance.getMemoryUsage().then((memUsage: any) => {
          this.recordMetric('memory_usage', memUsage.rss, { source: 'electron-main' });
        });
      }, 5000);
    }
  }

  private setupWebMonitoring(): void {
    // Web Performance API
    if ('performance' in window) {
      // Navigation Timing
      window.addEventListener('load', () => {
        const navTiming = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
        this.recordMetric('page_load_time', navTiming.loadEventEnd - navTiming.fetchStart);
        this.recordMetric('dom_content_loaded', navTiming.domContentLoadedEventEnd - navTiming.fetchStart);
      });

      // Resource Timing
      const resourceObserver = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.entryType === 'resource') {
            const resourceEntry = entry as PerformanceResourceTiming;
            this.recordMetric('resource_load_time', resourceEntry.duration, {
              resource: resourceEntry.name,
              type: resourceEntry.initiatorType
            });
          }
        });
      });
      resourceObserver.observe({ entryTypes: ['resource'] });
      this.observers.push(resourceObserver);

      // Long Task Detection
      if ('PerformanceObserver' in window) {
        const longTaskObserver = new PerformanceObserver((list) => {
          list.getEntries().forEach((entry) => {
            this.recordMetric('long_task', entry.duration, {
              attribution: (entry as any).attribution
            });
          });
        });

        try {
          longTaskObserver.observe({ entryTypes: ['longtask'] });
          this.observers.push(longTaskObserver);
        } catch (e) {
          console.warn('Long task observer not supported');
        }
      }
    }

    // Core Web Vitals
    this.setupCoreWebVitals();
  }

  private setupCoreWebVitals(): void {
    // CLS (Cumulative Layout Shift)
    let clsValue = 0;
    let clsEntries: PerformanceEntry[] = [];

    const clsObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!(entry as any).hadRecentInput) {
          clsValue += (entry as any).value;
          clsEntries.push(entry);
        }
      }
    });

    try {
      clsObserver.observe({ entryTypes: ['layout-shift'] });
      this.observers.push(clsObserver);

      // Report CLS when page becomes hidden
      document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
          this.recordMetric('cls', clsValue, {
            entries: clsEntries.length
          });
        }
      });
    } catch (e) {
      console.warn('Layout shift observer not supported');
    }

    // FID (First Input Delay)
    const fidObserver = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.recordMetric('fid', (entry as any).processingStart - entry.startTime, {
          target: (entry as any).target?.tagName
        });
      }
    });

    try {
      fidObserver.observe({ entryTypes: ['first-input'] });
      this.observers.push(fidObserver);
    } catch (e) {
      console.warn('First input observer not supported');
    }

    // LCP (Largest Contentful Paint)
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.recordMetric('lcp', lastEntry.startTime, {
        element: (lastEntry as any).element?.tagName
      });
    });

    try {
      lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
      this.observers.push(lcpObserver);
    } catch (e) {
      console.warn('Largest contentful paint observer not supported');
    }
  }

  private setupCommonMonitoring(): void {
    // JavaScript Errors
    window.addEventListener('error', (event) => {
      this.recordMetric('js_error', 1, {
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      });
    });

    // Unhandled Promise Rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.recordMetric('unhandled_promise_rejection', 1, {
        reason: event.reason?.toString()
      });
    });

    // Memory Usage (falls verfügbar)
    if ('memory' in performance) {
      setInterval(() => {
        const memInfo = (performance as any).memory;
        this.recordMetric('js_heap_used', memInfo.usedJSHeapSize);
        this.recordMetric('js_heap_total', memInfo.totalJSHeapSize);
        this.recordMetric('js_heap_limit', memInfo.jsHeapSizeLimit);
      }, 30000);
    }
  }

  private requestNativeMetrics(): void {
    if (this.detector.isReactNative()) {
      // Request iOS/Android specific metrics
      setInterval(() => {
        window.ReactNativeWebView?.postMessage(
          JSON.stringify({ type: 'REQUEST_PERFORMANCE_METRICS' })
        );
      }, 10000);
    }
  }

  recordMetric(name: string, value: number, context: Record<string, any> = {}): void {
    const metric: PerformanceMetric = {
      name,
      value,
      timestamp: Date.now(),
      platform: this.detector.getPlatform(),
      context: {
        ...context,
        userAgent: navigator.userAgent,
        screenSize: `${screen.width}x${screen.height}`,
        connectionType: this.getConnectionType()
      }
    };

    this.metrics.push(metric);

    // Sende sofort bei kritischen Metriken
    if (this.isCriticalMetric(name)) {
      this.sendMetricsImmediately([metric]);
    }

    // Batch-Verarbeitung für normale Metriken
    if (this.metrics.length >= 50) {
      this.flushMetrics();
    }
  }

  private isCriticalMetric(name: string): boolean {
    const criticalMetrics = [
      'js_error',
      'unhandled_promise_rejection',
      'long_task',
      'app_crash'
    ];
    return criticalMetrics.includes(name);
  }

  private getConnectionType(): string {
    if ('connection' in navigator) {
      return (navigator as any).connection.effectiveType || 'unknown';
    }
    return 'unknown';
  }

  async flushMetrics(): Promise<void> {
    if (this.metrics.length === 0) return;

    const metricsToSend = [...this.metrics];
    this.metrics = [];

    await this.sendMetricsImmediately(metricsToSend);
  }

  private async sendMetricsImmediately(metrics: PerformanceMetric[]): Promise<void> {
    try {
      const payload = {
        metrics,
        sessionId: this.getSessionId(),
        timestamp: Date.now(),
        platform: this.detector.getPlatform()
      };

      // Verwende sendBeacon für bessere Zuverlässigkeit
      if ('sendBeacon' in navigator) {
        navigator.sendBeacon('/api/metrics', JSON.stringify(payload));
      } else {
        // Fallback zu fetch
        await fetch('/api/metrics', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          keepalive: true // Wichtig für Metriken beim Seitenverlassen
        });
      }
    } catch (error) {
      console.error('Failed to send metrics:', error);
      // Füge Metriken zurück zur Queue bei Fehler
      this.metrics.unshift(...metrics);
    }
  }

  private getSessionId(): string {
    let sessionId = sessionStorage.getItem('performanceSessionId');
    if (!sessionId) {
      sessionId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
      sessionStorage.setItem('performanceSessionId', sessionId);
    }
    return sessionId;
  }

  // Aufräumen
  dispose(): void {
    this.observers.forEach(observer => observer.disconnect());
    this.flushMetrics();
  }
}

// React Hook
export function usePerformanceMonitor() {
  const [monitor] = useState(() => new PerformanceMonitor());

  useEffect(() => {
    return () => monitor.dispose();
  }, [monitor]);

  return {
    recordMetric: (name: string, value: number, context?: Record<string, any>) =>
      monitor.recordMetric(name, value, context),
    flushMetrics: () => monitor.flushMetrics()
  };
}

Die Lessons Learned: Was wirklich funktioniert#

Nach 4 Monaten Entwicklung und 6 Monaten Produktion hier die wichtigsten Erkenntnisse:

1. Plattform-Detection ist Critical#

Das Problem: Naive Feature-Detection führt zu inkonsistenten Erfahrungen.

Die Lösung: Umfassende Plattform- und Capability-Detection von Anfang an.

TypeScript
// Was NICHT funktioniert
if ('camera' in navigator) {
  // Wird auf Desktop mit WebRTC-Kamera funktionieren,
  // aber nicht optimal für Mobile
}

// Was funktioniert
const detector = PlatformDetector.getInstance();
if (detector.isMobile() && detector.getCapabilities().hasCamera) {
  // Optimiert für mobile Kamera-Workflows
}

2. Bundle-Größe ist plattformabhängig#

Das Problem: 2MB JavaScript ist auf Desktop ok, auf Mobile katastrophal.

Die Lösung: Adaptive Bundle-Strategien:

  • Mobile: Aggressive Code-Splitting, Lazy Loading
  • Desktop: Größere Bundles ok, Preloading für UX
  • Web: Balance zwischen beiden

3. Performance-Monitoring muss vereinheitlicht sein#

Das Problem: Verschiedene Metriken pro Plattform machten Debugging unmöglich.

Die Lösung: Einheitliche Metrik-Pipeline mit plattform-spezifischen Erweiterungen.

4. Offline-First ist non-negotiable#

Das Problem: Mobile Apps ohne Offline-Support werden nicht genutzt.

Die Lösung: Offline-First Design von Tag 1, nicht als Nachgedanke.

5. Platform-Specific Optimizations zahlen sich aus#

Das Problem: One-size-fits-all führt zur schlechtesten gemeinsamen Erfahrung.

Die Lösung: Progressive Enhancement basierend auf Plattform-Capabilities.

Nächste Schritte: Wohin geht die Reise?#

War es den Aufwand wert? Absolutely. Nach 10 Millionen täglichen Interaktionen:

Die Zahlen sprechen für sich:#

  • Entwicklungszeit: 70% weniger Zeit für neue Features (eine Codebasis vs. drei)
  • Bug-Rate: 60% weniger Bugs (einheitliche Logik)
  • Performance: Mobile 40% schneller, Desktop 25% schneller
  • User Satisfaction: 85% der Nutzer verwenden alle drei Plattformen

Die versteckten Vorteile:#

  • Team Velocity: Entwickler können zwischen Plattformen wechseln
  • Feature Parity: Keine Diskrepanzen zwischen Plattformen
  • Testing: Ein Test-Suite für alle Plattformen
  • Onboarding: Neue Entwickler produktiv in Tagen, nicht Wochen

Q4 2025: Native Module Federation#

Wir experimentieren mit echter Module Federation zwischen Plattformen:

TypeScript
// Kommende Architektur
const remoteModules = {
  mobile: 'mobile-shell@http://cdn.example.com/mobile/remoteEntry.js',
  desktop: 'desktop-shell@http://cdn.example.com/desktop/remoteEntry.js',
  web: 'web-shell@http://cdn.example.com/web/remoteEntry.js'
};

Q1 2026: WebAssembly Integration#

Performance-kritische Teile in WASM für alle Plattformen:

TypeScript
// Shared WASM module für alle Plattformen
import { processPayment } from './payment.wasm';

Q2 2026: Edge-First Deployment#

Micro Frontends direkt vom Edge, plattform-abhängig:

TypeScript
// Edge Worker bestimmt optimale Version
if (request.headers['user-agent'].includes('ReactNativeWebView')) {
  return mobileOptimizedBundle;
}

Fazit: Multi-Channel ist die Zukunft#

Multi-Channel Micro Frontends sind nicht nur möglich - sie sind der Schlüssel zur Skalierung moderner Anwendungen. Die anfänglichen Investitionen in eine einheitliche Architektur zahlen sich exponentiell aus, sobald Sie mehr als eine Plattform haben.

Meine wichtigste Empfehlung: Fang mit Platform-Detection und Capability-Based Design an. Alles andere folgt daraus.

Wenn du ähnliche Architekturen baust, würde ich gerne deine Erfahrungen und Herausforderungen hören. Der Bereich der Micro Frontends entwickelt sich schnell, und jede Implementierung lehrt uns neue Dinge über das Mögliche.

Offline Support and Synchronization#

Übersetzung folgt.

Production Monitoring and Analytics#

Übersetzung folgt.

Production Results and Metrics#

Übersetzung folgt.

Key Takeaways#

Übersetzung folgt.

Lessons Learned#

Übersetzung folgt.

When to Use Multi-Channel Micro Frontends#

Übersetzung folgt.

Conclusion#

Übersetzung folgt.

Loading...

Kommentare (0)

An der Unterhaltung teilnehmen

Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren

Noch keine Kommentare

Sei der erste, der deine Gedanken zu diesem Beitrag teilt!

Related Posts