53% of users abandon apps that take longer than 3 seconds to load. Every 100ms delay in load time can decrease conversions by 7%. Poor performance is the #2 reason for app uninstalls. This comprehensive guide shows you how to build blazingly fast mobile apps that users love and keep.
Why Performance Matters
Performance Impact Statistics
- 53% of users abandon apps taking > 3 seconds to load
- Every 100ms delay = 7% drop in conversions
- 1-second delay = 11% fewer page views
- Poor performance is #2 reason for uninstalls (after crashes)
- 79% of users won't return after bad performance experience
- App store algorithms favor performant apps in rankings
- Battery drain from poor code hurts ratings significantly
Business Benefits
- Higher retention: Fast apps = happy users who stay
- Better ratings: Performance strongly correlates with ratings
- More conversions: Speed directly impacts revenue
- Lower costs: Efficient apps use less server resources
- Better ASO: App stores rank fast apps higher
- Competitive edge: Speed is a feature users notice
Performance Metrics
Key Metrics to Track
App Launch Time
Cold start: App not in memory
- Target: < 2 seconds
- Acceptable: < 3 seconds
- Poor: > 3 seconds
Warm start: App in memory, not in foreground
- Target: < 1 second
- Acceptable: < 1.5 seconds
Hot start: App in foreground, returning from background
- Target: < 500ms
- Acceptable: < 1 second
What users perceive:
0-100ms: Instant
100-300ms: Slight delay
300-1000ms: Noticeable
1000ms+: Mental context switch, loss of flow
10000ms+: User leaves
iOS measurement:
// AppDelegate.swift
let start = CFAbsoluteTimeGetCurrent()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let elapsed = CFAbsoluteTimeGetCurrent() - start
print("Launch time: \(elapsed)s")
return true
}
Android measurement:
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val start = System.currentTimeMillis()
super.onCreate(savedInstanceState)
val elapsed = System.currentTimeMillis() - start
Log.d("Performance", "Launch time: ${elapsed}ms")
}
}
Frame Rate (FPS)
Target: 60 FPS (16.67ms per frame)
Acceptable: > 50 FPS
Poor: < 30 FPS
Why it matters:
- Smooth scrolling
- Fluid animations
- Responsive interactions
- Professional feel
Common causes of dropped frames:
- Heavy computation on main thread
- Complex view hierarchies
- Inefficient layouts
- Image processing
- Network on main thread
- Large data updates
iOS monitoring:
// Use Instruments > Core Animation
// Or in code:
CADisplayLink(target: self, selector: #selector(tick))
.add(to: .main, forMode: .common)
Android monitoring:
// Enable GPU profiling in Developer Options
// Or use:
Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
val elapsed = System.nanoTime() - frameTimeNanos
if (elapsed > 16_666_666) { // 16.67ms
Log.w("Performance", "Dropped frame: ${elapsed / 1_000_000}ms")
}
}
Memory Usage
Targets vary by device:
iPhone 15 Pro: < 200MB typical, < 500MB peak
iPhone SE: < 100MB typical, < 250MB peak
Android flagship: < 200MB typical, < 400MB peak
Android budget: < 100MB typical, < 200MB peak
Memory issues:
- Crashes from out-of-memory
- System kills background app
- Slow performance from swapping
- Battery drain
- User frustration
What to monitor:
- Total memory usage
- Memory leaks (growing over time)
- Peak memory (spike activities)
- Memory warnings/pressure
Network Performance
API response time:
Target: < 500ms
Acceptable: < 1000ms
Poor: > 2000ms
First contentful paint:
Target: < 1.5 seconds
Acceptable: < 2.5 seconds
Download sizes:
JSON response: < 100KB ideal
Images: < 200KB per image
Video: Adaptive streaming
Initial bundle: < 5MB
Network efficiency:
✓ Compress responses (gzip)
✓ Use CDN for static assets
✓ Implement caching
✓ Batch API requests
✓ Use pagination
✓ Prefetch strategically
✓ Implement retry logic
✓ Handle offline gracefully
Launch Time Optimization
iOS Launch Optimization
Reduce Work in didFinishLaunching
// ❌ Bad: Heavy work on launch
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Avoid these on launch:
setupAnalytics() // Can defer
setupCrashReporting() // Critical, but optimize
loadUserData() // Defer to first screen
setupDatabase() // Defer until needed
preloadImages() // Defer
checkForUpdates() // Defer
return true
}
// ✅ Good: Minimal launch work
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Only critical, fast operations
setupCrashReporting() // Must be early
// Defer everything else
DispatchQueue.main.async {
self.setupAnalytics()
}
// Or defer to when needed
// loadUserData() called when user opens profile
return true
}
// Lazy initialization
class AppServices {
static let shared = AppServices()
lazy var database: Database = {
// Only initialized when first accessed
return Database()
}()
lazy var analytics: Analytics = {
let analytics = Analytics()
analytics.configure()
return analytics
}()
}
Optimize Storyboard/XIB Loading
// ❌ Slow: Complex storyboard
// Main.storyboard with 20+ view controllers
// ✅ Fast: Split storyboards
// Main.storyboard: Launch + first screen only
// Home.storyboard: Home flow
// Profile.storyboard: Profile flow
// Settings.storyboard: Settings flow
// Lazy storyboard loading
lazy var profileStoryboard = UIStoryboard(
name: "Profile",
bundle: nil
)
func showProfile() {
let vc = profileStoryboard.instantiateInitialViewController()
present(vc, animated: true)
}
// Or use programmatic UI (faster than storyboards)
class HomeViewController: UIViewController {
override func loadView() {
view = HomeView() // Custom view
}
}
Android Launch Optimization
Reduce Application onCreate Work
// ❌ Bad: Heavy initialization
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Avoid these:
initializeAnalytics() // Defer
setupDatabase() // Defer
loadConfiguration() // Defer
setupImageLoading() // Defer
preloadData() // Defer
}
}
// ✅ Good: Minimal initialization
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Only critical, fast operations
setupCrashReporting() // Must be early
// Defer others
lifecycleScope.launch {
initializeAnalytics()
}
}
}
// Use content providers for initialization
class InitProvider : ContentProvider() {
override fun onCreate(): Boolean {
// Runs before Application.onCreate()
// But runs in parallel, so use for independent init
context?.let { initializeLibrary(it) }
return true
}
}
// Lazy initialization
object Services {
val database: Database by lazy {
Database.getInstance()
}
val analytics: Analytics by lazy {
Analytics.Builder()
.setContext(context)
.build()
}
}
Optimize Layout Inflation
// Use ViewBinding (faster than findViewById)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
// Flatten view hierarchies
// ❌ Bad: Nested layouts
// ✅ Good: Flat hierarchy
// Use merge tags
// ViewStub for deferred loading
// Inflate when needed
binding.stubImport.setOnInflateListener { _, inflated ->
// Setup inflated view
}
binding.stubImport.viewStub?.inflate()
Rendering Performance
iOS Rendering Optimization
Optimize View Hierarchy
// ❌ Bad: Deep hierarchy
UIView
└─ UIView
└─ UIView
└─ UIView
└─ UIImageView
└─ UILabel
└─ UILabel
// ✅ Good: Flat hierarchy
UIView
├─ UIImageView
├─ UILabel
└─ UILabel
// Use layer.shouldRasterize for complex static views
complexView.layer.shouldRasterize = true
complexView.layer.rasterizationScale = UIScreen.main.scale
// But beware: rasterization has memory cost
// Only use for complex views that don't change
// Opaque views for better compositing
view.backgroundColor = .white
view.isOpaque = true
// Avoid:
view.backgroundColor = .clear // Forces blending
view.alpha = 0.99 // Forces blending
Image Optimization
// Size images correctly
// ❌ Bad: Load 4000x3000 image for 300x200 view
imageView.image = UIImage(named: "large-photo")
// ✅ Good: Downscale to display size
func downsample(
imageAt imageURL: URL,
to pointSize: CGSize,
scale: CGFloat = UIScreen.main.scale
) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(
imageURL as CFURL,
imageSourceOptions
) else { return nil }
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(
imageSource,
0,
downsampleOptions
) else { return nil }
return UIImage(cgImage: downsampledImage)
}
// Use image caching
let cache = NSCache()
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
// Async image loading
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
completion(nil)
return
}
DispatchQueue.main.async {
completion(image)
}
}
}
Android Rendering Optimization
Optimize Layouts
// Use ConstraintLayout (most efficient)
// Avoid nested weights
// ❌ Bad
// ✅ Good: Use ConstraintLayout chains instead
// Reduce overdraw
// Check with: Developer Options > Debug GPU Overdraw
// Blue: 1x overdraw (acceptable)
// Green: 2x overdraw (monitor)
// Pink: 3x overdraw (bad)
// Red: 4x+ overdraw (very bad)
// Remove window backgrounds when not needed
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set background only on root view
binding.root.setBackgroundColor(Color.WHITE)
}
RecyclerView Optimization
// Set fixed size for better performance
recyclerView.setHasFixedSize(true)
// Use appropriate layout manager
recyclerView.layoutManager = LinearLayoutManager(this)
// Optimize ViewHolder
class ItemViewHolder(
private val binding: ItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
// Bind data
binding.title.text = item.title
// Load images efficiently
Glide.with(binding.root)
.load(item.imageUrl)
.placeholder(R.drawable.placeholder)
.into(binding.image)
}
}
// Use DiffUtil for efficient updates
class ItemDiffCallback : DiffUtil.ItemCallback- () {
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Item, newItem: Item) =
oldItem == newItem
}
class ItemAdapter : ListAdapter
- (ItemDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val binding = ItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ItemViewHolder(binding)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
// Prefetch items
val layoutManager = LinearLayoutManager(this)
layoutManager.initialPrefetchItemCount = 4
recyclerView.layoutManager = layoutManager
// Use RecycledViewPool for nested RecyclerViews
val sharedPool = RecyclerView.RecycledViewPool()
parentAdapter.setRecycledViewPool(sharedPool)
Memory Management
iOS Memory Management
ARC Best Practices
// Avoid retain cycles
class ViewController: UIViewController {
var completion: (() -> Void)?
func setupClosure() {
// ❌ Bad: Retain cycle
completion = {
self.updateUI()
}
// ✅ Good: Weak self
completion = { [weak self] in
self?.updateUI()
}
// ✅ Good: Unowned (if self guaranteed to exist)
completion = { [unowned self] in
self.updateUI()
}
}
}
// Delegate patterns
protocol DataSourceDelegate: AnyObject {
func dataDidUpdate()
}
class DataSource {
weak var delegate: DataSourceDelegate? // Weak to avoid cycle
}
// Notification cleanup
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .dataUpdated,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Timer cleanup
class TimerViewController: UIViewController {
var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] _ in
self?.update()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
timer = nil
}
}
Memory Warnings
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Clear caches
imageCache.removeAllObjects()
// Release non-critical data
cachedData = nil
// Stop non-essential operations
backgroundTasks.forEach { $0.cancel() }
}
// Monitor memory in Instruments
// Memory Graph Debugger in Xcode
// Look for:
// - Leaked objects
// - Abandoned memory
// - Retain cycles
Android Memory Management
Avoid Memory Leaks
// ❌ Bad: Activity leak
class LeakyActivity : AppCompatActivity() {
companion object {
var listener: (() -> Unit)? = null // Static reference to Activity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
listener = {
// This captures Activity reference
findViewById(R.id.text).text = "Updated"
}
}
}
// ✅ Good: Weak reference or cleanup
class GoodActivity : AppCompatActivity() {
private var listener: (() -> Unit)? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
listener = {
findViewById(R.id.text).text = "Updated"
}
}
override fun onDestroy() {
super.onDestroy()
listener = null
}
}
// Handler leaks
// ❌ Bad: Inner class Handler
class LeakyActivity : AppCompatActivity() {
private val handler = Handler() {
// Implicit reference to Activity
true
}
}
// ✅ Good: Static Handler with WeakReference
class GoodActivity : AppCompatActivity() {
private val handler = MyHandler(this)
private class MyHandler(activity: GoodActivity) : Handler(Looper.getMainLooper()) {
private val activityRef = WeakReference(activity)
override fun handleMessage(msg: Message) {
activityRef.get()?.let { activity ->
// Handle message
}
}
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
}
Bitmap Management
// Load bitmaps efficiently
fun decodeSampledBitmap(
res: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, this)
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
inJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
while (halfHeight / inSampleSize >= reqHeight &&
halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
// Use Glide or Coil for automatic optimization
Glide.with(context)
.load(imageUrl)
.override(300, 200) // Resize to display size
.into(imageView)
Network Optimization
Request Optimization
// Batch requests
// ❌ Bad: Multiple requests
fetchUser(id: 1)
fetchUser(id: 2)
fetchUser(id: 3)
// ✅ Good: Batch request
fetchUsers(ids: [1, 2, 3])
// Compress responses
// Server: Enable gzip
// Client: Accept-Encoding: gzip
// Use HTTP/2
// Multiplexing, header compression, server push
// Cache responses
iOS URLSession:
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
config.urlCache = URLCache(
memoryCapacity: 50 * 1024 * 1024, // 50MB
diskCapacity: 100 * 1024 * 1024 // 100MB
)
Android OkHttp:
val cacheSize = 10 * 1024 * 1024L // 10 MB
val cache = Cache(cacheDir, cacheSize)
val client = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
var request = chain.request()
request = request.newBuilder()
.header("Cache-Control", "public, max-age=3600")
.build()
chain.proceed(request)
}
.build()
// Pagination
GET /api/posts?page=1&limit=20
// Prefetching
func prefetchNextPage() {
guard !isPrefetching else { return }
isPrefetching = true
fetchPosts(page: currentPage + 1) { posts in
self.cachedNextPage = posts
self.isPrefetching = false
}
}
Profiling Tools
iOS Instruments
Key Instruments:
Time Profiler:
- CPU usage per method
- Hot spots in code
- Call trees
How to use:
1. Product > Profile (⌘I)
2. Select Time Profiler
3. Record while using app
4. Analyze call tree
Allocations:
- Memory allocations
- Object growth
- Leaks detection
How to use:
1. Profile > Allocations
2. Look for growing memory
3. Check for leaks (Leaks instrument)
Core Animation:
- FPS
- Blending
- Offscreen rendering
- Committed bytes
Enable in simulator:
Debug > Color Blended Layers
Debug > Color Offscreen-Rendered Yellow
Network:
- HTTP requests
- Response times
- Data transferred
- Connection reuse
Android Profiler
Android Studio Profiler:
CPU Profiler:
- Method traces
- Flame charts
- Top-down/bottom-up trees
How to use:
1. View > Tool Windows > Profiler
2. Select CPU
3. Record trace
4. Analyze methods
Memory Profiler:
- Heap dumps
- Memory allocations
- Garbage collections
- Memory leaks
How to use:
1. Select Memory in Profiler
2. Force GC to see leaks
3. Dump heap
4. Analyze with MAT
Network Profiler:
- Request/response
- Timeline
- Data transferred
Battery Profiler:
- Battery usage
- Wake locks
- Alarms
- Jobs
Conclusion
Performance optimization is not a one-time task—it's an ongoing process. By measuring key metrics, optimizing launch times, managing memory efficiently, and using the right tools, you can build apps that feel instant and effortless. Remember: every millisecond counts, and users notice the difference between good and great performance.
Fast apps need fast infrastructure. Our support URL generator creates optimized, high-performance support pages that load instantly and provide excellent user experience without slowing down your app.