Re.Pack and Rspack for Cross-Platform Micro Frontends
Patterns for shipping micro frontends across mobile, web, and desktop: performance, offline support, and production insights, with Rspack and Re.Pack approaches.
Mobile-first micro frontend architectures rarely survive contact with a second channel unchanged. The assumptions that made the architecture work on mobile (WebView lifecycle, native bridge, native gestures, per-channel asset bundles) either do not map to web and desktop at all, or they map at a performance cost that erases the point of sharing the code in the first place. True multi-channel micro frontends (mobile, web, desktop Electron) require a runtime abstraction layer and a discipline around channel-specific code paths, not just “build once, deploy everywhere.”
The default that holds up across all three channels: one universal bridge interface with a separate implementation per platform, a capability map that fails loudly when a feature is missing, and per-channel bundles that each team builds with its own tooling. Everything below hangs off that choice: what to share and what to fork, the bundle-splitting and memory budgets that keep a single codebase viable, the native-bridge discipline, and the operational model for shipping updates to web, mobile, and Electron without coordinating three separate release trains.
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
That default competes with three credible alternatives. Here is what each one costs, and the case where it is the better pick.
Option 1: Re.Pack Multi-Channel Architecture
Re.Pack offers a unified approach for React Native, web, and desktop through Module Federation:
Example setup:
// Re.Pack multi-channel setup
// webpack.config.js (shared config)
const { ModuleFederationPlugin } = require('webpack').container;
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
}
Reasons against 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
Rspack is another option for multi-channel builds:
// rspack.config.mjs - Multi-channel configuration
import { container } from '@rspack/core';
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 container.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');
Reasons against it:
- React Native compatibility: While Re.Pack 5.x now supports Rspack, earlier versions had limited React Native support at the time of evaluation
- 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
Vite is also worth evaluating 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
}
Reasons against it:
- Module Federation: Module Federation v2 is now stable, but Vite’s Module Federation was still experimental at the time of evaluation
- Production builds: Slower than webpack for this use case
- Plugin ecosystem: Fewer plugins for 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 (The Recommended Path)
After evaluating all options, a hybrid approach combines the strengths of each:
Why it holds up:
- Team autonomy: Each team keeps its preferred bundler
- Gradual migration: Teams move to better tooling on their own schedule
- Risk mitigation: If one approach stalls, the other channels keep shipping
- Performance: Each team tunes its own build
The Multi-Channel Challenge
An original mobile architecture built for React Native WebViews works well in isolation, but extending it to web and desktop reveals 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
Each of these is answered once at the bridge boundary in the diagram above, so individual micro frontends never have to solve them again.
Platform Detection and Adaptation
The first problem is 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 unifying move is a single bridge interface that every platform implements in its own way:
// 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 need different UX patterns. Adaptive components handle this:
// 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 surfaces performance bottlenecks that stay hidden 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
Memory constraints differ sharply between platforms:
// 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 behaviour varies 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 hardest parts is offline support that behaves the same on 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 forceSync(): Promise<void> {
await this.processSyncQueue();
}
}
Production Monitoring and Analytics
Monitoring performance across platforms takes one collector with platform-aware sources:
// 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;
}
Common Pitfalls
Four failure modes recur in this architecture, and each one is cheaper to prevent than to repair:
-
No performance budget per channel: Without a budget enforced in CI, shared bundles grow until the WebView channel is the slowest surface in the product. Set the budget before the second channel ships. Retrofitting one means renegotiating features that are already in users’ hands.
-
Testing only the channel you develop on: A bridge action that works in the browser can fail silently in a WebView because the native handler was never registered. Run the same contract test suite against every bridge implementation, including the web fallback.
-
Monitoring bolted on after launch: Bridge latency, sync-queue depth, and per-channel error rates have to be instrumented inside the bridge itself. Added later, they miss exactly the platform-specific failures they were meant to catch.
-
One memory budget for every platform: A cache sized for a desktop renderer will get the WebView terminated on a mid-tier Android device. Budget memory per channel from the start, and honour the platform’s memory-warning signal instead of waiting for a crash report.
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
A universal bridge with per-channel bundles behind it is the default worth starting from when several channels share product logic and the teams behind them ship on separate schedules. Override it when one channel carries the whole experience: a single-platform product, or a workload where frame-level performance decides the outcome, is better served by native code than by an abstraction that has to stay correct on four runtimes at once. If you are starting now, define the capability map first and let each platform-specific decision below it follow from what that map reports.
References
- Micro Frontends - Martin Fowler - Foundational article defining the micro frontend architectural pattern and integration approaches
- Re.Pack - Module Federation for React Native - Official Re.Pack documentation for Module Federation in React Native, including microfrontend setup
- Micro Frontends - micro-frontends.org - Community resource extending microservice principles to frontend development
- React Native Performance Overview - Official React Native documentation on JavaScript thread performance and optimization techniques
- Module Federation - webpack - Webpack’s official Module Federation documentation covering runtime code sharing between independent builds
- react-native-webview - GitHub - Community-maintained cross-platform WebView component for React Native, replacing the removed core WebView
Mobile Micro Frontends with React Native
A comprehensive 3-part series on building mobile micro frontends using React Native, Expo, and WebViews. Covers architecture, communication patterns, and production optimization.
All Posts in This Series
Related posts
How to implement micro frontend architecture in mobile apps with React Native Expo and WebViews, covering performance, proven patterns, and bundlers.
WebView-to-native communication patterns: message passing, service integration, and a type-safe request-response bridge with working code.
Step-by-step guide to adding Sentry to a React Native Expo app: SDK setup, Expo Router instrumentation, session replay, and source maps for EAS.
How native tools like esbuild, SWC, and Vite solved webpack's speed problems, taking builds from tens of seconds down to milliseconds.
Nub and Vite+ are both 2026 oxc-powered Rust toolchains that look like rivals but are not. A clear rule for which binary belongs in which repo.