Mobile Micro Frontends mit React Native Expo: WebView-Architektur und reale Implementierung
Tiefgreifende Analyse der Implementierung von Micro Frontend-Architekturen in mobilen Apps mit React Native Expo und WebViews. Echte Produktionserfahrungen, Performance-Daten und kampferprobte Patterns.
Letztes Jahr stand unser Team vor einer Herausforderung, die immer häufiger wird: Wir mussten eine mobile App entwickeln, die mehrere webbasierte Services verschiedener Teams integrieren konnte, jedes mit eigenen Deployment-Zyklen und Tech-Stacks. Der Haken? Wir hatten 8 Wochen Zeit für die Lieferung, und die Web-Teams konnten ihre Feature-Entwicklung nicht stoppen, um uns zu helfen.
Da entschieden wir uns, Micro Frontend-Architektur mit WebViews in unsere React Native App zu bringen. Was folgte, waren 6 Monate voller Debugging, Performance-Optimierung und einiger kreativer Lösungen, die ich nie zu implementieren gedacht hätte. Hier ist, was wir gelernt haben.
Mobile Micro Frontend Serie#
Dies ist Teil 1 einer umfassenden 3-teiligen Serie über mobile Micro Frontends:
- Teil 1 (Du bist hier): Architektur-Grundlagen und WebView-Integrations-Patterns
- Teil 2: WebView-Kommunikations-Patterns und Service-Integration
- Teil 3: Multi-Channel-Architektur und Production-Optimierung
Neu bei mobilen Micro Frontends? Fang hier an, um die Architektur und Implementierungs-Grundlagen zu verstehen.
Bereit zur Implementierung? Spring zu Teil 2 für Kommunikations-Patterns.
Läuft bereits in der Produktion? Check Teil 3 für Optimierungs-Strategien.
Warum Mobile Micro Frontends?#
Traditionelle mobile App-Entwicklung hat ein grundlegendes Problem: Native Code-Deployment ist langsam. App Store-Überprüfungen, Benutzer-Update-Adoption und die Koordination von Releases zwischen mehreren Teams schaffen Engpässe, die Web-Entwickler bereits vor Jahren gelöst haben.
Unsere spezifischen Einschränkungen waren:
- 5 verschiedene Web-Teams mit bestehenden React/Vue/Angular-Anwendungen
- Wöchentliche Web-Deployments vs. monatliche Mobile-Releases
- A/B-Test-Anforderungen, die nicht auf App-Updates warten konnten
- Compliance-Features, die sofortige Deployment-Fähigkeit benötigten
Die Lösung? Webbasierte Micro Frontends in unserer React Native App mit WebViews einbetten.
Architektur-Überblick#
Hier ist die High-Level-Architektur, die wir implementiert haben:
Loading diagram...
Die native App fungiert als Shell, die:
- Authentifizierung und Session-Management übernimmt
- Native Funktionalität bereitstellt (Kamera, Biometrie, etc.)
- Navigation zwischen Micro Frontends verwaltet
- Eine Kommunikations-Bridge zwischen WebViews und nativem Code implementiert
Alternative Ansätze: Jenseits traditioneller WebViews#
Bevor ich in unsere WebView-Implementierung eintauche, lass mich die alternativen Ansätze teilen, die wir evaluiert haben und warum wir WebViews anstelle von ihnen gewählt haben.
Option 1: Re.Pack mit Module Federation#
Re.Pack ist Callstack's Lösung, um Module Federation zu React Native zu bringen. Es ist im Wesentlichen webpack's Module Federation, die in React Native läuft.
Was wir ausprobierten:
// Re.Pack Konfiguration
const { ModuleFederationPlugin } = require('@module-federation/nextjs-mf');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
booking: 'booking@http://localhost:3001/remoteEntry.js',
shopping: 'shopping@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
Warum wir es nicht gewählt haben:
- Komplexität: Benötigte bedeutende Änderungen an unseren bestehenden webpack-Konfigurationen
- Team-Koordination: Alle Teams mussten gleichzeitig Re.Pack adoptieren
- Debugging: Module Federation-Debugging in React Native war noch unreif
- Performance: Initiale Bundle-Größe stieg um 40% durch Federation-Overhead
Wann Re.Pack verwenden:
- Du fängst frisch mit einem neuen Projekt an
- Alle Teams können sich auf denselben Bundler koordinieren
- Du brauchst echtes Runtime-Modul-Sharing
- Du baust eine "Super App" mit mehreren unabhängigen Teams
Option 2: Rspack mit React Native#
Rspack ist ein Rust-basierter Bundler, der webpack-kompatibel aber viel schneller ist. Wir experimentierten mit Rspack für unsere Micro Frontend-Builds.
Was wir ausprobierten:
// rspack.config.mjs
export default {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx$/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true
},
transform: {
react: {
runtime: 'automatic'
}
}
}
}
}
}
]
},
plugins: [
new ModuleFederationPlugin({
name: 'micro-frontend',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App.tsx'
}
})
]
};
Warum wir es nicht gewählt haben:
- React Native-Kompatibilität: Rspack hat noch keine native React Native-Unterstützung
- Ökosystem-Reife: Weniger Plugins und Loader im Vergleich zu webpack
- Team-Adoption: Hätte bedeutende Umschulung von 5 Teams erfordert
- Production-Stabilität: Wir konnten unser Zeitlimit nicht mit einem neueren Tool riskieren
Wann Rspack verwenden:
- Du baust reine Web-Micro-Frontends
- Build-Performance ist kritisch (Rspack ist 10x schneller als webpack)
- Du kannst es dir leisten, ein Early Adopter zu sein
- Deine Teams sind mit Rust-basierten Tools vertraut
Option 3: Vite + React Native#
Wir evaluierten auch die Verwendung von Vite für unsere Micro Frontend-Builds, um dessen schnelles HMR und modernes Build-System zu nutzen.
Was wir ausprobierten:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns']
}
}
}
},
server: {
cors: true
}
});
Warum wir es nicht gewählt haben:
- Module Federation: Vite's Module Federation-Unterstützung war noch experimentell
- Production-Builds: Unsere Production-Builds waren langsamer als webpack
- Plugin-Ökosystem: Weniger Plugins für unsere spezifischen Bedürfnisse
- Team-Vertrautheit: Teams waren mehr mit webpack vertraut
Wann Vite verwenden:
- Du baust moderne Web-Anwendungen
- Entwicklungsgeschwindigkeit ist wichtiger als Production-Optimierung
- Du brauchst keine komplexe Module Federation
- Deine Teams bevorzugen moderne Tooling
Option 4: Hybrid-Ansatz (Was wir tatsächlich gemacht haben)#
Nach der Evaluation aller Optionen wählten wir einen Hybrid-Ansatz, der uns das Beste aller Welten gab:
Loading diagram...
Warum das funktionierte:
- Team-Autonomie: Jedes Team konnte seinen bevorzugten Bundler verwenden
- Schrittweise Migration: Teams konnten im Laufe der Zeit zu besseren Tools migrieren
- Risiko-Minderung: Wenn ein Ansatz fehlschlug, arbeiteten andere weiter
- Performance: Jedes Team konnte seine eigenen Builds optimieren
Implementierung: Die wahre Geschichte#
Aufbau der WebView-Architektur#
Wir begannen mit Expo's WebView, stießen aber schnell auf unsere erste Herausforderung. Die anfängliche Implementierung sah täuschend einfach aus:
import React from 'react';
import { WebView } from 'react-native-webview';
import { SafeAreaView } from 'react-native-safe-area-context';
export function MicroFrontendContainer({ url }: { url: string }) {
return (
<SafeAreaView style={{ flex: 1 }}>
<WebView
source={{ uri: url }}
style={{ flex: 1 }}
/>
</SafeAreaView>
);
}
Das funktionierte genau 5 Minuten in der Entwicklung, bevor wir unser erstes Production-Problem trafen: Die WebView zeigte zufällig einen weißen Bildschirm auf Android-Geräten mit weniger als 4GB RAM.
Das Speicherproblem#
Nach dem Hinzufügen von Crash-Reporting entdeckten wir, dass WebViews jeweils 150-200MB verbrauchten. Mit 3 geladenen Micro Frontends erreichten wir Speicherlimits auf Low-End-Geräten. So lösten wir es:
import React, { useRef, useCallback, useState } from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { AppState, AppStateStatus } from 'react-native';
interface MicroFrontendConfig {
url: string;
preload?: boolean;
cachePolicy?: 'default' | 'reload' | 'cache-else-load';
}
class WebViewManager {
private static instance: WebViewManager;
private activeWebViews = new Map<string, {
ref: React.RefObject<WebView>;
lastUsed: number;
memoryUsage: number;
}>();
private maxWebViews = 3;
private maxMemoryPerWebView = 100 * 1024 * 1024; // 100MB
static getInstance(): WebViewManager {
if (!WebViewManager.instance) {
WebViewManager.instance = new WebViewManager();
}
return WebViewManager.instance;
}
registerWebView(id: string, ref: React.RefObject<WebView>): void {
this.activeWebViews.set(id, {
ref,
lastUsed: Date.now(),
memoryUsage: 0
});
// Aufräumen wenn wir Limits überschreiten
this.cleanupIfNeeded();
}
private cleanupIfNeeded(): void {
if (this.activeWebViews.size <= this.maxWebViews) return;
// Finde am wenigsten kürzlich verwendete WebView
const entries = Array.from(this.activeWebViews.entries());
const lru = entries.reduce((min, current) =>
current[1].lastUsed < min[1].lastUsed ? current : min
);
// Räume die WebView auf
lru[1].ref.current?.clearCache();
this.activeWebViews.delete(lru[0]);
console.log(`[WebViewManager] WebView ${lru[0]} wegen Speicherlimits aufgeräumt`);
}
updateUsage(id: string): void {
const webView = this.activeWebViews.get(id);
if (webView) {
webView.lastUsed = Date.now();
}
}
}
export function OptimizedMicroFrontendContainer({
config
}: {
config: MicroFrontendConfig
}) {
const webViewRef = useRef<WebView>(null);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const webViewManager = WebViewManager.getInstance();
const handleLoadStart = useCallback(() => {
setIsLoading(true);
setHasError(false);
}, []);
const handleLoadEnd = useCallback(() => {
setIsLoading(false);
webViewManager.updateUsage(config.url);
}, [config.url]);
const handleError = useCallback((error: any) => {
console.error('[WebView] Ladefehler:', error);
setHasError(true);
setIsLoading(false);
}, []);
const handleMessage = useCallback((event: WebViewMessageEvent) => {
// Bridge-Nachrichten behandeln (in Teil 2 behandelt)
console.log('[WebView] Nachricht erhalten:', event.nativeEvent.data);
}, []);
// Bei Manager registrieren
React.useEffect(() => {
webViewManager.registerWebView(config.url, webViewRef);
}, [config.url]);
// App-State-Änderungen behandeln
React.useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'background') {
// Cache löschen wenn App in den Hintergrund geht
webViewRef.current?.clearCache();
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription?.remove();
}, []);
if (hasError) {
return (
<SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Micro Frontend konnte nicht geladen werden</Text>
<Button title="Wiederholen" onPress={() => setHasError(false)} />
</SafeAreaView>
);
}
return (
<SafeAreaView style={{ flex: 1 }}>
{isLoading && (
<View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000
}}>
<ActivityIndicator size="large" />
</View>
)}
<WebView
ref={webViewRef}
source={{
uri: config.url,
headers: {
'X-Platform': 'react-native',
'X-App-Version': '1.0.0'
}
}}
style={{ flex: 1 }}
onLoadStart={handleLoadStart}
onLoadEnd={handleLoadEnd}
onError={handleError}
onMessage={handleMessage}
cacheEnabled={config.cachePolicy !== 'reload'}
cacheMode={config.cachePolicy === 'cache-else-load' ? 'LOAD_CACHE_ELSE_NETWORK' : 'LOAD_DEFAULT'}
// Kritisch für Performance
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
// Sicherheit
allowsInlineMediaPlayback={false}
mediaPlaybackRequiresUserAction={true}
// Performance
removeClippedSubviews={true}
overScrollMode="never"
/>
</SafeAreaView>
);
}
Das Bundle-Größen-Problem#
Unsere anfänglichen Micro Frontend-Bundles waren massiv - jeweils 2-3MB. Das verursachte langsame Ladezeiten und schlechte Benutzererfahrung. So optimierten wir:
// webpack.config.js - Optimiert für WebView-Bereitstellung
const { ModuleFederationPlugin } = require('webpack').container;
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\\\|\/]/node_modules[\\\\|\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5
}
}
},
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Console.logs entfernen
drop_debugger: true
},
mangle: {
safari10: true // Safari 10 Probleme beheben
}
}
})
]
},
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
}),
new ModuleFederationPlugin({
name: 'micro-frontend',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App.tsx'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};
Alternative: Rspack-Konfiguration#
Für Teams, die Rspack ausprobieren wollten, hier die äquivalente Konfiguration:
// rspack.config.mjs - Rspack-Version
export default {
entry: './src/index.tsx',
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\\\|\/]/node_modules[\\\\|\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
}
}
}
},
module: {
rules: [
{
test: /\.tsx$/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true
},
transform: {
react: {
runtime: 'automatic'
}
},
minify: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
}
}
}
]
},
plugins: [
new ModuleFederationPlugin({
name: 'micro-frontend',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App.tsx'
}
})
]
};
Performance-Vergleich:
- Webpack Build-Zeit: 45 Sekunden
- Rspack Build-Zeit: 8 Sekunden (82% schneller)
- Bundle-Größe: Ähnlich (innerhalb von 5%)
- Speichernutzung: Rspack verwendete 30% weniger Speicher während der Builds
Das Navigations-Problem#
WebView-Navigation funktioniert nicht wie native Navigation. Benutzer erwarten, dass der Zurück-Button funktioniert, aber WebViews haben ihre eigene History. Hier unsere Lösung:
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { BackHandler } from 'react-native';
export function WebViewWithNavigation({
webViewRef,
canGoBack,
onGoBack
}: {
webViewRef: React.RefObject<WebView>;
canGoBack: boolean;
onGoBack: () => void;
}) {
const navigation = useNavigation();
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
if (canGoBack) {
// Versuche zuerst in WebView zurückzugehen
webViewRef.current?.goBack();
return true; // Verhindere Standard-Zurück-Verhalten
} else {
// Lasse native Navigation damit umgehen
onGoBack();
return false;
}
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [canGoBack, onGoBack])
);
return null;
}
Performance-Ergebnisse#
Nach der Implementierung dieser Optimierungen verbesserten sich unsere Metriken dramatisch:
Lade-Performance#
- Initiale Ladezeit: 2.3s → 0.8s (65% Verbesserung)
- Bundle-Größe: 2.8MB → 1.2MB (57% Reduzierung)
- Speichernutzung: 200MB → 120MB pro WebView (40% Reduzierung)
Benutzererfahrung#
- App-Abstürze: 2.3% → 0.1% (96% Reduzierung)
- Weiße-Bildschirm-Vorfälle: 15% → 1% (93% Reduzierung)
- Benutzer-Zufriedenheitswert: 3.2 → 4.6 (44% Verbesserung)
Build-Performance#
- Webpack-Builds: 45s → 35s (22% schneller)
- Rspack-Builds: 8s (82% schneller als webpack)
- Entwicklungs-HMR: 2s → 0.3s (85% schneller)
Wichtige Erkenntnisse#
-
WebViews sind machbar, benötigen aber Optimierung: Sie funktionieren gut für Micro Frontends, benötigen aber sorgfältiges Speicher- und Performance-Management.
-
Hybrid-Ansätze funktionieren am besten: Lass Teams ihre bevorzugten Tools verwenden, während eine konsistente Schnittstelle beibehalten wird.
-
Rspack zeigt Potenzial: Für neue Projekte solltest du Rspack wegen seiner Geschwindigkeit und webpack-Kompatibilität in Betracht ziehen.
-
Re.Pack ist mächtig aber komplex: Großartig für Super Apps, benötigt aber erhebliche Koordination.
-
Performance ist kritisch: User bemerken langsame WebViews sofort. Optimiere aggressiv.
Was kommt als Nächstes?#
In Teil 2 tauchen wir ein in:
- Aufbau einer robusten Kommunikations-Bridge zwischen WebViews und nativem Code
- Type-safe Nachrichten-Passing
- Umgang mit Authentifizierung und nativen Features
- Debugging- und Monitoring-Strategien
Das Fundament ist entscheidend. Baust du die Architektur richtig auf, und alles andere wird handhabbar. Baust du sie falsch auf, und du wirst für immer gegen Performance-Probleme kämpfen.
Nächstes Mal schauen wir uns an, wie WebViews und nativer Code zuverlässig miteinander kommunizieren können, und die Debugging-Albträume, die wir dabei gelöst haben.
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!