Multi-Channel Micro Frontends: Production Optimization and Cross-Platform Rendering
Advanced patterns for deploying micro frontends across mobile, web, and desktop. Performance optimization strategies, offline support, and production war stories from scaling to 10M+ daily interactions. Includes Rspack, Re.Pack, and alternative bundler approaches.
Six months after launching our mobile micro frontend architecture, we faced a new challenge: our client wanted the same experience on web and desktop. "Can't you just reuse the same micro frontends?" they asked. "They're already built for web."
That innocent question led to 4 months of refactoring, performance hunting, and some of the most creative engineering solutions I've ever implemented. By the end, we had a single micro frontend codebase serving mobile apps, web browsers, and Electron desktop applications, processing over 10 million daily interactions.
Here's what we learned about truly multi-channel micro frontends and the production optimizations that made it possible.
Mobile Micro Frontend Series#
This is Part 3 (final) of our mobile micro frontends series:
- Part 1: Architecture fundamentals and WebView integration patterns
- Part 2: WebView communication patterns and service integration
- Part 3 (You are here): Multi-channel architecture and production optimization
Starting fresh? Begin with Part 1 for fundamentals.
Need communication patterns? Check Part 2 first.
Alternative Multi-Channel Approaches#
Before diving into our multi-channel solution, let me share the alternative approaches we evaluated and why we chose our current architecture.
Option 1: Re.Pack Multi-Channel Architecture#
Re.Pack offers a unified approach for React Native, web, and desktop through Module Federation:
What we experimented with:
// Re.Pack multi-channel setup
// webpack.config.js (shared config)
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 }
}
})
]
};
// Platform abstraction layer
// @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;
};
}
// Platform-specific implementations
export class ReactNativePlatform implements PlatformAPI {
// React Native implementation
}
export class WebPlatform implements PlatformAPI {
// Web implementation
}
export class ElectronPlatform implements PlatformAPI {
// Electron implementation
}
Why we didn't choose it:
- Complexity: Required significant refactoring of existing apps
- Team coordination: All teams needed to adopt simultaneously
- Performance: Module Federation overhead on mobile
- Debugging: Complex stack traces across platforms
When to use Re.Pack multi-channel:
- You're building a new super app from scratch
- All teams can coordinate on shared architecture
- You need true code sharing across platforms
- Performance overhead is acceptable
Option 2: Rspack Multi-Channel Builds#
We experimented with using Rspack for multi-channel builds:
// rspack.config.mjs - Multi-channel configuration
export default {
entry: './src/index.tsx',
target: ['web', 'electron-renderer'], // Multiple targets
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'
}
})
]
};
// Platform detection
const platform = process.env.PLATFORM || 'web';
// Conditional imports based on platform
const platformAPI = platform === 'react-native'
? require('./platforms/react-native')
: platform === 'electron'
? require('./platforms/electron')
: require('./platforms/web');
Why we didn't choose it:
- React Native compatibility: Rspack doesn't support React Native yet
- Build complexity: Multiple target builds were complex
- Ecosystem maturity: Fewer examples and documentation
- Team adoption: Would require significant retraining
When to use Rspack multi-channel:
- You're building web and desktop only
- Build performance is critical
- You can afford to be an early adopter
- Your teams are comfortable with Rust-based tooling
Option 3: Vite Multi-Channel Architecture#
We also evaluated Vite for multi-channel builds:
// vite.config.ts - Multi-channel setup
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const platform = process.env.PLATFORM || 'web';
return {
plugins: [react()],
build: {
rollupOptions: {
external: platform === 'electron' ? ['electron'] : [],
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
platform: [`./src/platforms/${platform}`]
}
}
}
},
define: {
__PLATFORM__: JSON.stringify(platform)
}
};
});
// Platform-specific entry points
// src/platforms/web/index.ts
export class WebPlatform {
// Web-specific implementation
}
// src/platforms/electron/index.ts
export class ElectronPlatform {
// Electron-specific implementation
}
Why we didn't choose it:
- Module Federation: Vite's Module Federation was still experimental
- Production builds: Slower than webpack for our use case
- Plugin ecosystem: Fewer plugins for our specific needs
- Team familiarity: Teams were more comfortable with webpack
When to use Vite multi-channel:
- You're building modern web applications
- Development speed is more important than production optimization
- You don't need complex Module Federation
- Your teams prefer modern tooling
Option 4: Hybrid Approach (What We Actually Did)#
After evaluating all options, we chose a hybrid approach that gave us the best of all worlds:
Loading diagram...
Why this worked:
- Team autonomy: Each team could use their preferred bundler
- Gradual migration: Teams could migrate to better tools over time
- Risk mitigation: If one approach failed, others continued working
- Performance: Each team could optimize their own builds
The Multi-Channel Challenge#
Our original mobile architecture worked great for React Native WebViews, but extending it to web and desktop revealed several problems:
- Performance: Desktop users expected native-level performance
- Navigation: Browser back button vs native navigation
- Authentication: Different security models across platforms
- Offline: Mobile needs work offline, web traditionally doesn't
- Platform APIs: Camera works differently everywhere
Multi-Channel Architecture Overview#
Here's the architecture we evolved to handle all platforms:
Loading diagram...
Platform Detection and Adaptation#
The first challenge was detecting the runtime environment reliably:
// Platform detection that actually works
export enum Platform {
REACT_NATIVE_IOS = 'react-native-ios',
REACT_NATIVE_ANDROID = 'react-native-android',
WEB_BROWSER = 'web-browser',
ELECTRON_DESKTOP = 'electron-desktop',
UNKNOWN = 'unknown'
}
class PlatformDetector {
private static _platform?: Platform;
static get platform(): Platform {
if (this._platform) return this._platform;
// Check for React Native first
if (typeof navigator !== 'undefined' &&
navigator.product === 'ReactNative') {
// We're in a React Native WebView
if (window.ReactNativeWebView) {
// Detect iOS vs Android through user agent
const userAgent = navigator.userAgent;
this._platform = userAgent.includes('iPhone') || userAgent.includes('iPad')
? Platform.REACT_NATIVE_IOS
: Platform.REACT_NATIVE_ANDROID;
} else {
this._platform = Platform.UNKNOWN;
}
}
// Check for Electron
else if (typeof window !== 'undefined' &&
window.process?.type === 'renderer') {
this._platform = Platform.ELECTRON_DESKTOP;
}
// Must be web browser
else if (typeof window !== 'undefined') {
this._platform = Platform.WEB_BROWSER;
}
else {
this._platform = Platform.UNKNOWN;
}
return this._platform;
}
static isMobile(): boolean {
return this.platform === Platform.REACT_NATIVE_IOS ||
this.platform === Platform.REACT_NATIVE_ANDROID;
}
static isWeb(): boolean {
return this.platform === Platform.WEB_BROWSER;
}
static isDesktop(): boolean {
return this.platform === Platform.ELECTRON_DESKTOP;
}
static hasNativeBridge(): boolean {
return this.isMobile() || this.isDesktop();
}
}
Universal Bridge Architecture#
The key insight was creating a unified bridge interface that could work across all platforms:
// Universal bridge interface
export interface UniversalBridge {
// Core messaging
request<T>(action: string, payload?: any): Promise<T>;
emit(action: string, payload?: any): void;
on(action: string, handler: (payload: any) => void): () => void;
// Platform capabilities
getCapabilities(): PlatformCapabilities;
isSupported(feature: string): boolean;
}
export interface PlatformCapabilities {
hasCamera: boolean;
hasBiometrics: boolean;
hasFileSystem: boolean;
hasNotifications: boolean;
canGoBack: boolean;
hasClipboard: boolean;
canDownload: boolean;
hasNativeNavigation: boolean;
}
// Platform-specific implementations
class ReactNativeBridge implements UniversalBridge {
private bridge: WebViewBridge;
constructor() {
this.bridge = window.bridge; // From Part 2
}
async request<T>(action: string, payload?: any): Promise<T> {
return this.bridge.request(action, payload);
}
emit(action: string, payload?: any): void {
this.bridge.emit(action, payload);
}
on(action: string, handler: (payload: any) => void): () => void {
return this.bridge.on(action, handler);
}
getCapabilities(): PlatformCapabilities {
return {
hasCamera: true,
hasBiometrics: true,
hasFileSystem: true,
hasNotifications: true,
canGoBack: true,
hasClipboard: true,
canDownload: true,
hasNativeNavigation: true
};
}
isSupported(feature: string): boolean {
return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
}
}
class WebBridge implements UniversalBridge {
private eventEmitter = new EventTarget();
async request<T>(action: string, payload?: any): Promise<T> {
// Simulate native features using web APIs
switch (action) {
case 'camera.takePhoto':
return this.handleWebCamera(payload) as Promise<T>;
case 'auth.getToken':
return this.handleWebAuth() as Promise<T>;
case 'navigation.goBack':
return this.handleWebNavigation() as Promise<T>;
default:
throw new Error(`Unsupported action on web: ${action}`);
}
}
emit(action: string, payload?: any): void {
this.eventEmitter.dispatchEvent(
new CustomEvent(action, { detail: payload })
);
}
on(action: string, handler: (payload: any) => void): () => void {
const listener = (event: any) => handler(event.detail);
this.eventEmitter.addEventListener(action, listener);
return () => {
this.eventEmitter.removeEventListener(action, listener);
};
}
private async handleWebCamera(options: any): Promise<any> {
// Use HTML5 camera API
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1920 },
height: { ideal: 1080 }
}
});
return new Promise((resolve, reject) => {
const video = document.createElement('video');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
video.srcObject = stream;
video.play();
video.onloadedmetadata = () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Simulate camera capture UI
const captureButton = document.createElement('button');
captureButton.textContent = 'Capture';
captureButton.onclick = () => {
ctx.drawImage(video, 0, 0);
const dataUri = canvas.toDataURL('image/jpeg', options?.quality || 0.8);
stream.getTracks().forEach(track => track.stop());
document.body.removeChild(video);
document.body.removeChild(captureButton);
resolve({
uri: dataUri,
width: canvas.width,
height: canvas.height
});
};
document.body.appendChild(video);
document.body.appendChild(captureButton);
};
});
}
private async handleWebAuth(): Promise<any> {
// Use localStorage/sessionStorage for web auth
const token = localStorage.getItem('auth_token');
if (!token) {
throw new Error('No authentication token');
}
return {
token,
expiresAt: Date.now() + 3600000 // 1 hour
};
}
private async handleWebNavigation(): Promise<boolean> {
if (window.history.length > 1) {
window.history.back();
return true;
}
return false;
}
getCapabilities(): PlatformCapabilities {
return {
hasCamera: !!navigator.mediaDevices?.getUserMedia,
hasBiometrics: false, // Not available in browsers
hasFileSystem: false, // Limited in browsers
hasNotifications: 'Notification' in window,
canGoBack: true,
hasClipboard: !!navigator.clipboard,
canDownload: true,
hasNativeNavigation: false
};
}
isSupported(feature: string): boolean {
return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
}
}
class ElectronBridge implements UniversalBridge {
async request<T>(action: string, payload?: any): Promise<T> {
// Use Electron's IPC to communicate with main process
return window.electronAPI.invoke(action, payload);
}
emit(action: string, payload?: any): void {
window.electronAPI.send(action, payload);
}
on(action: string, handler: (payload: any) => void): () => void {
const listener = (_: any, ...args: any[]) => handler(...args);
window.electronAPI.on(action, listener);
return () => {
window.electronAPI.removeListener(action, listener);
};
}
getCapabilities(): PlatformCapabilities {
return {
hasCamera: true,
hasBiometrics: false, // Would need native module
hasFileSystem: true,
hasNotifications: true,
canGoBack: true,
hasClipboard: true,
canDownload: true,
hasNativeNavigation: true
};
}
isSupported(feature: string): boolean {
return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
}
}
// Bridge factory
export class BridgeFactory {
static create(): UniversalBridge {
const platform = PlatformDetector.platform;
switch (platform) {
case Platform.REACT_NATIVE_IOS:
case Platform.REACT_NATIVE_ANDROID:
return new ReactNativeBridge();
case Platform.ELECTRON_DESKTOP:
return new ElectronBridge();
case Platform.WEB_BROWSER:
return new WebBridge();
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
}
Adaptive UI Components#
Different platforms needed different UX patterns. We built adaptive components:
// Adaptive button component
interface AdaptiveButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export function AdaptiveButton({
title,
onPress,
variant = 'primary',
disabled = false
}: AdaptiveButtonProps) {
const platform = PlatformDetector.platform;
// Platform-specific styles
const getButtonStyles = () => {
const baseStyles = 'px-4 py-2 rounded font-medium transition-colors';
if (disabled) {
return `${baseStyles} bg-gray-300 text-gray-500 cursor-not-allowed`;
}
const variantStyles = variant === 'primary'
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-200 text-gray-900 hover:bg-gray-300';
// Platform-specific modifications
switch (platform) {
case Platform.REACT_NATIVE_IOS:
// iOS-style button
return `${baseStyles} ${variantStyles} shadow-sm`;
case Platform.REACT_NATIVE_ANDROID:
// Material Design style
return `${baseStyles} ${variantStyles} shadow-md elevation-2`;
case Platform.WEB_BROWSER:
// Web-optimized
return `${baseStyles} ${variantStyles} focus:outline-none focus:ring-2 focus:ring-blue-500`;
case Platform.ELECTRON_DESKTOP:
// Desktop app style
return `${baseStyles} ${variantStyles} text-sm`;
default:
return `${baseStyles} ${variantStyles}`;
}
};
const handleClick = () => {
if (disabled) return;
// Add haptic feedback on mobile
if (PlatformDetector.isMobile()) {
bridge.emit('haptic.impact', { style: 'medium' });
}
onPress();
};
return (
<button
className={getButtonStyles()}
onClick={handleClick}
disabled={disabled}
// Accessibility
role="button"
aria-label={title}
>
{title}
</button>
);
}
// Adaptive layout components
export function AdaptiveLayout({ children }: { children: React.ReactNode }) {
const platform = PlatformDetector.platform;
const getLayoutClass = () => {
switch (platform) {
case Platform.REACT_NATIVE_IOS:
case Platform.REACT_NATIVE_ANDROID:
// Mobile: full height, safe areas
return 'min-h-screen pt-safe pb-safe px-4';
case Platform.WEB_BROWSER:
// Web: responsive, max width
return 'min-h-screen max-w-4xl mx-auto px-4 py-8';
case Platform.ELECTRON_DESKTOP:
// Desktop: compact, window-aware
return 'h-screen p-6 overflow-auto';
default:
return 'min-h-screen p-4';
}
};
return (
<div className={getLayoutClass()}>
{children}
</div>
);
}
Performance Optimization Strategies#
Running the same codebase across all platforms revealed performance bottlenecks that weren't obvious in single-platform development:
Code Splitting by Platform#
// Platform-aware dynamic imports
class PlatformModuleLoader {
private moduleCache = new Map<string, any>();
async loadPlatformModule<T>(moduleName: string): Promise<T> {
const cacheKey = `${moduleName}-${PlatformDetector.platform}`;
if (this.moduleCache.has(cacheKey)) {
return this.moduleCache.get(cacheKey);
}
let module: T;
try {
// Try platform-specific module first
const platformModule = await this.loadPlatformSpecific<T>(moduleName);
module = platformModule;
} catch (error) {
// Fall back to generic module
console.warn(`Platform module ${moduleName} not found, using generic`);
module = await this.loadGeneric<T>(moduleName);
}
this.moduleCache.set(cacheKey, module);
return module;
}
private async loadPlatformSpecific<T>(moduleName: string): Promise<T> {
const platform = PlatformDetector.platform;
switch (platform) {
case Platform.REACT_NATIVE_IOS:
return import(`./modules/${moduleName}/ios`);
case Platform.REACT_NATIVE_ANDROID:
return import(`./modules/${moduleName}/android`);
case Platform.WEB_BROWSER:
return import(`./modules/${moduleName}/web`);
case Platform.ELECTRON_DESKTOP:
return import(`./modules/${moduleName}/electron`);
default:
throw new Error(`No platform-specific module for ${platform}`);
}
}
private async loadGeneric<T>(moduleName: string): Promise<T> {
return import(`./modules/${moduleName}/index`);
}
}
// Usage
const moduleLoader = new PlatformModuleLoader();
async function initializeApp() {
// Load platform-optimized modules
const analytics = await moduleLoader.loadPlatformModule('analytics');
const storage = await moduleLoader.loadPlatformModule('storage');
const networking = await moduleLoader.loadPlatformModule('networking');
// Initialize with platform-specific implementations
await analytics.initialize();
await storage.initialize();
await networking.initialize();
}
Memory Management Across Platforms#
Different platforms had very different memory constraints:
// Adaptive memory management
class MemoryManager {
private memoryLimit: number;
private memoryWarningThreshold: number;
private cache = new Map<string, { data: any; timestamp: number; size: number }>();
private totalCacheSize = 0;
constructor() {
// Set limits based on platform
const platform = PlatformDetector.platform;
switch (platform) {
case Platform.REACT_NATIVE_IOS:
case Platform.REACT_NATIVE_ANDROID:
// Mobile: conservative limits
this.memoryLimit = 50 * 1024 * 1024; // 50MB
this.memoryWarningThreshold = 40 * 1024 * 1024; // 40MB
break;
case Platform.WEB_BROWSER:
// Web: moderate limits
this.memoryLimit = 100 * 1024 * 1024; // 100MB
this.memoryWarningThreshold = 80 * 1024 * 1024; // 80MB
break;
case Platform.ELECTRON_DESKTOP:
// Desktop: generous limits
this.memoryLimit = 200 * 1024 * 1024; // 200MB
this.memoryWarningThreshold = 150 * 1024 * 1024; // 150MB
break;
default:
this.memoryLimit = 50 * 1024 * 1024;
this.memoryWarningThreshold = 40 * 1024 * 1024;
}
// Set up memory pressure monitoring
this.setupMemoryMonitoring();
}
set(key: string, data: any): void {
const serialized = JSON.stringify(data);
const size = new Blob([serialized]).size;
// Check if we need to free memory first
if (this.totalCacheSize + size > this.memoryLimit) {
this.freeMemory(size);
}
this.cache.set(key, {
data,
timestamp: Date.now(),
size
});
this.totalCacheSize += size;
}
get(key: string): any {
const item = this.cache.get(key);
if (!item) return null;
// Update timestamp for LRU
item.timestamp = Date.now();
return item.data;
}
private freeMemory(requiredSize: number): void {
// Sort by timestamp (LRU)
const entries = Array.from(this.cache.entries())
.sort(([, a], [, b]) => a.timestamp - b.timestamp);
let freed = 0;
for (const [key, item] of entries) {
this.cache.delete(key);
this.totalCacheSize -= item.size;
freed += item.size;
if (freed >= requiredSize || this.totalCacheSize < this.memoryWarningThreshold) {
break;
}
}
console.log(`[MemoryManager] Freed ${freed} bytes`);
}
private setupMemoryMonitoring(): void {
// Monitor memory usage every 30 seconds
setInterval(() => {
if (this.totalCacheSize > this.memoryWarningThreshold) {
console.warn(`[MemoryManager] Memory usage high: ${this.totalCacheSize} bytes`);
// Free 25% of cache
this.freeMemory(this.totalCacheSize * 0.25);
}
}, 30000);
// React to platform-specific memory warnings
if (PlatformDetector.isMobile()) {
// Mobile apps can receive memory warnings
bridge.on('memory.warning', () => {
console.warn('[MemoryManager] Received memory warning, clearing cache');
this.clearAll();
});
}
}
clearAll(): void {
this.cache.clear();
this.totalCacheSize = 0;
}
getStats(): {
totalSize: number;
itemCount: number;
limit: number;
utilizationPercent: number;
} {
return {
totalSize: this.totalCacheSize,
itemCount: this.cache.size,
limit: this.memoryLimit,
utilizationPercent: (this.totalCacheSize / this.memoryLimit) * 100
};
}
}
Network Optimization#
Network behavior varied significantly across platforms:
// Adaptive networking strategy
class NetworkManager {
private retryDelays: number[];
private timeoutMs: number;
private maxConcurrentRequests: number;
private activeRequests = new Set<string>();
private requestQueue: Array<() => Promise<any>> = [];
constructor() {
const platform = PlatformDetector.platform;
switch (platform) {
case Platform.REACT_NATIVE_IOS:
case Platform.REACT_NATIVE_ANDROID:
// Mobile: conservative settings
this.retryDelays = [1000, 2000, 5000];
this.timeoutMs = 15000;
this.maxConcurrentRequests = 3;
break;
case Platform.WEB_BROWSER:
// Web: moderate settings
this.retryDelays = [500, 1000, 2000];
this.timeoutMs = 10000;
this.maxConcurrentRequests = 6;
break;
case Platform.ELECTRON_DESKTOP:
// Desktop: aggressive settings
this.retryDelays = [200, 500, 1000];
this.timeoutMs = 5000;
this.maxConcurrentRequests = 10;
break;
default:
this.retryDelays = [1000, 2000, 5000];
this.timeoutMs = 10000;
this.maxConcurrentRequests = 3;
}
}
async request(url: string, options: RequestInit = {}): Promise<Response> {
// Implement request queuing for mobile
if (PlatformDetector.isMobile() &&
this.activeRequests.size >= this.maxConcurrentRequests) {
return this.queueRequest(() => this.executeRequest(url, options));
}
return this.executeRequest(url, options);
}
private async executeRequest(url: string, options: RequestInit): Promise<Response> {
const requestId = `${url}-${Date.now()}`;
this.activeRequests.add(requestId);
try {
return await this.retryRequest(url, options);
} finally {
this.activeRequests.delete(requestId);
this.processQueue();
}
}
private async retryRequest(url: string, options: RequestInit): Promise<Response> {
let lastError: Error;
for (let attempt = 0; attempt <= this.retryDelays.length; attempt++) {
try {
// Add timeout to request
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
// Don't retry on 4xx errors (except 429)
if (response.status >= 400 && response.status <500 && response.status !== 429) {
return response;
}
if (response.ok || response.status === 429) {
return response;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
lastError = error as Error;
// Don't retry on abort
if (error.name === 'AbortError') {
throw error;
}
// Wait before retrying
if (attempt < this.retryDelays.length) {
console.warn(`Request attempt ${attempt + 1} failed, retrying in ${this.retryDelays[attempt]}ms`);
await this.delay(this.retryDelays[attempt]);
}
}
}
throw lastError!;
}
private async queueRequest<T>(requestFn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.requestQueue.push(async () => {
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
});
});
}
private processQueue(): void {
while (this.requestQueue.length > 0 &&
this.activeRequests.size < this.maxConcurrentRequests) {
const nextRequest = this.requestQueue.shift();
nextRequest?.();
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Offline Support and Synchronization#
One of the biggest challenges was implementing offline support that worked across all platforms:
// Universal offline support
class OfflineManager {
private isOnline = navigator.onLine;
private syncQueue: Array<{
id: string;
action: string;
payload: any;
timestamp: number;
attempts: number;
}> = [];
private storage: Storage;
private maxRetries = 5;
constructor() {
// Use platform-appropriate storage
this.storage = PlatformDetector.isMobile()
? this.createMobileStorage()
: localStorage;
this.setupNetworkMonitoring();
this.loadQueueFromStorage();
}
private createMobileStorage(): Storage {
// In React Native WebView, we need to use the bridge for persistent storage
return {
getItem: async (key: string): Promise<string | null> => {
try {
return await bridge.request('storage.getItem', { key });
} catch {
return null;
}
},
setItem: async (key: string, value: string): Promise<void> => {
await bridge.request('storage.setItem', { key, value });
},
removeItem: async (key: string): Promise<void> => {
await bridge.request('storage.removeItem', { key });
},
clear: async (): Promise<void> => {
await bridge.request('storage.clear');
}
} as any;
}
private setupNetworkMonitoring(): void {
// Web/Desktop network monitoring
if (typeof window !== 'undefined') {
window.addEventListener('online', () => {
this.isOnline = true;
this.processSyncQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
// Mobile network monitoring through bridge
if (PlatformDetector.isMobile()) {
bridge.on('network.statusChanged', ({ isOnline }: { isOnline: boolean }) => {
this.isOnline = isOnline;
if (isOnline) {
this.processSyncQueue();
}
});
}
}
async queueForSync(action: string, payload: any): Promise<void> {
const queueItem = {
id: this.generateId(),
action,
payload,
timestamp: Date.now(),
attempts: 0
};
this.syncQueue.push(queueItem);
await this.saveQueueToStorage();
// Try to sync immediately if online
if (this.isOnline) {
this.processSyncQueue();
}
}
private async processSyncQueue(): Promise<void> {
if (!this.isOnline || this.syncQueue.length === 0) {
return;
}
const itemsToProcess = [...this.syncQueue];
for (const item of itemsToProcess) {
try {
await this.syncItem(item);
// Remove from queue on success
this.syncQueue = this.syncQueue.filter(q => q.id !== item.id);
} catch (error) {
console.error(`Sync failed for ${item.action}:`, error);
item.attempts++;
// Remove if max retries exceeded
if (item.attempts >= this.maxRetries) {
console.error(`Max retries exceeded for ${item.action}, removing from queue`);
this.syncQueue = this.syncQueue.filter(q => q.id !== item.id);
}
}
}
await this.saveQueueToStorage();
}
private async syncItem(item: any): Promise<void> {
// Send queued action to server
const response = await fetch('/api/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getAuthToken()}`
},
body: JSON.stringify({
action: item.action,
payload: item.payload,
timestamp: item.timestamp
})
});
if (!response.ok) {
throw new Error(`Sync failed: ${response.status}`);
}
}
private async getAuthToken(): Promise<string> {
if (PlatformDetector.hasNativeBridge()) {
const { token } = await bridge.request('auth.getToken');
return token;
} else {
return localStorage.getItem('auth_token') || '';
}
}
private async saveQueueToStorage(): Promise<void> {
const serialized = JSON.stringify(this.syncQueue);
await this.storage.setItem('sync_queue', serialized);
}
private async loadQueueFromStorage(): Promise<void> {
try {
const serialized = await this.storage.getItem('sync_queue');
if (serialized) {
this.syncQueue = JSON.parse(serialized);
}
} catch (error) {
console.error('Failed to load sync queue:', error);
this.syncQueue = [];
}
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Public API
isOnlineState(): boolean {
return this.isOnline;
}
getPendingActions(): number {
return this.syncQueue.length;
}
async forcSync(): Promise<void> {
await this.processSyncQueue();
}
}
Production Monitoring and Analytics#
Monitoring performance across platforms required a sophisticated approach:
// Universal analytics system
class UniversalAnalytics {
private sessionId: string;
private userId?: string;
private deviceInfo: DeviceInfo;
private performanceBuffer: PerformanceEntry[] = [];
constructor() {
this.sessionId = this.generateSessionId();
this.deviceInfo = this.collectDeviceInfo();
this.setupPerformanceMonitoring();
}
private collectDeviceInfo(): DeviceInfo {
const platform = PlatformDetector.platform;
return {
platform,
userAgent: navigator.userAgent,
screenWidth: window.screen?.width || 0,
screenHeight: window.screen?.height || 0,
devicePixelRatio: window.devicePixelRatio || 1,
// Platform-specific info
...this.getPlatformSpecificInfo()
};
}
private getPlatformSpecificInfo(): Partial<DeviceInfo> {
const platform = PlatformDetector.platform;
if (PlatformDetector.isMobile()) {
// Get device info from native bridge
return {
deviceModel: 'mobile-device', // Would come from bridge
osVersion: 'unknown', // Would come from bridge
appVersion: '1.0.0' // Would come from bridge
};
}
return {
browserName: this.getBrowserName(),
browserVersion: this.getBrowserVersion()
};
}
private setupPerformanceMonitoring(): void {
// Monitor Web Vitals
this.observeWebVitals();
// Monitor custom performance metrics
this.observeCustomMetrics();
// Platform-specific monitoring
if (PlatformDetector.isMobile()) {
this.setupMobileMonitoring();
}
}
private observeWebVitals(): void {
// Core Web Vitals
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.trackPerformance(entry.name, entry.duration, {
type: entry.entryType,
startTime: entry.startTime
});
});
});
try {
observer.observe({ entryTypes: ['measure', 'navigation', 'paint'] });
} catch (error) {
console.warn('Performance observer not supported:', error);
}
}
}
private observeCustomMetrics(): void {
// Track micro frontend load times
this.trackMicroFrontendLoads();
// Track bridge message performance
this.trackBridgePerformance();
// Track memory usage
this.trackMemoryUsage();
}
private trackMicroFrontendLoads(): void {
// Monitor when micro frontends become visible
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const loadTime = performance.now();
this.track('micro_frontend_visible', {
microFrontendId: entry.target.id,
loadTime,
visibilityRatio: entry.intersectionRatio
});
}
});
});
// Observe all micro frontend containers
document.querySelectorAll('[data-micro-frontend]').forEach((element) => {
observer.observe(element);
});
}
private trackBridgePerformance(): void {
// Wrap bridge requests to track performance
const originalRequest = bridge.request;
bridge.request = async (action: string, payload: any) => {
const startTime = performance.now();
try {
const result = await originalRequest.call(bridge, action, payload);
const duration = performance.now() - startTime;
this.trackPerformance('bridge_request', duration, {
action,
success: true,
payloadSize: JSON.stringify(payload).length
});
return result;
} catch (error) {
const duration = performance.now() - startTime;
this.trackPerformance('bridge_request', duration, {
action,
success: false,
error: error.message
});
throw error;
}
};
}
private trackMemoryUsage(): void {
if ('memory' in performance) {
setInterval(() => {
const memInfo = (performance as any).memory;
this.track('memory_usage', {
usedJSHeapSize: memInfo.usedJSHeapSize,
totalJSHeapSize: memInfo.totalJSHeapSize,
jsHeapSizeLimit: memInfo.jsHeapSizeLimit,
utilizationPercent: (memInfo.usedJSHeapSize / memInfo.jsHeapSizeLimit) * 100
});
}, 30000); // Every 30 seconds
}
}
private setupMobileMonitoring(): void {
// Track native bridge errors
bridge.on('error', (error: any) => {
this.track('bridge_error', {
error: error.message,
code: error.code,
platform: PlatformDetector.platform
});
});
// Track app state changes
bridge.on('app_state_change', (state: string) => {
this.track('app_state_change', {
state,
sessionDuration: Date.now() - this.getSessionStartTime()
});
});
}
// Public API
track(event: string, properties: Record<string, any> = {}): void {
const eventData = {
event,
properties: {
...properties,
sessionId: this.sessionId,
userId: this.userId,
platform: this.deviceInfo.platform,
timestamp: Date.now()
},
deviceInfo: this.deviceInfo
};
// Send to analytics service
this.sendAnalytics(eventData);
}
trackPerformance(metric: string, value: number, properties: Record<string, any> = {}): void {
this.track('performance_metric', {
metric,
value,
...properties
});
}
setUserId(userId: string): void {
this.userId = userId;
}
private async sendAnalytics(data: any): Promise<void> {
try {
// Use offline manager for reliable delivery
await offlineManager.queueForSync('analytics', data);
} catch (error) {
console.error('Failed to send analytics:', error);
}
}
private generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private getSessionStartTime(): number {
return parseInt(this.sessionId.split('-')[0]);
}
private getBrowserName(): string {
const userAgent = navigator.userAgent;
if (userAgent.includes('Chrome')) return 'Chrome';
if (userAgent.includes('Firefox')) return 'Firefox';
if (userAgent.includes('Safari')) return 'Safari';
if (userAgent.includes('Edge')) return 'Edge';
return 'Unknown';
}
private getBrowserVersion(): string {
// Simplified version detection
const userAgent = navigator.userAgent;
const match = userAgent.match(/(?:Chrome|Firefox|Safari|Edge)\/([0-9.]+)/);
return match ? match[1] : 'Unknown';
}
}
interface DeviceInfo {
platform: Platform;
userAgent: string;
screenWidth: number;
screenHeight: number;
devicePixelRatio: number;
deviceModel?: string;
osVersion?: string;
appVersion?: string;
browserName?: string;
browserVersion?: string;
}
interface PerformanceEntry {
name: string;
duration: number;
startTime: number;
type: string;
}
Production Results and Metrics#
After 6 months running this multi-channel architecture, here are our production metrics:
Performance Metrics by Platform#
Mobile (React Native WebView)
- First contentful paint: 1.2s average
- Time to interactive: 2.1s average
- Memory usage: 85MB average per micro frontend
- Battery impact: 18% increase over native screens
Web Browser
- First contentful paint: 800ms average
- Time to interactive: 1.4s average
- Memory usage: 120MB average per micro frontend
- Lighthouse score: 92/100 average
Desktop (Electron)
- First contentful paint: 600ms average
- Time to interactive: 1.1s average
- Memory usage: 150MB average per micro frontend
- CPU usage: 15% average
Reliability Metrics#
- Bridge message success rate: 99.97%
- Offline sync success rate: 99.2%
- Platform detection accuracy: 100%
- Cross-platform feature parity: 94%
Development Metrics#
- Code reuse across platforms: 89%
- Platform-specific code: 11%
- Team deployment independence: 100%
- Time to deploy fix: 2 minutes (web) to 24 hours (mobile app store)
Key Takeaways#
-
Alternative Approaches Have Trade-offs: Each bundler and communication method has its strengths. Choose based on your specific needs and team constraints.
-
Hybrid Approaches Work Best: Let teams use their preferred tools while maintaining a consistent interface.
-
Rspack Shows Promise: For new projects, consider Rspack for its speed and webpack compatibility.
-
Re.Pack is Powerful but Complex: Great for super apps but requires significant coordination.
-
Performance is Critical: Users notice slow WebViews immediately. Optimize aggressively.
-
Platform Detection is Essential: Reliable platform detection enables adaptive behavior.
-
Offline Support Changes Everything: Building offline-first makes your app more robust.
-
Monitoring is Non-Negotiable: Comprehensive monitoring should be built-in, not bolted on.
-
Memory Management Matters: Aggressive memory management is crucial on mobile.
-
User Experience Trumps Technology: Users care about consistency, not implementation details.
Lessons Learned#
What Worked Exceptionally Well#
-
Universal Bridge Pattern: Having a single interface that worked across all platforms was game-changing for developer productivity.
-
Adaptive Components: Platform-aware UI components reduced maintenance overhead significantly.
-
Progressive Enhancement: Starting with web capabilities and adding native features worked better than the reverse.
-
Offline-First Architecture: Building offline support from the start made the architecture more robust overall.
What We'd Do Differently#
-
Performance Budget: Set and enforce performance budgets from day one. It's harder to optimize later.
-
Testing Strategy: We should have invested more in automated testing across platforms earlier.
-
Monitoring: Comprehensive monitoring should be built-in, not bolted on.
-
Memory Management: More aggressive memory management on mobile from the start.
Unexpected Discoveries#
-
Desktop Performance: Electron was surprisingly the best-performing platform for our use case.
-
Battery Life: Proper optimization actually improved battery life on mobile compared to our original native screens.
-
User Satisfaction: Users didn't notice the hybrid architecture - consistency mattered more than being "pure native."
-
Team Dynamics: Having deployment independence changed how teams collaborated for the better.
When to Use Multi-Channel Micro Frontends#
This architecture makes sense when:
- You need to support multiple platforms with limited resources
- Teams need deployment independence
- You have significant existing web applications to reuse
- Consistency across platforms is more important than maximum performance
- You can invest in platform-specific optimization
It's not ideal when:
- Performance is absolutely critical (games, video editing, etc.)
- You have platform-specific features that are core to the experience
- Team size is small enough that coordination isn't a bottleneck
- You're building a simple application
Conclusion#
Building a truly multi-channel micro frontend architecture was one of the most challenging projects I've worked on, but also one of the most rewarding. We went from supporting one platform to four platforms with the same team size, while actually improving our deployment velocity.
The key insights were:
- Embrace platform differences rather than trying to hide them
- Build abstractions that enhance rather than obscure the underlying platforms
- Invest heavily in monitoring and debugging tools from the start
- Performance optimization is an ongoing process, not a one-time task
Today, our architecture serves millions of users across mobile apps, web browsers, and desktop applications. Teams can deploy independently, users get consistent experiences, and we've proven that micro frontends can work at scale across all major platforms.
The future of frontend development is multi-channel. The question isn't whether you'll need to support multiple platforms, but whether you'll be ready when that requirement arrives.
This concludes our three-part series on mobile micro frontends. From WebView basics to production-scale multi-channel architecture, we've covered the complete journey. The code, patterns, and lessons shared here come from real production systems serving real users.
If you're building similar architectures, I'd love to hear about your experiences and challenges. The micro frontend space is evolving rapidly, and every implementation teaches us something new about what's possible.
Comments (0)
Join the conversation
Sign in to share your thoughts and engage with the community
No comments yet
Be the first to share your thoughts on this post!
Comments (0)
Join the conversation
Sign in to share your thoughts and engage with the community
No comments yet
Be the first to share your thoughts on this post!