UTA DevHub
Guides

Background Initialization Guide

Implementation patterns for the Background initialization stage in the application bootstrap process.

Background Initialization Guide

Overview

The Background initialization stage is the third phase in the application bootstrap process, executing non-blocking tasks in parallel after the initial-ui has been rendered. This stage is dedicated to operations that are important but not critical for the first visual rendering - such as data prefetching, secondary store hydration, and non-essential service initialization. This guide provides implementation patterns for efficiently managing background initialization.

Quick Start

To implement the Background initialization stage:

  1. Register tasks with the initializer:

    import { initializer, InitStage } from '@/core/shared/app/initialization';
     
    // Register a Background stage task
    initializer.registerTask(InitStage.BACKGROUND, {
      name: 'data:prefetch-common',
      priority: 'high', // Optional priority (high, normal, low)
      critical: false, // Not critical - won't block app if it fails
      execute: async () => {
        // Prefetch commonly needed data
        await Promise.all([
          queryClient.prefetchQuery(productQueryKeys.featured),
          queryClient.prefetchQuery(categoryQueryKeys.list)
        ]);
      }
    });
  2. After the Initial UI rendering is complete, execute Background tasks in parallel:

    // In your initialization controller
    async function continueInitialization() {
      // Initial UI stage has already completed
      
      // Execute Background tasks (non-blocking, parallel)
      initializer.executeParallelStage(InitStage.BACKGROUND)
        .then(() => {
          // Background tasks completed, proceed to finalization
          initializer.executeStage(InitStage.FINALIZATION);
        })
        .catch(error => {
          console.warn('Some background tasks failed:', error);
          // Still proceed to finalization since background tasks are non-critical
          initializer.executeStage(InitStage.FINALIZATION);
        });
    }
  3. Show subtle loading indicators during this stage without blocking user interaction

When To Use

Add initialization tasks to the Background stage when they:

  • Require network operations: API prefetching, data synchronization
  • Set up non-critical services: Analytics, monitoring, push notifications
  • Hydrate secondary stores: Non-essential app state (critical state should be loaded in the pre-ui)
  • Perform expensive calculations: Operations that would block the UI thread
  • Prepare cached resources: Image preloading, data processing

The Background stage is ideal for tasks that enhance the user experience but aren't required for the initial UI rendering. By deferring these tasks until after the initial-ui, you can significantly improve perceived performance.

Implementation Patterns

The Background stage follows the initial-ui and precedes the finalization. Once all Background tasks complete, the app proceeds to Finalization to complete the initialization process.

Feature-Specific Background Initialization

Regardless of which navigation system you use, feature-specific initialization can be implemented with a common pattern. This builds upon the navigation setup completed during the initial-ui:

// app/(app)/products.tsx (Expo Router)
import { useEffect } from 'react';
import { initializer, InitStage } from '@/core/shared/app/initialization';
import { useInitialization } from '@/core/shared/app/initialization';
import { View, Text } from 'react-native';
import { ProductList } from '@/features/products/components/ProductList';
import { LoadingOverlay } from '@/ui/components/LoadingOverlay';
 
export default function ProductsScreen() {
  const { initialized, currentStage } = useInitialization();
  
  // Register feature-specific initialization task
  useEffect(() => {
    const productInitTask = {
      name: 'features:products-init',
      stage: InitStage.BACKGROUND,
      priority: 'high',
      execute: async () => {
        // Prefetch products, load category data, etc.
        await productService.prefetchFeaturedProducts();
      }
    };
    
    initializer.registerTask(InitStage.BACKGROUND, productInitTask);
  }, []);
  
  return (
    <View style={{ flex: 1 }}>
      <ProductList />
      
      {/* Show loading overlay if not fully initialized */}
      {!initialized && currentStage === InitStage.BACKGROUND && (
        <LoadingOverlay message="Loading products..." />
      )}
    </View>
  );
}

Parallel Task Execution

The key characteristic of the Background stage is parallel execution of tasks. Unlike the sequential execution in the pre-ui, Background tasks run concurrently to maximize efficiency.

Reference Implementation

The complete implementation of the Initializer class, including the executeParallelStage method that powers Background tasks, is available in the Core Implementation Reference document.

Here's how the parallel execution works:

  1. Tasks are grouped by priority level (high, normal, low)
  2. High priority tasks are executed first, in parallel
  3. Once high priority tasks complete, normal priority tasks run in parallel
  4. Finally, low priority tasks run in parallel

This approach provides a balance between parallelism (for efficiency) and prioritization (for user experience).

Data Prefetching

Prefetch essential data during the Background stage:

// core/domains/products/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { queryClient } from '@/core/shared/query/queryClient';
import { productApi } from './api';
import { productQueryKeys } from './queryKeys';
 
const productPrefetchTask: InitTask = {
  name: 'products:prefetch',
  stage: InitStage.BACKGROUND,
  priority: 'high', // Higher priority for better UX
  critical: false, // Non-critical - app can function without prefetched data
  
  async execute(): Promise<void> {
    try {
      // Prefetch featured products
      await queryClient.prefetchQuery({
        queryKey: productQueryKeys.featured(),
        queryFn: productApi.public.getFeaturedProducts,
        staleTime: 5 * 60 * 1000, // 5 minutes
      });
      
      // Prefetch product categories
      await queryClient.prefetchQuery({
        queryKey: productQueryKeys.categories(),
        queryFn: productApi.public.getCategories,
        staleTime: 60 * 60 * 1000, // 1 hour
      });
      
      // Don't prefetch full product catalog - can be fetched on demand
    } catch (error) {
      console.error('Product prefetching failed:', error);
      // Non-critical - app can continue
    }
  }
};
 
// Register task
initializer.registerTask(InitStage.BACKGROUND, productPrefetchTask);

Service Initialization

Initialize non-critical services during the Background stage:

// core/shared/analytics/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { analyticsService } from './service';
import { useAuthStore } from '@/core/domains/auth/store';
 
const analyticsInitTask: InitTask = {
  name: 'analytics:init',
  stage: InitStage.BACKGROUND,
  priority: 'normal',
  critical: false,
  
  async execute(): Promise<void> {
    try {
      // Initialize analytics with configuration
      await analyticsService.initialize({
        enabled: configStore.getConfig().features.analytics,
        debugMode: __DEV__,
        sessionTimeout: 30 * 60 * 1000, // 30 minutes
      });
      
      // Get user ID if authenticated
      const authState = useAuthStore.getState();
      if (authState.isAuthenticated && authState.user) {
        // Set user identity for analytics
        analyticsService.identifyUser(authState.user.id, {
          email: authState.user.email,
          accountType: authState.user.accountType,
        });
      } else {
        // Use anonymous tracking
        analyticsService.trackAnonymous();
      }
      
      // Track app open event
      analyticsService.trackEvent('app_opened', {
        initialScreen: navigationService.getCurrentRouteName(),
        appVersion: DeviceInfo.getVersion(),
        deviceModel: DeviceInfo.getModel(),
      });
    } catch (error) {
      console.error('Analytics initialization failed:', error);
      // Non-critical - app can function without analytics
    }
  }
};
 
// Register task
initializer.registerTask(InitStage.BACKGROUND, analyticsInitTask);

State Synchronization

Synchronize local and remote state during the Background stage:

// core/domains/cart/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { useCartStore } from './store';
import { cartApi } from './api';
import { useAuthStore } from '@/core/domains/auth/store';
import { queryClient } from '@/core/shared/query/queryClient';
import { cartQueryKeys } from './queryKeys';
 
const cartSyncTask: InitTask = {
  name: 'cart:sync',
  stage: InitStage.BACKGROUND,
  priority: 'high',
  critical: false,
  dependencies: ['auth:token-retrieval'], // Depends on auth being initialized
  
  async execute(): Promise<void> {
    try {
      const authState = useAuthStore.getState();
      const cartState = useCartStore.getState();
      
      // Only sync when user is authenticated
      if (authState.isAuthenticated) {
        // Get server cart first
        const serverCart = await cartApi.protected.getCart();
        
        // Check if we have local cart items to merge
        if (cartState.localItems.length > 0) {
          // Merge local cart with server cart
          const mergedCart = await cartApi.protected.syncCart(cartState.localItems);
          
          // Update query cache with merged cart
          queryClient.setQueryData(cartQueryKeys.current(), mergedCart);
          
          // Clear local cart now that it's synced
          useCartStore.getState().clearLocalCart();
        } else {
          // Just set server cart in cache
          queryClient.setQueryData(cartQueryKeys.current(), serverCart);
        }
      }
    } catch (error) {
      console.error('Cart synchronization failed:', error);
      // Non-critical - keep using local cart
    }
  }
};
 
// Register task
initializer.registerTask(InitStage.BACKGROUND, cartSyncTask);

Push Notification Setup

Configure push notifications during the Background stage:

// core/shared/notifications/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { notificationService } from './service';
import { permissionService } from '@/core/shared/permissions/service';
import { useAuthStore } from '@/core/domains/auth/store';
 
const pushNotificationsTask: InitTask = {
  name: 'notifications:push-setup',
  stage: InitStage.BACKGROUND,
  priority: 'low', // Lower priority than core features
  critical: false,
  
  async execute(): Promise<void> {
    try {
      // Check if notifications are enabled in app config
      if (!configStore.getConfig().features.pushNotifications) {
        return; // Feature disabled, exit early
      }
      
      // Initialize the notification library
      await notificationService.initialize();
      
      // Check if we have permission
      const hasPermission = await permissionService.checkPermission('notifications');
      
      if (hasPermission) {
        // Register for push notifications
        const token = await notificationService.registerForPushNotifications();
        
        // Only send token to backend if user is authenticated
        const authState = useAuthStore.getState();
        if (authState.isAuthenticated && token) {
          // Register device token with backend
          await notificationApi.protected.registerDevice(token, {
            deviceId: DeviceInfo.getUniqueId(),
            deviceType: Platform.OS,
            appVersion: DeviceInfo.getVersion(),
          });
        }
        
        // Set up notification handlers
        notificationService.setupNotificationHandlers({
          onNotificationReceived: this.handleNotificationReceived,
          onNotificationOpened: this.handleNotificationOpened,
        });
      } else {
        // Store this state so we can prompt user later
        notificationService.setPermissionState('denied');
      }
    } catch (error) {
      console.error('Push notification setup failed:', error);
      // Non-critical - app can function without push notifications
    }
  },
  
  handleNotificationReceived(notification) {
    // Handle incoming notification
    analyticsService.trackEvent('notification_received', {
      type: notification.type,
    });
  },
  
  handleNotificationOpened(notification) {
    // Navigate based on notification type
    switch (notification.type) {
      case 'order_update':
        navigationService.navigate('OrderDetails', { 
          orderId: notification.data.orderId 
        });
        break;
      case 'promotion':
        navigationService.navigate('Promotions');
        break;
      // Other notification types
    }
    
    analyticsService.trackEvent('notification_opened', {
      type: notification.type,
    });
  }
};
 
// Register task
initializer.registerTask(InitStage.BACKGROUND, pushNotificationsTask);

Task Progress Reporting

Implement progress reporting for background tasks:

// core/shared/app/initialization/progress.ts
import { EventEmitter } from '@/core/shared/utils/events';
 
export type TaskProgressEvents = {
  'task:progress': {
    taskName: string;
    progress: number;
    message?: string;
  };
};
 
export const taskProgressEvents = new EventEmitter<TaskProgressEvents>();
 
// Enhanced task execution with progress reporting
export function createTaskWithProgress(
  name: string,
  executor: (
    reportProgress: (progress: number, message?: string) => void
  ) => Promise<void>
): InitTask {
  return {
    name,
    async execute(): Promise<void> {
      // Create progress reporter function for this task
      const reportProgress = (progress: number, message?: string) => {
        taskProgressEvents.emit('task:progress', {
          taskName: name,
          progress,
          message,
        });
      };
      
      // Start at 0%
      reportProgress(0, 'Starting...');
      
      // Execute with progress reporting capability
      await executor(reportProgress);
      
      // Complete at 100%
      reportProgress(1, 'Complete');
    }
  };
}
 
// Usage example:
const assetPreloadTask = createTaskWithProgress(
  'assets:preload',
  async (reportProgress) => {
    const assetUrls = await getAssetUrls();
    const totalAssets = assetUrls.length;
    
    for (let i = 0; i < totalAssets; i++) {
      await preloadAsset(assetUrls[i]);
      
      // Report progress percentage and status message
      reportProgress(
        (i + 1) / totalAssets,
        `Loading assets (${i + 1}/${totalAssets})`
      );
    }
  }
);

Common Challenges

Error Handling and Recovery

Implement robust error handling for background tasks:

// core/shared/app/initialization/error-handling.ts
import { initEvents } from './events';
 
// Register global error handler for initialization tasks
export function setupTaskErrorHandling() {
  initEvents.on('task:error', ({ name, stage, error }) => {
    // Log error to monitoring service
    monitoringService.logError(error, {
      context: 'initialization',
      taskName: name,
      stage,
    });
    
    // Show non-blocking error notification for important tasks
    const criticalBackgroundTasks = [
      'push-notification:setup',
      'cart:sync',
    ];
    
    if (stage === InitStage.BACKGROUND && criticalBackgroundTasks.includes(name)) {
      notificationService.showErrorBanner({
        message: `Failed to initialize ${name.split(':')[0]}`,
        actionLabel: 'Retry',
        onAction: () => {
          // Find the task and retry it
          const task = initializer.getTask(name);
          if (task) {
            task.execute().catch(console.error);
          }
        },
      });
    }
  });
}

Task Timeout Management

Implement timeouts for background tasks to prevent hanging:

// core/shared/app/initialization/timeout.ts
import { InitTask } from './initializer';
 
// Higher-order function to add timeout to any initialization task
export function withTimeout(
  task: InitTask,
  timeoutMs: number = 30000 // Default 30s timeout
): InitTask {
  return {
    ...task,
    name: task.name,
    async execute(): Promise<void> {
      return new Promise(async (resolve, reject) => {
        // Create timeout
        const timeoutId = setTimeout(() => {
          const error = new Error(`Task ${task.name} timed out after ${timeoutMs}ms`);
          console.error(error);
          
          // Only reject for critical tasks, resolve with warning for non-critical
          if (task.critical) {
            reject(error);
          } else {
            console.warn(`Non-critical task ${task.name} timed out, continuing initialization`);
            resolve();
          }
        }, timeoutMs);
        
        try {
          // Execute original task
          await task.execute();
          clearTimeout(timeoutId);
          resolve();
        } catch (error) {
          clearTimeout(timeoutId);
          reject(error);
        }
      });
    }
  };
}
 
// Usage:
const dataImportTask = withTimeout({
  name: 'data:import',
  execute: async () => {
    // Potentially long-running task
    await importLargeDataset();
  }
}, 60000); // 60 second timeout

Resource-Intensive Operations

Handle resource-intensive operations carefully:

// core/shared/utils/resource-management.ts
import { AppState } from 'react-native';
 
// Split large operation into smaller chunks with yield points
export async function executeChunked<T>(
  items: T[],
  processor: (item: T) => Promise<void>,
  options: {
    chunkSize?: number;
    delayBetweenChunks?: number;
    onProgress?: (completed: number, total: number) => void;
  } = {}
): Promise<void> {
  const { 
    chunkSize = 10, 
    delayBetweenChunks = 50,
    onProgress
  } = options;
  
  const total = items.length;
  let completed = 0;
  
  // Process in chunks
  for (let i = 0; i < total; i += chunkSize) {
    // Only process when app is in foreground
    await waitForActiveAppState();
    
    // Take a chunk
    const chunk = items.slice(i, i + chunkSize);
    
    // Process chunk items in parallel
    await Promise.all(chunk.map(item => processor(item)));
    
    completed += chunk.length;
    
    // Report progress
    if (onProgress) {
      onProgress(completed, total);
    }
    
    // Small delay to let UI thread breathe
    if (i + chunkSize < total) {
      await new Promise(resolve => setTimeout(resolve, delayBetweenChunks));
    }
  }
}
 
// Wait for app to be in active state
async function waitForActiveAppState(): Promise<void> {
  if (AppState.currentState === 'active') {
    return;
  }
  
  return new Promise(resolve => {
    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') {
        subscription.remove();
        resolve();
      }
    });
  });
}
 
// Usage example:
// In a background initialization task
await executeChunked(
  largeDataset,
  async (item) => {
    await processDataItem(item);
  },
  {
    chunkSize: 20,
    delayBetweenChunks: 100,
    onProgress: (completed, total) => {
      reportProgress(completed / total, `Processing ${completed}/${total} items`);
    }
  }
);

Performance Considerations

  • (Do ✅) Use task prioritization

    • Higher priority for user-visible features
    • Normal priority for standard services
    • Lower priority for analytics and non-critical services
  • (Do ✅) Implement progressive loading

    • Load data in smaller chunks
    • Prioritize data for visible UI elements
    • Defer off-screen content loading
  • (Don't ❌) Block the main thread

    • Move heavy calculations to separate threads
    • Break large operations into smaller chunks
    • Add yield points to allow the UI to remain responsive
  • (Do ✅) Respect device capabilities

    • Scale operations based on device performance
    • Reduce workload on low-end devices
    • Adjust parallelism based on available resources

Practical Examples

Asset Preloading System

// core/shared/assets/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { createTaskWithProgress } from '@/core/shared/app/initialization/progress';
import { executeChunked } from '@/core/shared/utils/resource-management';
import { assetStore } from './store';
import { Image } from 'react-native';
 
// Preload important images and assets
const assetPreloadTask = createTaskWithProgress(
  'assets:preload',
  async (reportProgress) => {
    try {
      // Get list of important assets to preload
      const assetsToPreload = [
        // Common UI elements
        require('@/assets/images/logo.png'),
        require('@/assets/images/background.png'),
        
        // Category icons
        ...getCategoryIcons(),
        
        // Other critical assets
      ];
      
      // Preload images using chunked execution
      await executeChunked(
        assetsToPreload,
        async (asset) => {
          await new Promise<void>((resolve) => {
            if (typeof asset === 'number') {
              // Local asset (from require)
              Image.prefetch(Image.resolveAssetSource(asset).uri)
                .then(() => resolve())
                .catch(() => resolve()); // Continue even if one fails
            } else {
              // Remote URL
              Image.prefetch(asset)
                .then(() => resolve())
                .catch(() => resolve());
            }
          });
        },
        {
          chunkSize: 5,
          delayBetweenChunks: 50,
          onProgress: (completed, total) => {
            reportProgress(
              completed / total,
              `Preloading assets (${completed}/${total})`
            );
          },
        }
      );
      
      // Mark assets as preloaded
      assetStore.setAssetsPreloaded(true);
    } catch (error) {
      console.error('Asset preloading failed:', error);
      // Non-critical - app can function without preloaded assets
    }
  }
);
 
// Register task
initializer.registerTask(InitStage.BACKGROUND, 
  { ...assetPreloadTask, priority: 'normal' }
);
 
// Helper to get category icons
function getCategoryIcons() {
  return [
    require('@/assets/images/categories/electronics.png'),
    require('@/assets/images/categories/fashion.png'),
    require('@/assets/images/categories/home.png'),
    // More categories
  ];
}

Offline Data Synchronization

// core/shared/offline/initialization.ts
import { initializer, InitStage, InitTask } from '@/core/shared/app/initialization';
import { offlineStore } from './store';
import { storage } from '@/core/shared/utils/storage';
import { executeChunked } from '@/core/shared/utils/resource-management';
import { createTaskWithProgress } from '@/core/shared/app/initialization/progress';
import { withTimeout } from '@/core/shared/app/initialization/timeout';
 
// Synchronize pending offline actions
const offlineSyncTask = createTaskWithProgress(
  'offline:sync-pending-actions',
  async (reportProgress) => {
    try {
      // Check for network connectivity first
      const isConnected = await networkService.isConnected();
      if (!isConnected) {
        console.log('Device offline, skipping offline sync');
        return;
      }
      
      // Load pending offline actions
      const pendingActions = await storage.get('offline:pending-actions');
      
      if (!pendingActions) {
        console.log('No pending offline actions to sync');
        return;
      }
      
      const actions = JSON.parse(pendingActions);
      
      if (actions.length === 0) {
        console.log('No pending offline actions to sync');
        return;
      }
      
      console.log(`Found ${actions.length} pending offline actions to sync`);
      
      // Process actions in chunks
      await executeChunked(
        actions,
        async (action) => {
          try {
            await processPendingAction(action);
            
            // Remove from pending list
            offlineStore.removeAction(action.id);
          } catch (error) {
            console.error(`Failed to process offline action ${action.id}:`, error);
            
            // Mark as failed but keep for retry
            offlineStore.markActionFailed(action.id, error.message);
          }
        },
        {
          chunkSize: 1, // Process one at a time for reliability
          delayBetweenChunks: 300, // Larger delay to avoid overloading API
          onProgress: (completed, total) => {
            reportProgress(
              completed / total,
              `Syncing offline actions (${completed}/${total})`
            );
          },
        }
      );
      
      // Update storage with remaining actions
      const remainingActions = offlineStore.getPendingActions();
      if (remainingActions.length > 0) {
        await storage.set('offline:pending-actions', JSON.stringify(remainingActions));
      } else {
        await storage.remove('offline:pending-actions');
      }
    } catch (error) {
      console.error('Offline sync failed:', error);
      // Non-critical - can retry later
    }
  }
);
 
// Process a single offline action
async function processPendingAction(action) {
  switch (action.type) {
    case 'create_order':
      await orderApi.protected.createOrder(action.payload);
      break;
    case 'update_cart':
      await cartApi.protected.updateCart(action.payload);
      break;
    // Other action types
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}
 
// Register task with timeout
initializer.registerTask(
  InitStage.BACKGROUND,
  withTimeout(
    { ...offlineSyncTask, priority: 'normal' },
    60000 // 1 minute timeout
  )
);

Migration Considerations

When transitioning from traditional data loading patterns to the Background initialization stage, these strategies can help make your migration smoother:

From Eager Loading to Progressive Loading

Many applications traditionally fetch all data upfront, which can delay user interaction:

// Traditional approach - blocking data loading
const HomeScreen = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState({
    products: [],
    categories: [],
    user: null,
    notifications: []
  });
  
  useEffect(() => {
    async function loadEverything() {
      try {
        // Fetch everything at once, blocking UI until complete
        const [products, categories, user, notifications] = await Promise.all([
          api.getProducts(),
          api.getCategories(),
          api.getUserProfile(),
          api.getNotifications()
        ]);
        
        setData({ products, categories, user, notifications });
        setIsLoading(false);
      } catch (error) {
        console.error('Data loading failed', error);
        setIsLoading(false);
      }
    }
    
    loadEverything();
  }, []);
  
  if (isLoading) {
    return <LoadingScreen />;
  }
  
  return (
    <Screen>
      <UserProfile user={data.user} />
      <ProductCarousel products={data.products} />
      <CategoryList categories={data.categories} />
      <NotificationPanel notifications={data.notifications} />
    </Screen>
  );
};

You can enhance this with the Background stage approach:

  1. First render the UI shell with minimal data from the Initial UI stage
  2. Then prioritize data loading in the Background stage based on importance
  3. Show appropriate loading states for each component as its data loads

Prioritized Data Loading Pattern

A key migration strategy is to prioritize data loading:

// Background stage approach
const HomeScreen = () => {
  const { currentStage } = useInitialization();
  const { data: user } = useUserProfile(); // Essential - load in Initial UI stage
  const { data: products, isLoading: productsLoading } = useProducts();
  const { data: categories, isLoading: categoriesLoading } = useCategories();
  const { data: notifications, isLoading: notificationsLoading } = useNotifications();
  
  // Register initialization tasks once
  useEffect(() => {
    // Register tasks for data prefetching in Background stage
    initializer.registerTask(InitStage.BACKGROUND, {
      name: 'home:products-prefetch',
      priority: 'high', // High priority - visible at top of screen
      execute: async () => {
        await queryClient.prefetchQuery(productKeys.featured);
      }
    });
    
    initializer.registerTask(InitStage.BACKGROUND, {
      name: 'home:categories-prefetch',
      priority: 'normal',
      execute: async () => {
        await queryClient.prefetchQuery(categoryKeys.list);
      }
    });
    
    initializer.registerTask(InitStage.BACKGROUND, {
      name: 'home:notifications-prefetch',
      priority: 'low', // Low priority - below the fold
      execute: async () => {
        await queryClient.prefetchQuery(notificationKeys.recent);
      }
    });
  }, []);
  
  return (
    <Screen>
      <UserProfile user={user} />
      <ProductCarousel 
        products={products} 
        isLoading={currentStage === InitStage.BACKGROUND || productsLoading} 
      />
      <CategoryList 
        categories={categories} 
        isLoading={currentStage === InitStage.BACKGROUND || categoriesLoading} 
      />
      <NotificationPanel 
        notifications={notifications} 
        isLoading={currentStage === InitStage.BACKGROUND || notificationsLoading} 
      />
    </Screen>
  );
};

This approach improves perceived performance by showing content progressively as it becomes available.

Tracking and Optimizing Data Loading

When adopting the Background stage pattern, consider implementing these improvements:

  • Add progress reporting to long-running background tasks
  • Use chunk strategies for large data sets to avoid blocking the JavaScript thread
  • Implement timeout safety for API calls to prevent stalled initialization
  • Cache aggressively to reduce repeated data fetching across app launches

Remember that the Background stage is designed for parallel execution, so leverage that to maximize efficiency while maintaining a responsive UI.

Summary

The Background initialization stage allows your application to continue setting up important services and prefetching data without blocking the initial UI rendering. By carefully implementing and prioritizing background tasks, you can create a smooth, responsive user experience that progressively enhances as initialization completes.

After all Background tasks complete, your app transitions to the finalization to complete the initialization process and remove any remaining loading indicators.

Key principles to remember:

  • Execute tasks in parallel to maximize efficiency
  • Prioritize tasks based on their importance to the user experience
  • Implement proper error handling and recovery strategies
  • Use progress reporting to provide feedback to users
  • Be mindful of device resources and performance
  • Break large operations into smaller chunks with yield points

Core Initialization Documentation

Other Initialization Stages

  • pre-ui - First initialization stage
  • initial-ui - Stage before Background initialization
  • finalization - Final stage after Background completion