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
});
});Related Documents
- Vector Icons Guide - Foundation for vector libraries
- Custom Icons Guide - SVG workflow and optimization
- Implementation Patterns - Advanced architectural patterns