53% of users abandon apps that take over 3 seconds to load. Every 100ms delay decreases conversion by 7%. This guide shows how to optimize performance and keep users engaged.
Performance Impact
- 53% abandon if load time > 3 seconds
- 100ms delay = 7% conversion drop
- 1-star increase in rating = 8% revenue boost
- Slow apps get 1-star reviews 2x more
App Startup Optimization
Cold Start Optimization
Target times:
- Cold start: < 1.5 seconds
- Warm start: < 1 second
- Hot start: < 500ms
Optimization strategy:
1. Defer non-critical initialization
2. Lazy load heavy resources
3. Minimize Application onCreate
4. Preload critical data only
5. Use splash screen effectively
iOS Launch Optimization
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Critical only
setupWindow()
configureFirebase()
// Defer to background
DispatchQueue.global(qos: .utility).async {
self.setupAnalytics()
self.fetchRemoteConfig()
self.initializeSDKs()
}
return true
}
Android Launch Optimization
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Critical initialization
Timber.plant(Timber.DebugTree())
// Use WorkManager for deferred tasks
WorkManager.getInstance(this).enqueue(
OneTimeWorkRequestBuilder()
.setInitialDelay(2, TimeUnit.SECONDS)
.build()
)
}
}
Memory Management
iOS Memory Best Practices
// Use weak references to avoid retain cycles
class ViewController: UIViewController {
weak var delegate: MyDelegate?
// Lazy loading
lazy var heavyResource: HeavyObject = {
return HeavyObject()
}()
// Release resources
deinit {
NotificationCenter.default.removeObserver(self)
heavyResource.cleanup()
}
}
// Image loading optimization
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.sd_setImage(with: url, placeholderImage: placeholder)
.retry(maxAttempts: 3)
.downsampling(size: targetSize)
Android Memory Best Practices
// Use lifecycle-aware components
class MyActivity : AppCompatActivity() {
// Avoid memory leaks
private val viewModel: MyViewModel by viewModels()
override fun onDestroy() {
super.onDestroy()
// Clean up references
binding = null
adapter = null
}
}
// Image loading with Glide
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.override(targetWidth, targetHeight)
.placeholder(R.drawable.placeholder)
.into(imageView)
Network Optimization
API Request Optimization
Best practices:
✓ Compress requests (gzip)
✓ Batch API calls
✓ Implement caching strategy
✓ Use pagination
✓ Implement retry with exponential backoff
✓ Cancel unused requests
// Request batching
class APIBatcher {
private var pendingRequests: [Request] = []
func addRequest(_ request: Request) {
pendingRequests.append(request)
// Batch every 100ms or 10 requests
if pendingRequests.count >= 10 || timer.elapsed > 0.1 {
executeBatch()
}
}
}
Image Optimization
- Use WebP format (30% smaller than PNG)
- Serve multiple resolutions (1x, 2x, 3x)
- Lazy load images below fold
- Implement progressive loading
- Cache aggressively
Caching Strategy
Cache levels:
1. Memory cache (fastest, limited)
2. Disk cache (fast, larger)
3. Network (slow, always fresh)
Implementation:
let cache = URLCache(
memoryCapacity: 50_000_000, // 50 MB
diskCapacity: 200_000_000, // 200 MB
diskPath: "myCache"
)
URLSession.shared.configuration.urlCache = cache
URLSession.shared.configuration.requestCachePolicy = .returnCacheDataElseLoad
UI Performance
Smooth Scrolling
iOS - UITableView/UICollectionView:
- Reuse cells properly
- Avoid heavy calculations in cellForRow
- Preload images before scrolling
- Use opaque views when possible
- Minimize view hierarchy depth
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell", for: indexPath
)
// Fast operation only
cell.textLabel?.text = items[indexPath.row].title
// Defer heavy operations
DispatchQueue.global().async {
let image = self.processImage(at: indexPath.row)
DispatchQueue.main.async {
cell.imageView?.image = image
}
}
return cell
}
Android RecyclerView Optimization
class MyAdapter : RecyclerView.Adapter() {
// ViewHolder pattern (built-in)
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.title)
val image: ImageView = view.findViewById(R.id.image)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = items[position]
// Fast operations only
holder.title.text = item.title
// Async image loading
Glide.with(holder.itemView.context)
.load(item.imageUrl)
.into(holder.image)
}
}
// Enable optimizations
recyclerView.apply {
setHasFixedSize(true)
setItemViewCacheSize(20)
setDrawingCacheEnabled(true)
setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
}
Animation Performance
- Target 60 FPS (16.67ms per frame)
- Use transform animations (translate, scale, rotate)
- Avoid animating layout properties
- Use hardware acceleration
- Keep animations under 300ms
Background Task Optimization
iOS Background Tasks
// Background fetch
func application(_ application: UIApplication,
performFetchWithCompletionHandler completionHandler:
@escaping (UIBackgroundFetchResult) -> Void) {
// Fetch data efficiently
DataManager.shared.syncData { success in
if success {
completionHandler(.newData)
} else {
completionHandler(.failed)
}
}
}
// Use background URLSession
let config = URLSessionConfiguration.background(
withIdentifier: "com.app.background"
)
let session = URLSession(configuration: config)
Android WorkManager
// Efficient background work
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Do work efficiently
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
// Schedule periodic sync
val syncRequest = PeriodicWorkRequestBuilder(
15, TimeUnit.MINUTES
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(syncRequest)
Database Performance
SQLite Optimization
Best practices:
✓ Create indexes on frequently queried columns
✓ Use transactions for bulk operations
✓ Normalize data structure
✓ Use prepared statements
✓ VACUUM regularly to defragment
// Batch insert with transaction
database.beginTransaction()
try {
for (item in items) {
database.insert("items", null, item.toContentValues())
}
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
Core Data / Room Optimization
- Use batch operations
- Implement pagination
- Use background contexts
- Fetch only needed properties
- Set appropriate fetch batch sizes
Build Optimization
App Size Reduction
iOS:
- Enable bitcode
- Use on-demand resources
- Strip unused architectures
- Compress assets
- Remove unused code (AppThinning)
Android:
- Enable R8/ProGuard
- Use Android App Bundle (AAB)
- Split APKs by ABI
- Compress resources
- Remove unused resources (shrinkResources)
Compilation Speed
- Modularize codebase
- Use build caching
- Incremental builds
- Parallel compilation
- Optimize dependencies
Profiling Tools
iOS Instruments
- Time Profiler: CPU usage
- Allocations: Memory leaks
- Leaks: Retain cycles
- Network: API performance
- Energy Log: Battery impact
Android Profiler
- CPU Profiler: Method tracing
- Memory Profiler: Heap dumps
- Network Profiler: Request/response
- Energy Profiler: Battery drain
Performance Metrics
Key Metrics to Track
Launch Time:
- Cold start: < 1.5s
- Warm start: < 1s
- Hot start: < 500ms
Frame Rate:
- Target: 60 FPS (16.67ms/frame)
- Acceptable: 50+ FPS
- Poor: < 30 FPS
Memory:
- Steady state: < 100 MB
- Peak: < 200 MB
- No memory leaks
Network:
- API latency: < 200ms
- Image load: < 1s
- Cache hit rate: > 80%
Battery:
- Background drain: < 2%/hour
- Active use: < 5%/hour
Monitoring and Alerts
Real-Time Monitoring
- Firebase Performance: Automatic metrics
- New Relic: Detailed APM
- AppDynamics: End-to-end monitoring
- Sentry: Performance tracking
Custom Metrics
// Track custom performance
let trace = Performance.startTrace(name: "image_load")
trace.setValue(imageSize, forMetric: "size")
loadImage(url) { image in
trace.incrementMetric("images_loaded", by: 1)
trace.stop()
}
Performance Testing
Load Testing
- Test with 1000+ items
- Simulate slow network (3G)
- Test on low-end devices
- Memory stress testing
- Battery drain testing
Automated Testing
// XCTest performance testing
func testPerformanceExample() {
measure {
// Code to measure
viewModel.loadData()
}
}
// Android benchmark
@Test
fun testListScrolling() {
benchmarkRule.measureRepeated {
// Scroll list
recyclerView.scrollToPosition(100)
}
}
Platform-Specific Optimizations
iOS-Specific
- Use Swift over Objective-C (faster)
- Enable whole module optimization
- Use value types (struct) when possible
- Implement prefetching for collections
- Use background modes judiciously
Android-Specific
- Use Kotlin coroutines (vs threads)
- Implement ViewModel + LiveData
- Use DataBinding for UI
- Enable Jetpack Compose optimization
- Use Paging library for lists
Quick Wins Checklist
□ Enable code obfuscation/minification
□ Compress all images
□ Implement image caching
□ Add request caching
□ Lazy load non-critical resources
□ Use pagination for lists
□ Defer analytics initialization
□ Remove unused libraries
□ Enable gzip compression
□ Optimize database queries
□ Profile app with Instruments/Profiler
□ Test on low-end devices
□ Monitor real-user metrics
□ Set performance budgets
Conclusion
Performance optimization is an ongoing process. Profile regularly, set performance budgets, and prioritize user experience. Every millisecond counts in user retention and app store rankings.