UTA DevHub

App Initialization

Reference guide to the application startup flow, initialization sequence, and stage-based bootstrap process.

App Initialization

Executive Summary

This document outlines the application's initialization architecture, which manages the staged bootstrapping process from launch to fully interactive UI. Our architecture employs a modular, stage-based initialization sequence that balances immediate user feedback with comprehensive system preparation. This approach optimizes perceived performance while ensuring all necessary services are properly initialized.

Purpose & Scope

This guide serves as the primary reference for understanding how the application initializes, and how different parts of the system should participate in the startup sequence. Its purpose is to:

  • Define the core initialization stages and their sequence
  • Establish patterns for registering initialization tasks
  • Explain how to handle dependencies between initialization steps
  • Provide guidance on optimizing startup performance

This document is intended for all developers who need to add initialization logic to the application, manage startup performance, or understand the bootstrap process holistically.

Prerequisites

To fully understand the concepts in this guide, we recommend familiarity with:

Native App Launch Sequence

Before our JavaScript initialization architecture begins execution, the native platforms (iOS and Android) go through their own launch sequences. Understanding these native processes is important for a complete picture of app initialization.

The iOS app launch sequence follows several distinct phases as documented in Apple's official documentation:

  1. Pre-launch Phase: System verifies app's signature and prepares its environment
  2. Launch Phase: UIKit creates the app's process and entry point
  3. Initialization Phase: App delegate methods are called to set up the app's infrastructure
  4. Event Processing: App begins processing events and displaying UI

Our JavaScript initialization begins during the later part of the Initialization Phase, after the native iOS components have been set up. The splash screen is displayed by the native side during these early phases, providing immediate visual feedback to users while the JavaScript engine initializes.

Our JavaScript initialization architecture is designed to work harmoniously with these native launch sequences, picking up where they leave off to complete the initialization process.

Initialization Stages

Our application uses a staged initialization approach to optimize both actual and perceived performance:

Stage 1: Pre-UI (Critical)

  • Purpose: Initialize critical services required before any UI rendering
  • Timing: Blocking - must complete before UI can be shown
  • Components:
    • Authentication token retrieval
    • Critical store hydration (theme, language, etc.)
    • Configuration loading
  • Characteristics:
    • Focused only on essential services
    • Optimized for speed
    • Blocking the UI render

Stage 2: Initial UI

  • Purpose: Render initial UI shell to improve perceived performance
  • Timing: As soon as Stage 1 completes
  • Components:
    • Navigation setup
    • Initial route determination
    • App shell rendering
    • Loading indicators
  • Characteristics:
    • Focused on visual feedback
    • May show placeholders or skeletons
    • Navigation guards based on auth state

Stage 3: Background (Parallel)

  • Purpose: Initialize non-critical services in parallel
  • Timing: Started after Stage 2, non-blocking
  • Components:
    • API client initialization
    • TanStack Query prefetching
    • Secondary store hydration
    • Analytics setup
  • Characteristics:
    • Parallel execution where possible
    • Progress can be reported to UI
    • Can be prioritized by importance

Stage 4: Finalization

  • Purpose: Complete initialization and enable full interactivity
  • Timing: After all background tasks complete
  • Components:
    • Update UI with loaded data
    • Remove loading indicators
    • Enable deferred interactions
    • Report completion analytics
  • Characteristics:
    • Ensures smooth transition to fully interactive state
    • May be feature-specific rather than app-wide
    • Can be gradual rather than all-at-once

Our initialization architecture is designed to work seamlessly with both Expo Router and React Navigation. Below are examples of how to integrate with each navigation system using our abstracted SplashScreen component.

// app/_layout.tsx (Expo Router)
import { Stack } from 'expo-router';
import { useEffect, useState } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/ui/theme/ThemeProvider';
import { InitializationProvider } from '@/core/shared/app/initialization/InitializationProvider';
import { initializer, InitStage } from '@/core/shared/app/initialization';
import { SplashScreen } from '@/core/shared/splash-screen/SplashScreen';
import { View } from 'react-native';
 
export default function RootLayout() {
  const [preUIComplete, setPreUIComplete] = useState(false);
 
  // Start Pre-UI initialization when component mounts
  useEffect(() => {
    async function initializePreUI() {
      try {
        // Execute Pre-UI stage (critical, blocking)
        await initializer.executeStage(InitStage.PRE_UI);
        
        // Mark Pre-UI complete so we can render app shell
        setPreUIComplete(true);
        
        // Hide splash screen with fade transition
        SplashScreen.hide({ fade: true, duration: 500 });
        
        // Continue with non-blocking stages
        initializer.executeStage(InitStage.INITIAL_UI).then(() => {
          initializer.executeParallelStage(InitStage.BACKGROUND).then(() => {
            initializer.executeStage(InitStage.FINALIZATION);
          });
        });
      } catch (error) {
        console.error('Pre-UI initialization failed:', error);
        // Handle critical initialization failure
        SplashScreen.hide({ fade: true }); // Still hide splash screen
        // Show error screen
      }
    }
 
    initializePreUI();
  }, []);
 
  // Keep splash screen visible until Pre-UI is complete
  if (!preUIComplete) {
    return <View />; // Return empty view, splash screen is still showing
  }
 
  // Render app once Pre-UI is complete
  return (
    <InitializationProvider>
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>
          <Stack />
        </ThemeProvider>
      </QueryClientProvider>
    </InitializationProvider>
  );
}

Both approaches follow the same initialization flow but integrate with different navigation systems. The key similarities are:

  1. Execute Pre-UI initialization first (blocking)
  2. Hide splash screen with a fade transition once Pre-UI is complete
  3. Continue with remaining initialization stages in the background
  4. Provide the same state and services through the InitializationProvider

Initialization Tasks

Modular tasks that can be registered for specific initialization stages:

// core/domains/auth/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { tokenService } from './tokenService';
import { authStore } from './store';
 
export const authInitialization: InitTask = {
  name: 'auth:token-retrieval',
  
  async execute(): Promise<void> {
    try {
      // Retrieve tokens from secure storage
      const token = await tokenService.getStoredAccessToken();
      const refreshToken = await tokenService.getStoredRefreshToken();
      
      // Update auth store
      if (token && refreshToken) {
        authStore.setTokens(token, refreshToken);
      }
    } catch (error) {
      console.error('Failed to initialize auth tokens:', error);
      // Non-blocking error - continue initialization
    }
  }
};
 
// Register task with initializer
initializer.registerTask(InitStage.PRE_UI, authInitialization);

Initialization Event System

An event system for coordinating activities during initialization:

// core/shared/app/initialization/events.ts
import { EventEmitter } from '@/core/shared/utils/events';
import { InitStage } from './initializer';
 
export type InitEvents = {
  'stage:complete': InitStage;
  'initialization:complete': void;
  'initialization:error': Error;
};
 
export const initEvents = new EventEmitter<InitEvents>();

Initialization Provider Component

React component that manages initialization state in the component tree:

// core/shared/app/initialization/InitializationProvider.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { initializer, InitStage } from './initializer';
import { initEvents } from './events';
 
interface InitializationState {
  initialized: boolean;
  currentStage: InitStage | null;
  error: Error | null;
  initializer: typeof initializer; // Expose initializer for direct usage
}
 
const InitializationContext = createContext<InitializationState>({
  initialized: false,
  currentStage: null,
  error: null,
  initializer
});
 
export const useInitialization = () => useContext(InitializationContext);
 
export function InitializationProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<InitializationState>({
    initialized: false,
    currentStage: null,
    error: null,
    initializer
  });
  
  useEffect(() => {
    const stageListener = (stage: InitStage) => {
      setState(prev => ({ ...prev, currentStage: stage }));
    };
    
    const completeListener = () => {
      setState(prev => ({ ...prev, initialized: true }));
    };
    
    const errorListener = (error: Error) => {
      setState(prev => ({ ...prev, error }));
    };
    
    initEvents.on('stage:complete', stageListener);
    initEvents.on('initialization:complete', completeListener);
    initEvents.on('initialization:error', errorListener);
    
    // Note: We don't automatically start initialization here anymore
    // This is now handled in the root navigation component
    
    return () => {
      initEvents.off('stage:complete', stageListener);
      initEvents.off('initialization:complete', completeListener);
      initEvents.off('initialization:error', errorListener);
    };
  }, []);
  
  return (
    <InitializationContext.Provider value={{...state, initializer}}>
      {children}
    </InitializationContext.Provider>
  );
}

Design Principles

Core Architectural Principles

  1. Progressive Initialization

    • Prioritize critical paths over complete initialization
    • Show UI feedback as early as possible
    • Load data and services in stages based on priority
  2. Modular and Extensible

    • Each domain and feature can register its own initialization tasks
    • Clear interfaces for adding new initialization tasks
    • Flexible dependency management between tasks
  3. Failure Resilience

    • Graceful handling of initialization failures
    • Critical vs. non-critical error differentiation
    • Recovery strategies for non-fatal errors

Trade-offs and Design Decisions

DecisionBenefitsTrade-offsRationale
Staged initializationBetter perceived performance, earlier UI interactionMore complex coordinationUser experience prioritized over implementation simplicity
Blocking pre-UI stageEnsures critical services are readyCan delay initial renderingPrevents jarring state changes after initial render
Parallel background tasksFaster overall initializationMore complex error handlingOptimizes total initialization time
Event-based coordinationLoose coupling between systemsMore challenging to debugAllows dynamic registration of initialization components

Constraints and Considerations

  • Memory Usage: Be mindful of initialization processes that consume significant memory
  • Initialization Order: Some services have implicit dependencies that affect initialization order
  • Error Handling: Have a strategy for handling initialization failures at each stage
  • Timeout Handling: Implement timeouts for initialization tasks to prevent hanging

Core Implementation Reference

This section provides the complete reference implementation of the initialization system. The guides in App Initialization Guides build upon this foundation with stage-specific implementation patterns.

Core Types and Interfaces

// core/shared/app/initialization/types.ts
 
/**
 * Defines the distinct stages of the application initialization process.
 * Stages execute in the order defined here (Pre-UI → Initial UI → Background → Finalization).
 */
export enum InitStage {
  /** Critical blocking operations that must complete before any UI rendering */
  PRE_UI = 'preUI',
  
  /** Initial UI shell rendering and essential components */
  INITIAL_UI = 'initialUI',
  
  /** Non-blocking parallel operations that enhance user experience */
  BACKGROUND = 'background',
  
  /** Final tasks to complete initialization and transition to full interactivity */
  FINALIZATION = 'finalization'
}
 
/**
 * Priority levels for initialization tasks.
 * Primarily used for Background stage tasks to determine execution order.
 */
export type TaskPriority = 'high' | 'normal' | 'low';
 
/**
 * Interface defining an initialization task.
 */
export interface InitTask {
  /** Unique name identifying the task, using domain:feature-task format */
  name: string;
  
  /** The function to execute - must return a Promise */
  execute: () => Promise<void>;
  
  /** The initialization stage this task belongs to */
  stage: InitStage;
  
  /** Optional array of task names this task depends on */
  dependencies?: string[];
  
  /** Priority level (default: 'normal') */
  priority?: TaskPriority;
  
  /** Whether failure should halt initialization (default: false) */
  critical?: boolean;
  
  /** Timeout in milliseconds (default: 30000) */
  timeout?: number;
}
 
/**
 * Event types emitted during initialization.
 */
export type InitEventTypes = {
  'stage:started': { stage: InitStage };
  'stage:complete': { stage: InitStage };
  'task:started': { name: string; stage: InitStage };
  'task:completed': { name: string; stage: InitStage };
  'task:error': { name: string; stage: InitStage; error: Error };
  'initialization:complete': void;
  'initialization:error': Error;
};

Initializer Class Implementation

// core/shared/app/initialization/initializer.ts
import { EventEmitter } from '@/core/shared/utils/events';
import { InitStage, InitTask, InitEventTypes, TaskPriority } from './types';
 
/**
 * Event emitter for initialization events.
 * Allows components to subscribe to initialization progress events.
 */
export const initEvents = new EventEmitter<InitEventTypes>();
 
/**
 * Core class that manages the application initialization process.
 * Handles registration, dependency resolution, and execution of initialization tasks.
 */
export class Initializer {
  /** Map of stages to their registered tasks */
  private stages: Map<InitStage, InitTask[]> = new Map();
  
  /** Tracks completed tasks by name */
  private completedTasks: Set<string> = new Set();
  
  /** Tracks whether specific stages have completed */
  private completedStages: Set<InitStage> = new Set();
  
  /** Tracks currently executing tasks */
  private executingTasks: Set<string> = new Set();
  
  /**
   * Registers an initialization task for a specific stage.
   * 
   * @param stage - The initialization stage this task belongs to
   * @param task - The task configuration object
   */
  registerTask(stage: InitStage, task: Omit<InitTask, 'stage'>): void {
    // Create the full task with stage information
    const fullTask: InitTask = {
      ...task,
      stage,
      priority: task.priority || 'normal',
      critical: task.critical ?? false,
      timeout: task.timeout ?? 30000 // Default 30 second timeout
    };
    
    // Initialize the stage array if needed
    if (!this.stages.has(stage)) {
      this.stages.set(stage, []);
    }
    
    // Add the task to the appropriate stage
    const stageTasks = this.stages.get(stage)!;
    
    // Check for duplicate task names
    if (stageTasks.some(t => t.name === fullTask.name)) {
      console.warn(`Task with name ${fullTask.name} already registered for stage ${stage}`);
      return;
    }
    
    stageTasks.push(fullTask);
  }
  
  /**
   * Executes all tasks for a specific stage sequentially, respecting dependencies.
   * Used for PRE_UI and INITIAL_UI stages that require ordered execution.
   * 
   * @param stage - The stage to execute
   * @returns Promise resolving when all tasks in the stage complete
   */
  async executeStage(stage: InitStage): Promise<void> {
    try {
      const tasks = this.stages.get(stage) || [];
      
      // Notify stage started
      initEvents.emit('stage:started', { stage });
      
      // Sort tasks to respect dependencies
      const sortedTasks = this.sortTasksByDependencies(tasks);
      
      // Execute tasks sequentially
      for (const task of sortedTasks) {
        await this.executeTask(task);
      }
      
      // Mark stage as completed
      this.completedStages.add(stage);
      
      // Notify stage complete
      initEvents.emit('stage:complete', { stage });
      
      // Check if all stages are complete
      this.checkInitializationComplete();
    } catch (error) {
      console.error(`Stage ${stage} execution failed:`, error);
      initEvents.emit('initialization:error', error as Error);
      throw error;
    }
  }
  
  /**
   * Executes tasks for a stage in parallel, grouped by priority.
   * Used for BACKGROUND and FINALIZATION stages where tasks can run concurrently.
   * 
   * @param stage - The stage to execute in parallel
   * @returns Promise resolving when all tasks in the stage complete
   */
  async executeParallelStage(stage: InitStage): Promise<void> {
    try {
      const tasks = this.stages.get(stage) || [];
      
      // Notify stage started
      initEvents.emit('stage:started', { stage });
      
      // Group tasks by priority
      const highPriorityTasks = tasks.filter(task => task.priority === 'high');
      const normalPriorityTasks = tasks.filter(task => task.priority === 'normal');
      const lowPriorityTasks = tasks.filter(task => task.priority === 'low');
      
      // Execute high priority tasks first
      await Promise.all(highPriorityTasks.map(task => this.executeTask(task)));
      
      // Then normal priority tasks
      await Promise.all(normalPriorityTasks.map(task => this.executeTask(task)));
      
      // Finally low priority tasks
      await Promise.all(lowPriorityTasks.map(task => this.executeTask(task)));
      
      // Mark stage as completed
      this.completedStages.add(stage);
      
      // Notify stage complete
      initEvents.emit('stage:complete', { stage });
      
      // Check if all stages are complete
      this.checkInitializationComplete();
    } catch (error) {
      console.error(`Stage ${stage} execution failed:`, error);
      initEvents.emit('initialization:error', error as Error);
      throw error;
    }
  }
  
  /**
   * Executes a single initialization task with timeout and error handling.
   * 
   * @param task - The task to execute
   * @returns Promise resolving when the task completes
   */
  private async executeTask(task: InitTask): Promise<void> {
    // Skip if dependencies aren't satisfied
    if (!this.areDependenciesSatisfied(task)) {
      throw new Error(
        `Task ${task.name} dependencies not satisfied. ` +
        `Missing: ${this.getMissingDependencies(task).join(', ')}`
      );
    }
    
    // Skip if already completed
    if (this.completedTasks.has(task.name)) {
      return;
    }
    
    // Mark as executing
    this.executingTasks.add(task.name);
    
    try {
      // Notify task started
      initEvents.emit('task:started', { name: task.name, stage: task.stage });
      
      // Create a timeout promise
      const timeoutPromise = new Promise<never>((_, reject) => {
        setTimeout(() => {
          reject(new Error(`Task ${task.name} timed out after ${task.timeout}ms`));
        }, task.timeout);
      });
      
      // Race between task execution and timeout
      await Promise.race([
        task.execute(),
        timeoutPromise
      ]);
      
      // Mark as completed
      this.completedTasks.add(task.name);
      this.executingTasks.delete(task.name);
      
      // Notify task completed
      initEvents.emit('task:completed', { name: task.name, stage: task.stage });
    } catch (error) {
      // Handle task error
      this.executingTasks.delete(task.name);
      console.error(`Task ${task.name} failed:`, error);
      initEvents.emit('task:error', { 
        name: task.name, 
        stage: task.stage, 
        error: error as Error 
      });
      
      // Rethrow critical errors, swallow non-critical ones
      if (task.critical) {
        throw error;
      }
    }
  }
  
  /**
   * Checks if all required dependencies for a task have been completed.
   * 
   * @param task - The task to check dependencies for
   * @returns True if all dependencies are satisfied
   */
  private areDependenciesSatisfied(task: InitTask): boolean {
    if (!task.dependencies || task.dependencies.length === 0) {
      return true;
    }
    
    return task.dependencies.every(dep => this.completedTasks.has(dep));
  }
  
  /**
   * Gets the list of dependencies that haven't been satisfied yet.
   * 
   * @param task - The task to check
   * @returns Array of missing dependency names
   */
  private getMissingDependencies(task: InitTask): string[] {
    if (!task.dependencies) {
      return [];
    }
    
    return task.dependencies.filter(dep => !this.completedTasks.has(dep));
  }
  
  /**
   * Sorts tasks based on their dependencies to ensure proper execution order.
   * Uses a topological sort algorithm.
   * 
   * @param tasks - Array of tasks to sort
   * @returns Sorted array of tasks respecting dependencies
   */
  private sortTasksByDependencies(tasks: InitTask[]): InitTask[] {
    // Build dependency graph
    const graph = new Map<string, string[]>();
    tasks.forEach(task => {
      graph.set(task.name, task.dependencies || []);
    });
    
    // Topological sort
    const result: InitTask[] = [];
    const visited = new Set<string>();
    const temp = new Set<string>();
    
    const visit = (taskName: string) => {
      if (temp.has(taskName)) {
        throw new Error(`Dependency cycle detected involving task ${taskName}`);
      }
      
      if (visited.has(taskName)) {
        return;
      }
      
      temp.add(taskName);
      
      const dependencies = graph.get(taskName) || [];
      for (const dep of dependencies) {
        visit(dep);
      }
      
      temp.delete(taskName);
      visited.add(taskName);
      
      const task = tasks.find(t => t.name === taskName);
      if (task) {
        result.push(task);
      }
    };
    
    // Visit all tasks
    for (const task of tasks) {
      if (!visited.has(task.name)) {
        visit(task.name);
      }
    }
    
    return result.reverse();
  }
  
  /**
   * Checks if all stages have completed and emits the initialization complete event.
   */
  private checkInitializationComplete(): void {
    const allStages = Object.values(InitStage);
    const registeredStages = Array.from(this.stages.keys());
    
    // Check if all registered stages are complete
    const allComplete = registeredStages.every(stage => 
      this.completedStages.has(stage)
    );
    
    if (allComplete && registeredStages.length > 0) {
      initEvents.emit('initialization:complete', undefined);
    }
  }
  
  /**
   * Resets the initializer state.
   * Primarily used for testing purposes.
   */
  reset(): void {
    this.stages = new Map();
    this.completedTasks = new Set();
    this.completedStages = new Set();
    this.executingTasks = new Set();
  }
}
 
// Create singleton instance
export const initializer = new Initializer();

Usage in React Components

The InitializationProvider component manages initialization state and makes it available throughout the React component tree:

// core/shared/app/initialization/InitializationProvider.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { initializer, InitStage } from './initializer';
import { initEvents } from './events';
 
interface InitializationState {
  initialized: boolean;
  currentStage: InitStage | null;
  error: Error | null;
  initializer: typeof initializer; // Expose initializer for direct usage
}
 
const InitializationContext = createContext<InitializationState>({
  initialized: false,
  currentStage: null,
  error: null,
  initializer
});
 
export const useInitialization = () => useContext(InitializationContext);
 
export function InitializationProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<InitializationState>({
    initialized: false,
    currentStage: null,
    error: null,
    initializer
  });
  
  useEffect(() => {
    const stageListener = ({ stage }: { stage: InitStage }) => {
      setState(prev => ({ ...prev, currentStage: stage }));
    };
    
    const completeListener = () => {
      setState(prev => ({ ...prev, initialized: true }));
    };
    
    const errorListener = (error: Error) => {
      setState(prev => ({ ...prev, error }));
    };
    
    initEvents.on('stage:started', stageListener);
    initEvents.on('initialization:complete', completeListener);
    initEvents.on('initialization:error', errorListener);
    
    return () => {
      initEvents.off('stage:started', stageListener);
      initEvents.off('initialization:complete', completeListener);
      initEvents.off('initialization:error', errorListener);
    };
  }, []);
  
  return (
    <InitializationContext.Provider value={{...state, initializer}}>
      {children}
    </InitializationContext.Provider>
  );
}

Implementation Considerations

Performance Implications

  • (Do ✅) Measure initialization performance

    • Track time spent in each initialization stage
    • Identify and optimize bottlenecks
    • Consider using performance monitoring tools
  • (Do ✅) Minimize Pre-UI blocking tasks

    • Only include truly essential initialization in Stage 1
    • Move as much as possible to background stages
    • Consider lazy loading for non-critical features
  • (Don't ❌) Load excessive data during initialization

    • Defer data loading until components mount when possible
    • Use pagination or windowing for large data sets
    • Consider background prefetching for critical data only

Security Considerations

  • (Do ✅) Handle authentication state securely

    • Store tokens in secure storage
    • Validate tokens during initialization
    • Clear invalid tokens during startup if detected
  • (Do ✅) Implement proper boot security

    • Check for tampered application state
    • Verify integrity of critical initialization components
    • Consider certificate pinning for API communications
  • (Don't ❌) Store sensitive information in non-secure stores

    • Use appropriate storage mechanisms for sensitive data
    • Clear sensitive data from memory when no longer needed

Error Handling and Fallback UI

Providing a graceful user experience during initialization failures is critical. Instead of just logging errors, implement fallback UI components that guide users through recovery options.

Error States UI Example

// core/shared/initialization/ErrorBoundary.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { InitErrorType, useInitialization } from '@/core/shared/app/initialization';
 
/**
 * Error screen displayed when initialization fails
 */
export function InitializationErrorScreen({ 
  error, 
  retry 
}: { 
  error: Error; 
  retry: () => void 
}) {
  // Determine error type to show appropriate guidance
  const errorType = error.name === 'NetworkError' 
    ? 'network' 
    : error.name === 'AuthError' 
      ? 'auth' 
      : 'unknown';
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Unable to Start Application</Text>
      
      {errorType === 'network' && (
        <>
          <Text style={styles.message}>
            Please check your internet connection and try again.
          </Text>
          <View style={styles.iconContainer}>
            {/* Network error icon */}
          </View>
        </>
      )}
      
      {errorType === 'auth' && (
        <>
          <Text style={styles.message}>
            There was a problem with your account. Please log in again.
          </Text>
          <View style={styles.iconContainer}>
            {/* Auth error icon */}
          </View>
        </>
      )}
      
      {errorType === 'unknown' && (
        <>
          <Text style={styles.message}>
            An unexpected error occurred while starting the app.
          </Text>
          <View style={styles.iconContainer}>
            {/* Generic error icon */}
          </View>
        </>
      )}
      
      <TouchableOpacity 
        style={styles.button} 
        onPress={retry}
        activeOpacity={0.7}
      >
        <Text style={styles.buttonText}>Try Again</Text>
      </TouchableOpacity>
    </View>
  );
}
 
/**
 * Wrapper component that handles initialization errors
 */
export function InitializationErrorBoundary({ children }: { children: React.ReactNode }) {
  const { error, initializer } = useInitialization();
  const [retryCount, setRetryCount] = useState(0);
  
  // Retry initialization
  const handleRetry = () => {
    setRetryCount(count => count + 1);
    // Reset initializer state and try again
    initializer.reset();
    initializer.executeStage(InitStage.PRE_UI);
  };
  
  // If there's an initialization error, show the error screen
  if (error) {
    return <InitializationErrorScreen error={error} retry={handleRetry} />;
  }
  
  // No error, render children normally
  return <>{children}</>;
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 24,
    backgroundColor: '#FFFFFF'
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#E53935'
  },
  message: {
    fontSize: 16,
    textAlign: 'center',
    marginBottom: 24,
    color: '#424242'
  },
  iconContainer: {
    width: 120,
    height: 120,
    marginBottom: 32,
    justifyContent: 'center',
    alignItems: 'center'
  },
  button: {
    backgroundColor: '#2196F3',
    paddingVertical: 12,
    paddingHorizontal: 32,
    borderRadius: 24
  },
  buttonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: 'bold'
  }
});

Integration in Root Component

// App.tsx or app/_layout.tsx
import { InitializationErrorBoundary } from '@/core/shared/app/initialization/ErrorBoundary';
 
export default function App() {
  // Other initialization code...
  
  return (
    <InitializationProvider>
      <InitializationErrorBoundary>
        <QueryClientProvider client={queryClient}>
          <ThemeProvider>
            <NavigationContainer>
              <AppNavigator />
            </NavigationContainer>
          </ThemeProvider>
        </QueryClientProvider>
      </InitializationErrorBoundary>
    </InitializationProvider>
  );
}

Stage-Specific Error Handling

For finer-grained error management during different initialization stages:

// In InitializationProvider
useEffect(() => {
  const stageListener = ({ stage }: { stage: InitStage }) => {
    setState(prev => ({ ...prev, currentStage: stage }));
  };
  
  const errorListener = (error: Error) => {
    // Log detailed error information for debugging
    console.error(`Initialization error (${state.currentStage || 'unknown stage'}):`, error);
    
    // Categorize errors by stage for appropriate UI response
    const errorWithContext: InitError = {
      ...error,
      stage: state.currentStage,
      timestamp: new Date().toISOString()
    };
    
    setState(prev => ({ ...prev, error: errorWithContext }));
    
    // For non-critical errors in later stages, you might
    // want to show a toast or snackbar instead of a full error screen
    if (state.currentStage !== InitStage.PRE_UI) {
      showNonBlockingError(errorWithContext);
    }
  };
  
  // Rest of the effect code...
}, []);

Robust Timeout Handling

Timeout handling is critical during initialization to prevent tasks from hanging indefinitely. Here's a comprehensive approach to implementing timeout handling for initialization tasks:

/**
 * Execute a task with timeout protection
 * @param task The initialization task to execute
 * @param timeoutMs Maximum execution time in milliseconds
 */
async function executeTaskWithTimeout<T>(
  task: () => Promise<T>,
  timeoutMs: number = 10000
): Promise<T> {
  // Create a timeout error promise
  const timeoutPromise = new Promise<never>((_, reject) => {
    const id = setTimeout(() => {
      clearTimeout(id);
      reject(new InitializationTimeoutError(
        `Task timed out after ${timeoutMs}ms`,
        { timeoutMs }
      ));
    }, timeoutMs);
  });
  
  // Race the task against the timeout
  return Promise.race([task(), timeoutPromise]);
}
 
// Usage example:
try {
  await executeTaskWithTimeout(
    () => loadUserSettings(),
    5000 // 5 second timeout
  );
  console.log('Settings loaded successfully');
} catch (error) {
  if (error instanceof InitializationTimeoutError) {
    console.error('Settings load timed out, using defaults');
    // Fall back to default settings
  } else {
    // Handle other errors
    console.error('Error loading settings:', error);
  }
}

Design Principles for Timeout Handling

  • (Do ✅) Set appropriate timeouts per task

    • Consider each task's expected execution time in different environments
    • Set stricter timeouts for critical UI-blocking operations
    • Allow longer timeouts for background operations
  • (Do ✅) Implement fallback behaviors

    • Provide safe default values when initialization times out
    • Consider if partial initialization is acceptable for specific features
    • Document fallback behaviors in task implementations
  • (Do ✅) Use graduated timeout responses

    • For critical tasks, retry before giving up completely
    • For non-critical tasks, gracefully degrade functionality
    • Use exponential backoff for network-related timeouts
  • (Consider 🤔) Environment-specific timeouts

    • Development environments may need longer timeouts
    • Production timeouts should be optimized for user experience
    • Testing environments might use very short timeouts to catch regressions
  • (Be Aware ❗) Device-specific considerations

    • Older/slower devices may need more generous timeouts
    • Consider detecting device capabilities for adaptive timeouts
    • Monitor timeout frequency in production across device types

Integration with Progress Reporting

Combining timeout handling with progress reporting provides the best user experience:

// In your root component
import { initializer, InitStage, initEvents } from '@/core/shared/app/initialization';
import { SplashScreen } from '@/core/shared/splash-screen/SplashScreen';
 
export default function App() {
  useEffect(() => {
    async function initialize() {
      try {
        // Listen for timeout events to communicate to the user
        const timeoutListener = ({ task, stage }) => {
          // Update UI to show that something is taking longer than expected
          if (stage === InitStage.PRE_UI) {
            SplashScreen.setMessage('Taking longer than expected...');
          }
        };
        
        initEvents.on('task:timeout', timeoutListener);
        
        // Start initialization with progress updates
        await initializer.executeStage(InitStage.PRE_UI);
        SplashScreen.setProgress(0.4);
        
        // Continue with other stages...
        
        // Clean up
        initEvents.off('task:timeout', timeoutListener);
      } catch (error) {
        // Handle initialization failure
        if (error instanceof InitializationTimeoutError) {
          // Show specific timeout error UI
          setTimeoutError(error);
        } else {
          // Handle other errors
          setGeneralError(error);
        }
      }
    }
    
    initialize();
  }, []);
  
  // Render appropriate UI based on initialization state
}

Performance Measurement and Optimization

Measuring initialization performance is crucial for identifying bottlenecks and ensuring a positive user experience. Implementing a performance tracking system allows you to monitor initialization times across different stages and tasks.

Performance Tracking Implementation

// core/shared/app/initialization/performance.ts
import { InitStage } from './types';
 
/**
 * Interface for tracking detailed performance metrics
 */
export interface PerformanceMetrics {
  // Overall timing
  totalDuration: number;
  appStartTimestamp: number;
  
  // Stage timing
  stageDurations: Record<InitStage, number>;
  stageStartTimes: Record<InitStage, number>;
  stageEndTimes: Record<InitStage, number>;
  
  // Task timing
  taskDurations: Record<string, number>;
  longestTasks: Array<{name: string; duration: number}>;
}
 
/**
 * Service for tracking initialization performance
 */
export class PerformanceTracker {
  private appStartTime: number;
  private stageStartTimes: Partial<Record<InitStage, number>> = {};
  private stageEndTimes: Partial<Record<InitStage, number>> = {};
  private taskStartTimes: Map<string, number> = new Map();
  private taskDurations: Map<string, number> = new Map();
  
  constructor() {
    // Record app start time as soon as JavaScript bundle loads
    this.appStartTime = performance.now();
    
    // Listen for stage and task events to track timing
    initEvents.on('stage:started', this.handleStageStarted);
    initEvents.on('stage:complete', this.handleStageComplete);
    initEvents.on('task:started', this.handleTaskStarted);
    initEvents.on('task:completed', this.handleTaskCompleted);
  }
  
  private handleStageStarted = ({ stage }: { stage: InitStage }) => {
    this.stageStartTimes[stage] = performance.now();
  };
  
  private handleStageComplete = ({ stage }: { stage: InitStage }) => {
    this.stageEndTimes[stage] = performance.now();
  };
  
  private handleTaskStarted = ({ name }: { name: string }) => {
    this.taskStartTimes.set(name, performance.now());
  };
  
  private handleTaskCompleted = ({ name }: { name: string }) => {
    const startTime = this.taskStartTimes.get(name);
    if (startTime) {
      const duration = performance.now() - startTime;
      this.taskDurations.set(name, duration);
    }
  };
  
  /**
   * Get complete performance metrics
   */
  getMetrics(): PerformanceMetrics {
    const now = performance.now();
    const totalDuration = now - this.appStartTime;
    
    // Calculate stage durations
    const stageDurations: Partial<Record<InitStage, number>> = {};
    Object.values(InitStage).forEach(stage => {
      const start = this.stageStartTimes[stage] || 0;
      const end = this.stageEndTimes[stage] || now;
      stageDurations[stage] = end - start;
    });
    
    // Find longest tasks
    const taskEntries = Array.from(this.taskDurations.entries())
      .map(([name, duration]) => ({ name, duration }))
      .sort((a, b) => b.duration - a.duration)
      .slice(0, 5); // Top 5 longest tasks
    
    return {
      totalDuration,
      appStartTimestamp: this.appStartTime,
      stageDurations: stageDurations as Record<InitStage, number>,
      stageStartTimes: this.stageStartTimes as Record<InitStage, number>,
      stageEndTimes: this.stageEndTimes as Record<InitStage, number>,
      taskDurations: Object.fromEntries(this.taskDurations),
      longestTasks: taskEntries
    };
  }
  
  /**
   * Log performance metrics to console in a readable format
   */
  logMetricsToConsole() {
    const metrics = this.getMetrics();
    
    console.group('📊 App Initialization Performance Metrics');
    console.log(`Total Time: ${metrics.totalDuration.toFixed(2)}ms`);
    
    console.group('Stage Durations:');
    Object.entries(metrics.stageDurations).forEach(([stage, duration]) => {
      console.log(`- ${stage}: ${duration.toFixed(2)}ms`);
    });
    console.groupEnd();
    
    console.group('Longest Tasks:');
    metrics.longestTasks.forEach(task => {
      console.log(`- ${task.name}: ${task.duration.toFixed(2)}ms`);
    });
    console.groupEnd();
    
    console.groupEnd();
    
    // Additional analytics reporting if needed
    if (__DEV__) {
      // Only log details in development mode
      console.log('Full performance data:', metrics);
    } else {
      // In production, you might want to send this data to an analytics service
      analyticsService?.trackEvent('app_initialization_complete', {
        totalTime: Math.round(metrics.totalDuration),
        preUiTime: Math.round(metrics.stageDurations[InitStage.PRE_UI] || 0),
        uiRenderTime: Math.round(metrics.stageDurations[InitStage.INITIAL_UI] || 0),
        backgroundTime: Math.round(metrics.stageDurations[InitStage.BACKGROUND] || 0)
      });
    }
    
    return metrics;
  }
  
  /**
   * Reset the tracker for testing purposes
   */
  reset() {
    this.stageStartTimes = {};
    this.stageEndTimes = {};
    this.taskStartTimes.clear();
    this.taskDurations.clear();
  }
}
 
// Create singleton instance
export const performanceTracker = new PerformanceTracker();

Integration with Initialization Flow

To use the performance tracker with your initialization process:

// In your root component (App.tsx or _layout.tsx)
import { performanceTracker } from '@/core/shared/app/initialization/performance';
 
export default function App() {
  // Other initialization code...
  
  useEffect(() => {
    async function initialize() {
      try {
        await initializer.executeStage(InitStage.PRE_UI);
        setPreUIComplete(true);
        
        // Continue with other stages...
        await initializer.executeStage(InitStage.INITIAL_UI);
        await initializer.executeParallelStage(InitStage.BACKGROUND);
        await initializer.executeStage(InitStage.FINALIZATION);
        
        // Log performance metrics when initialization completes
        performanceTracker.logMetricsToConsole();
      } catch (error) {
        console.error('Initialization failed:', error);
      }
    }
    
    initialize();
  }, []);
  
  // Rest of component...
}

Performance Optimization Strategies

Here are the most effective performance optimization techniques for app initialization:

  • (Do ✅) Establish performance budgets
    • Set maximum time limits for each initialization stage
    • Create specific budgets for critical tasks
    • Regularly audit performance metrics against budgets
// Example performance budgets by stage
const PERFORMANCE_BUDGETS = {
  [InitStage.PRE_UI]: 2000,        // 2 seconds max
  [InitStage.INITIAL_UI]: 1000,    // 1 second max
  [InitStage.BACKGROUND]: 5000,    // 5 seconds max
  [InitStage.FINALIZATION]: 1000,  // 1 second max
  TOTAL: 7000                      // 7 seconds total max
};
 
// Validate performance against budgets
function validatePerformance(metrics: PerformanceMetrics): void {
  const issues = [];
  
  // Check each stage against its budget
  Object.entries(metrics.stageDurations).forEach(([stage, duration]) => {
    const budget = PERFORMANCE_BUDGETS[stage as InitStage];
    if (budget && duration > budget) {
      issues.push(`${stage} exceeded budget: ${duration.toFixed(0)}ms vs ${budget}ms budget`);
    }
  });
  
  // Check total duration
  if (metrics.totalDuration > PERFORMANCE_BUDGETS.TOTAL) {
    issues.push(`Total initialization exceeded budget: ${metrics.totalDuration.toFixed(0)}ms vs ${PERFORMANCE_BUDGETS.TOTAL}ms budget`);
  }
  
  // Report issues in development
  if (issues.length > 0 && __DEV__) {
    console.warn('⚠️ Performance budget violations:\n' + issues.join('\n'));
  }
}
  • (Do ✅) Implement progressive initialization
    • Prioritize tasks based on UI visibility requirements
    • Show meaningful UI as early as possible
    • Defer non-critical work to background stages
// Example task prioritization
initializer.registerTask({
  name: 'localization',
  stage: InitStage.PRE_UI,
  execute: async () => {
    // Only load critical translations for initial UI
    await i18n.loadNamespaces(['common', 'auth']);
    
    // Schedule remaining translations to load later
    setTimeout(() => {
      i18n.loadNamespaces(['settings', 'profile', 'notifications']);
    }, 0);
  }
});
  • (Do ✅) Optimize async operations
    • Run independent operations in parallel
    • Implement timeout handling for unreliable operations
    • Cache results of expensive operations
initializer.registerTask({
  name: 'data-preloading',
  stage: InitStage.INITIAL_UI,
  execute: async ({ storage }) => {
    // Run independent operations in parallel
    const [userPrefs, appSettings, recentItems] = await Promise.all([
      storage.get('user:preferences'),
      storage.get('app:settings'),
      storage.get('recent:items')
    ]);
    
    return { userPrefs, appSettings, recentItems };
  }
});
  • (Do ✅) Minimize JavaScript bundle size
    • Implement code splitting for non-critical features
    • Use dynamic imports for components loaded after initialization
    • Remove unused dependencies and code
// Dynamic import example for a feature used after initialization
const AnalyticsDashboard = React.lazy(() => import('@/features/analytics/Dashboard'));
 
function Dashboard() {
  return (
    <Suspense fallback={<LoadingIndicator />}>
      <AnalyticsDashboard />
    </Suspense>
  );
}

Performance Best Practices

  • (Do ✅) Prioritize perceived performance

    • Set target durations for each initialization stage (e.g., Pre-UI < 500ms)
    • Monitor metrics regularly to catch regressions
  • (Do ✅) Profile in production-like conditions

    • Test on actual devices, not just simulators
    • Measure with production builds, not just development builds
    • Test with realistic network conditions
  • (Consider 🤔) Progressive enhancement

    • Start with minimal feature set and add features after initial rendering
    • Use feature flags to enable/disable performance-intensive features
  • (Be Aware ❗) Of device-specific optimizations

    • Low-end devices may need different initialization strategies
    • Consider device capabilities when prioritizing tasks

Scalability Aspects

  • (Consider 🤔) Feature-level initialization

    • Allow features to register their own initialization tasks
    • Define dependencies between initialization tasks
    • Create domain-specific initialization modules
  • (Consider 🤔) Environment-specific initialization

    • Support different initialization flows per environment
    • Enable/disable features based on environment
    • Configure initialization timeouts per environment
  • (Be Aware ❗) Of interaction between features

    • Features may have dependencies on each other
    • Some features may require specific initialization order
    • Document dependencies between feature initialization

Adding New Initialization Stages

To add a new initialization stage:

  1. Define the stage

    • Add the new stage to the InitStage enum
    • Document its purpose and timing characteristics
    • Consider its relationship to existing stages
  2. Update the Initializer service

    • Add the new stage to the initialization sequence
    • Implement appropriate execution strategy (blocking vs. parallel)
    • Add event emission for stage completion
  3. Create a registration mechanism

    • Define how tasks register for the new stage
    • Document any specific requirements for the stage
    • Consider dependencies with other stages
  4. Test the new stage

    • Verify correct execution order
    • Measure performance impact
    • Ensure error handling works correctly