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:
- Teil 1: Architektur-Grundlagen und WebView-Integrationsmuster
- Teil 2: WebView-Kommunikationsmuster und Service-Integration
- Teil 3 (Du bist hier): Multi-Channel-Architektur und Produktionsoptimierung
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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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#
// 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.
// 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:
// 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:
// 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:
// 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.
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!
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!