UTA DevHub

Performance Optimization

Advanced performance optimization techniques for icon systems including bundle analysis, lazy loading, caching, and performance monitoring

Icon Performance Optimization

Overview

Icon performance significantly impacts app startup time and memory usage. This guide covers optimization strategies from bundle analysis to runtime performance monitoring, ensuring your icon system scales efficiently.

Bundle Size Optimization

Import Analysis and Tree Shaking

Bundle Impact: Improper icon imports can increase bundle size by 50-200KB per icon library.

// ❌ BAD: Imports entire icon library
import * as Icons from '@expo/vector-icons/Ionicons';
import * as CustomIcons from '@/ui/foundation/icons/custom';
 
// ✅ GOOD: Import specific icons only
import { Home, Search, Profile } from '@expo/vector-icons/Ionicons';
import { ArrowLeftIcon, MenuIcon } from '@/ui/foundation/icons/custom';
 
// ✅ GOOD: Dynamic imports for better code splitting
const LazyIcon = React.lazy(() => import('@/ui/foundation/icons/custom/ComplexIcon'));

Metro Bundle Analyzer Integration

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
 
const config = getDefaultConfig(__dirname);
 
// Enable bundle analysis in development
if (process.env.NODE_ENV === 'development') {
  config.transformer = {
    ...config.transformer,
    unstable_allowRequireContext: true,
  };
  
  // Add bundle size analysis
  config.serializer = {
    ...config.serializer,
    customSerializer: require('./scripts/bundle-analyzer'),
  };
}
 
module.exports = config;

Custom Icon Bundle Analysis

// scripts/icon-bundle-analyzer.ts
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
 
interface IconBundleStats {
  totalIcons: number;
  totalSize: number;
  averageSize: number;
  largestIcons: Array<{name: string; size: number}>;
  recommendations: string[];
}
 
export class IconBundleAnalyzer {
  private customIconsPath = 'ui/foundation/icons/custom';
  
  analyze(): IconBundleStats {
    const icons = this.scanCustomIcons();
    const stats = this.calculateStats(icons);
    const recommendations = this.generateRecommendations(icons, stats);
    
    return {
      ...stats,
      recommendations,
    };
  }
  
  private scanCustomIcons() {
    const iconFiles = fs.readdirSync(this.customIconsPath)
      .filter(file => file.endsWith('.tsx'))
      .map(file => {
        const filePath = path.join(this.customIconsPath, file);
        const content = fs.readFileSync(filePath, 'utf8');
        const size = Buffer.byteLength(content, 'utf8');
        
        return {
          name: file.replace('.tsx', ''),
          size,
          complexity: this.analyzeComplexity(content),
        };
      });
    
    return iconFiles;
  }
  
  private analyzeComplexity(content: string): 'low' | 'medium' | 'high' {
    const pathCount = (content.match(/<path/g) || []).length;
    const transformCount = (content.match(/transform/g) || []).length;
    
    if (pathCount > 10 || transformCount > 3) return 'high';
    if (pathCount > 5 || transformCount > 1) return 'medium';
    return 'low';
  }
  
  private generateRecommendations(icons: any[], stats: IconBundleStats): string[] {
    const recommendations = [];
    
    const largeIcons = icons.filter(icon => icon.size > 5000);
    if (largeIcons.length > 0) {
      recommendations.push(`Consider optimizing ${largeIcons.length} large icons (>5KB)`);
    }
    
    const complexIcons = icons.filter(icon => icon.complexity === 'high');
    if (complexIcons.length > 0) {
      recommendations.push(`Simplify ${complexIcons.length} complex icons for better performance`);
    }
    
    if (stats.totalIcons > 50) {
      recommendations.push('Consider implementing lazy loading for icons');
    }
    
    return recommendations;
  }
}

Runtime Performance Optimization

Icon Caching Strategy

// ui/foundation/Icon/IconCache.ts
class IconCache {
  private static instance: IconCache;
  private cache = new Map<string, React.ComponentType>();
  private preloadQueue = new Set<string>();
  
  static getInstance(): IconCache {
    if (!IconCache.instance) {
      IconCache.instance = new IconCache();
    }
    return IconCache.instance;
  }
  
  get(key: string): React.ComponentType | null {
    return this.cache.get(key) || null;
  }
  
  set(key: string, component: React.ComponentType): void {
    this.cache.set(key, component);
    this.preloadQueue.delete(key);
  }
  
  async preload(iconNames: string[]): Promise<void> {
    const promises = iconNames
      .filter(name => !this.cache.has(name))
      .map(name => this.loadIcon(name));
    
    await Promise.allSettled(promises);
  }
  
  private async loadIcon(name: string): Promise<void> {
    try {
      const iconModule = await import(`../icons/custom/${name}Icon`);
      this.cache.set(name, iconModule.default);
    } catch (error) {
      console.warn(`Failed to preload icon: ${name}`);
    }
  }
  
  getCacheStats() {
    return {
      size: this.cache.size,
      memoryUsage: this.cache.size * 1024, // Rough estimate
      hitRate: this.calculateHitRate(),
    };
  }
  
  private calculateHitRate(): number {
    // Implementation depends on usage tracking
    return 0.85; // Placeholder
  }
}

Lazy Loading Implementation

// ui/foundation/Icon/LazyIcon.tsx
import React, { Suspense, useState, useEffect } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { IconCache } from './IconCache';
 
interface LazyIconProps {
  name: string;
  size?: number;
  color?: string;
  fallback?: React.ReactNode;
  preload?: boolean;
}
 
export const LazyIcon: React.FC<LazyIconProps> = ({
  name,
  size = 24,
  color = 'currentColor',
  fallback,
  preload = false,
  ...props
}) => {
  const [Component, setComponent] = useState<React.ComponentType | null>(null);
  const cache = IconCache.getInstance();
  
  useEffect(() => {
    const cached = cache.get(name);
    if (cached) {
      setComponent(() => cached);
      return;
    }
    
    if (preload) {
      cache.preload([name]);
    }
    
    const loadComponent = async () => {
      try {
        const iconModule = await import(`../icons/custom/${name}Icon`);
        const LoadedComponent = iconModule.default;
        
        cache.set(name, LoadedComponent);
        setComponent(() => LoadedComponent);
      } catch (error) {
        console.warn(`Failed to load icon: ${name}`);
      }
    };
    
    loadComponent();
  }, [name, preload, cache]);
  
  const defaultFallback = (
    <View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
      <ActivityIndicator size="small" color={color} />
    </View>
  );
  
  if (!Component) {
    return fallback || defaultFallback;
  }
  
  return <Component size={size} color={color} {...props} />;
};

Memory Management

// ui/foundation/Icon/MemoryManager.ts
export class IconMemoryManager {
  private static maxCacheSize = 100; // Maximum cached icons
  private static memoryThreshold = 50 * 1024 * 1024; // 50MB threshold
  
  static cleanup(): void {
    const cache = IconCache.getInstance();
    const stats = cache.getCacheStats();
    
    if (stats.memoryUsage > this.memoryThreshold) {
      this.performCleanup(cache);
    }
  }
  
  private static performCleanup(cache: IconCache): void {
    // Implement LRU cleanup strategy
    const usage = this.getUsageStats();
    const leastUsed = usage
      .sort((a, b) => a.lastUsed - b.lastUsed)
      .slice(0, Math.floor(cache.getCacheStats().size * 0.3));
    
    leastUsed.forEach(item => {
      cache.delete(item.name);
    });
    
    console.log(`Cleaned up ${leastUsed.length} cached icons`);
  }
  
  private static getUsageStats(): Array<{name: string; lastUsed: number}> {
    // Implementation depends on usage tracking
    return [];
  }
}

Critical Icon Preloading

App Startup Optimization

// core/performance/IconPreloader.ts
export class IconPreloader {
  private static criticalIcons = [
    'home', 'search', 'profile', 'menu', 'back', 'close'
  ];
  
  static async preloadCriticalIcons(): Promise<void> {
    const startTime = performance.now();
    
    try {
      const cache = IconCache.getInstance();
      await cache.preload(this.criticalIcons);
      
      const duration = performance.now() - startTime;
      console.log(`Preloaded ${this.criticalIcons.length} icons in ${duration.toFixed(2)}ms`);
      
    } catch (error) {
      console.warn('Icon preloading failed:', error);
    }
  }
  
  static preloadForRoute(routeName: string): Promise<void> {
    const routeIcons = this.getRouteIcons(routeName);
    const cache = IconCache.getInstance();
    return cache.preload(routeIcons);
  }
  
  private static getRouteIcons(routeName: string): string[] {
    const routeIconMap: Record<string, string[]> = {
      'Home': ['home', 'notification', 'search'],
      'Profile': ['profile', 'edit', 'settings'],
      'Settings': ['settings', 'help', 'logout'],
      // Add more route mappings
    };
    
    return routeIconMap[routeName] || [];
  }
}
 
// App.tsx - Preload during app initialization
export default function App() {
  useEffect(() => {
    IconPreloader.preloadCriticalIcons();
  }, []);
  
  // ... rest of app
}

Route-based Preloading

// navigation/IconPreloadingNavigator.tsx
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { IconPreloader } from '@/core/performance/IconPreloader';
 
export const useRouteIconPreloading = () => {
  const navigation = useNavigation();
  
  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', (e) => {
      const routeName = e.target?.split('-')[0];
      if (routeName) {
        IconPreloader.preloadForRoute(routeName);
      }
    });
    
    return unsubscribe;
  }, [navigation]);
};

Performance Monitoring

Icon Performance Metrics

// utils/IconPerformanceMonitor.ts
interface IconMetrics {
  loadTime: number;
  renderTime: number;
  cacheHit: boolean;
  iconName: string;
  size: number;
}
 
export class IconPerformanceMonitor {
  private static metrics: IconMetrics[] = [];
  private static maxMetrics = 1000;
  
  static measureIconLoad<T>(
    iconName: string,
    loadFn: () => Promise<T>
  ): Promise<T> {
    const startTime = performance.now();
    
    return loadFn().then(result => {
      const loadTime = performance.now() - startTime;
      
      this.recordMetric({
        iconName,
        loadTime,
        renderTime: 0,
        cacheHit: false,
        size: 0,
      });
      
      return result;
    });
  }
  
  static measureIconRender(iconName: string, renderFn: () => void): void {
    const startTime = performance.now();
    renderFn();
    const renderTime = performance.now() - startTime;
    
    this.recordMetric({
      iconName,
      loadTime: 0,
      renderTime,
      cacheHit: true,
      size: 0,
    });
  }
  
  private static recordMetric(metric: IconMetrics): void {
    this.metrics.push(metric);
    
    if (this.metrics.length > this.maxMetrics) {
      this.metrics = this.metrics.slice(-this.maxMetrics);
    }
  }
  
  static getPerformanceReport(): {
    averageLoadTime: number;
    averageRenderTime: number;
    cacheHitRate: number;
    slowestIcons: Array<{name: string; time: number}>;
  } {
    const loadTimes = this.metrics.map(m => m.loadTime);
    const renderTimes = this.metrics.map(m => m.renderTime);
    const cacheHits = this.metrics.filter(m => m.cacheHit).length;
    
    const slowestIcons = this.metrics
      .sort((a, b) => (b.loadTime + b.renderTime) - (a.loadTime + a.renderTime))
      .slice(0, 10)
      .map(m => ({ name: m.iconName, time: m.loadTime + m.renderTime }));
    
    return {
      averageLoadTime: loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length,
      averageRenderTime: renderTimes.reduce((a, b) => a + b, 0) / renderTimes.length,
      cacheHitRate: cacheHits / this.metrics.length,
      slowestIcons,
    };
  }
}

Development Performance Tools

// tools/IconDebugger.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Button, ScrollView } from 'react-native';
import { IconPerformanceMonitor } from '@/utils/IconPerformanceMonitor';
import { IconCache } from '@/ui/foundation/Icon/IconCache';
 
export const IconDebugger: React.FC = () => {
  const [report, setReport] = useState<any>(null);
  const [cacheStats, setCacheStats] = useState<any>(null);
  
  useEffect(() => {
    updateStats();
    const interval = setInterval(updateStats, 5000);
    return () => clearInterval(interval);
  }, []);
  
  const updateStats = () => {
    setReport(IconPerformanceMonitor.getPerformanceReport());
    setCacheStats(IconCache.getInstance().getCacheStats());
  };
  
  const clearCache = () => {
    IconCache.getInstance().clear();
    updateStats();
  };
  
  if (!__DEV__) return null;
  
  return (
    <View style={{ padding: 20, backgroundColor: '#f0f0f0' }}>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Icon Performance</Text>
      
      {report && (
        <View style={{ marginTop: 10 }}>
          <Text>Avg Load Time: {report.averageLoadTime?.toFixed(2)}ms</Text>
          <Text>Avg Render Time: {report.averageRenderTime?.toFixed(2)}ms</Text>
          <Text>Cache Hit Rate: {(report.cacheHitRate * 100)?.toFixed(1)}%</Text>
        </View>
      )}
      
      {cacheStats && (
        <View style={{ marginTop: 10 }}>
          <Text>Cached Icons: {cacheStats.size}</Text>
          <Text>Memory Usage: ~{(cacheStats.memoryUsage / 1024).toFixed(1)}KB</Text>
        </View>
      )}
      
      <Button title="Clear Cache" onPress={clearCache} />
      
      {report?.slowestIcons && (
        <ScrollView style={{ maxHeight: 200, marginTop: 10 }}>
          <Text style={{ fontWeight: 'bold' }}>Slowest Icons:</Text>
          {report.slowestIcons.map((icon: any, index: number) => (
            <Text key={index}>{icon.name}: {icon.time.toFixed(2)}ms</Text>
          ))}
        </ScrollView>
      )}
    </View>
  );
};

Production Optimization Strategies

Build-time Optimization

// scripts/optimize-icons-build.js
const fs = require('fs');
const path = require('path');
const svgo = require('svgo');
 
class BuildTimeIconOptimizer {
  constructor() {
    this.svgoConfig = {
      plugins: [
        'preset-default',
        'removeDoctype',
        'removeComments',
        'removeMetadata',
        'removeViewBox',
        'removeDimensions',
      ],
    };
  }
  
  async optimizeForProduction() {
    const iconsDir = 'ui/foundation/icons/custom';
    const files = fs.readdirSync(iconsDir)
      .filter(file => file.endsWith('.tsx'));
    
    console.log(`Optimizing ${files.length} icons for production...`);
    
    for (const file of files) {
      await this.optimizeIconFile(path.join(iconsDir, file));
    }
    
    console.log('Icon optimization complete!');
  }
  
  async optimizeIconFile(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    
    // Extract SVG content
    const svgMatch = content.match(/<svg[^>]*>.*?<\/svg>/s);
    if (!svgMatch) return;
    
    const optimizedSvg = await svgo.optimize(svgMatch[0], this.svgoConfig);
    const optimizedContent = content.replace(svgMatch[0], optimizedSvg.data);
    
    fs.writeFileSync(filePath, optimizedContent);
  }
}
 
if (require.main === module) {
  const optimizer = new BuildTimeIconOptimizer();
  optimizer.optimizeForProduction();
}

Runtime Performance Best Practices

// ✅ GOOD: Efficient icon usage patterns
 
// 1. Use memoization for expensive icons
const MemoizedIcon = React.memo(Icon);
 
// 2. Preload critical icons at app start
useEffect(() => {
  IconPreloader.preloadCriticalIcons();
}, []);
 
// 3. Use appropriate sizes
<Icon name="home" size={24} /> // Standard size
<Icon name="hero" size={48} />  // Larger for hero sections
 
// 4. Implement proper caching
const iconCache = IconCache.getInstance();
 
// 5. Monitor performance in development
if (__DEV__) {
  IconPerformanceMonitor.measureIconRender('home', renderIcon);
}

Testing Performance

Performance Test Suite

// __tests__/icon-performance.test.ts
import { IconPerformanceMonitor } from '@/utils/IconPerformanceMonitor';
import { IconCache } from '@/ui/foundation/Icon/IconCache';
import { render } from '@testing-library/react-native';
import { Icon } from '@/ui/foundation/Icon/Icon';
 
describe('Icon Performance', () => {
  beforeEach(() => {
    IconCache.getInstance().clear();
  });
  
  it('should load icons within performance thresholds', async () => {
    const startTime = performance.now();
    
    const { findByTestId } = render(
      <Icon name="home" testID="performance-icon" />
    );
    
    await findByTestId('performance-icon');
    const loadTime = performance.now() - startTime;
    
    expect(loadTime).toBeLessThan(100); // Should load within 100ms
  });
  
  it('should cache icons effectively', async () => {
    const cache = IconCache.getInstance();
    
    // First load
    await cache.preload(['home']);
    const firstStats = cache.getCacheStats();
    
    // Second access should be from cache
    const cachedIcon = cache.get('home');
    
    expect(cachedIcon).toBeTruthy();
    expect(firstStats.size).toBe(1);
  });
  
  it('should handle memory cleanup', () => {
    const cache = IconCache.getInstance();
    
    // Fill cache
    for (let i = 0; i < 150; i++) {
      cache.set(`icon-${i}`, jest.fn());
    }
    
    // Trigger cleanup
    IconMemoryManager.cleanup();
    
    const stats = cache.getCacheStats();
    expect(stats.size).toBeLessThan(150); // Should have cleaned up
  });
});