---
title: "Long-Term Mobile App Performance: Memory Leaks, ANRs, Crashes, and App Size Optimization"
description: "Learn how to fix memory leaks, prevent ANRs, reduce crashes, and optimize app size for long-term mobile app stability and sustained performance."
publishedAt: "2026-03-09T12:00:00.000Z"
updatedAt: "2026-03-09T12:00:00.000Z"
author: "Tushar Gupta"
categories: ["Mobile App Architecture", "Mobile App Development Trends"]
canonical: "https://www.digia.tech/post/mobile-app-stability-memory-leaks-anr-crash-optimization"
---

# Long-Term Mobile App Performance: Memory Leaks, ANRs, Crashes, and App Size Optimization


---


[<u>Startup performance</u>](https://www.digia.tech/post/app-startup-time-performance-guide) creates the first impression. [<u>Runtime smoothness</u>](https://www.digia.tech/post/mobile-app-runtime-performance-fps-jank) determines how the app feels in the first few minutes. Screen load performance decides how quickly users can begin interacting.

But there is a fourth dimension of performance that rarely shows up in demos.

It is not about how fast the app begins. It is about how well it survives.

To understand this, imagine taking a car on a long highway drive. At the start, everything feels perfect. The engine starts instantly. The steering is light. Acceleration is smooth. You trust the machine.

Two hours later, the engine temperature rises. The steering feels heavier. Warning lights begin to flicker. 

Eventually, the car stalls.

Nothing was wrong at the beginning. 

The problem appeared under sustained load.

That is how long-term performance issues behave in mobile apps.

They are not first-minute problems.

They are endurance problems.

## Memory Leaks: The Weight That Slowly Builds

Imagine that at every rest stop on your road trip, you place a small bag in the trunk. You never remove any. You just keep adding.

The car still moves, but gradually it becomes heavier. Acceleration slows. Fuel consumption increases. The engine works harder than it should.

A memory leak behaves the same way.

Technically, a leak occurs when objects that should be garbage collected remain strongly referenced. The heap grows steadily. Garbage collection runs more often. Eventually, the system throws an OutOfMemoryError or terminates the process.

> Memory should fluctuate. It should not trend upward indefinitely.

Over time, this results in:

- Increasing memory footprint
- More frequent garbage collection cycles
- Longer GC pause durations
- Higher probability of OutOfMemory (OOM) crashes

From an engineering dashboard, it appears as gradual heap growth. From the user’s perspective, it appears as instability after prolonged usage, sudden slowdowns, or unexpected crashes.

> Leaks are cumulative. They reveal themselves over duration, not instantly.

### Common Causes

On Android:

- Static references holding Activity context
- Long-lived singletons storing UI references
- Coroutines not cancelled in onDestroy
- Listeners not unregistered

On iOS:

- Strong reference cycles (retain cycles)
- Closures capturing self without [weak self]
- Delegates not marked weak

On Flutter:

- Controllers not disposed
- Streams not cancelled
- Global state persisting unnecessarily

Memory leaks are dangerous precisely because they are quiet.

> The first five minutes of usage rarely reveal a memory leak. The thirtieth minute does.

### Detecting Memory Leaks in Real Projects

Instrumentation is essential. Stability cannot rely on intuition.


![Diagram showing data flow between Service, Activity, Fragment, and Fragment View. Orange highlights indicate "Leak!" areas. Black background.](https://cdn.sanity.io/images/53loe8pn/production/0c5e5528dfb8b711bea307b4041bfef817a1bdb4-1440x972.png?w=1200&fit=max&auto=format)


On Android, LeakCanary provides automatic detection of retained activities and fragments during debug sessions.


```gradle
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"
```


For heap inspection at runtime:


```text
adb shell dumpsys meminfo com.example.app
```


Repeated navigation cycles should not cause heap size to increase indefinitely.

On iOS, Xcode Instruments offers Allocations and Leaks tools. If heap memory continues growing after dismissing views, something is being retained unexpectedly.

In Flutter, DevTools’ Memory tab reveals Dart heap growth, allocation spikes, and retained widget trees. If garbage collection runs but memory never stabilizes, investigation is required.

> Leak detection should be part of QA, not an afterthought.

## Garbage Collection and Allocation Pressure

Not all instability comes from leaks. Sometimes the issue is not retained memory, but excessive allocation.

When an app frequently creates short-lived objects like during scrolling, animation, or list rebuilding, the garbage collector must run more often. Garbage collection pauses can block execution, and if the main thread is paused, frames are dropped.

Users do not see “GC pause time” in a log. 

They experience:

- Scroll jitter
- Random stutter
- Input delay
- Inconsistent animations

For example, in Android:


```text
overridefunonBindViewHolder(holder: ViewHolder, position:Int) {val formatter = SimpleDateFormat("dd-MM-yyyy")
    holder.date.text = formatter.format(items[position].date)
}
```


Creating a new formatter for every bind operation increases allocation overhead. A better approach:


```text
privateval formatter = SimpleDateFormat("dd-MM-yyyy")overridefunonBindViewHolder(holder: ViewHolder, position:Int) {
    holder.date.text = formatter.format(items[position].date)
}
```


In Flutter, using const constructors prevents unnecessary rebuild allocations:


```text
const Text("Static Label");
```


The goal is not zero allocation. It is predictable allocation.

In high-interaction flows, aggressive allocation patterns can cause unpredictable smoothness even if total memory usage remains within limits.

Stability is not only about how much memory you use.

It is also about how efficiently you allocate and release it.

## ANRs: When the App Stops Responding

On Android, an ANR (Application Not Responding) is triggered when the main thread remains blocked for too long.

On Android:

- 5 seconds for input events
- 10+ seconds for broadcast receivers
- 20+ seconds for services


![Smartphone displays app error message "ANR Sample isn't responding" with options to "Close app" or "Wait" against a gray background.](https://cdn.sanity.io/images/53loe8pn/production/1f360c8e3055be423b2e3fc6f8385271c0230be1-345x591.png?w=1200&fit=max&auto=format)


If the UI thread blocks for five seconds during input handling, the system shows an ANR dialog.

Imagine driving and suddenly the steering wheel locks. Even if it unlocks after a few seconds, confidence in the vehicle is gone.

ANRs typically result from:

- Blocking I/O operations on the main thread
- Long synchronous database queries
- Heavy computation inside UI callbacks
- Deadlocks between threads
- Improper coroutine or async handling

On Android, coroutines should isolate heavy work:


```text
lifecycleScope.launch {valdata = withContext(Dispatchers.IO) {
        fetchLargeData()
    }
    updateUI(data)
}
```


StrictMode can detect accidental main-thread violations:


```text
StrictMode.setThreadPolicy(
    StrictMode.ThreadPolicy.Builder()
        .detectAll()
        .penaltyLog()
        .build()
)
```


On iOS:


```text
DispatchQueue.global(qos: .userInitiated).async {let data= fetchData()DispatchQueue.main.async {self.updateUI(data)
    }
}
```


In Flutter:


```text
final result = await compute(parseLargeJson, rawJson);
```


The main thread should orchestrate rendering and input. Everything else belongs elsewhere.

ANRs are particularly damaging because they feel like the app has frozen. Taps do not register. The screen does not update. In many cases, the operating system displays a dialog asking whether the user wants to close the app.

From the user’s perspective, **the app has failed**.

## Crash Rates and the Cost of Scale

If memory leaks are gradual degradation and ANRs are momentary paralysis, crashes are hard stops.

A crash terminates the session immediately. Any context is lost. If it happens during a critical action - checkout, payment confirmation, data submission - the business impact is direct.

Crash stability is usually measured using metrics like:


| Crash-free sessions | Percentage of sessions without crashes |
| --- | --- |
| Crash-free users | Percentage of users unaffected |
| OOM crash rate | Memory exhaustion frequency |
| Crash rate per release | Stability regression after updates |


A 99.5% crash-free session rate may appear acceptable. However, at one million active users, that remaining 0.5% represents thousands of disrupted experiences.

At scale, small percentages translate into significant real-world impact.

## Memory Fragmentation and Long-Session Degradation

Beyond leaks and GC pressure, long-running apps can suffer from memory fragmentation. As memory is allocated and freed unevenly, the heap becomes fragmented, reducing allocation efficiency and increasing pressure on the allocator.

This manifests in:

- Longer allocation times
- Increased GC activity
- Progressive performance degradation

Fragmentation issues often appear in apps that handle large media assets, frequent image decoding, or dynamic view construction.

These are not startup problems. They are session-duration problems.

## App Size: The Invisible Performance Multiplier

App size is rarely discussed during early development. Over time, it becomes a silent performance tax.

Larger binaries increase install time, reduce update adoption, and discourage users in bandwidth-constrained environments. Excessive size also increases memory mapping overhead at runtime.

App growth is gradual. SDK by SDK. Asset by asset.

Measurement precedes optimization.

### Size Analysis

Android Studio’s APK Analyzer reveals DEX size, native library weight, and resource contributions. Dependency trees expose transitive library growth.


```text
./gradlew app:dependencies
```


On iOS, Xcode’s App Size Report identifies heavy asset bundles and framework contributions.

Optimization without analysis is guesswork.

### Code Shrinking and Resource Minification

Enabling R8 or ProGuard removes unused classes and resources.


```text
buildTypes {
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
```


Dead code elimination reduces DEX size. Resource shrinking removes unused drawables and layouts. Reflection-heavy libraries require proper keep rules to avoid runtime crashes.

### Image Optimization

Images frequently dominate binary size. Converting **PNG** assets to **WebP** significantly reduces footprint without sacrificing quality. Vector-based assets eliminate redundant density variants.

Optimized images reduce both install size and runtime memory consumption.

### App Bundles and ABI Splits

Traditional APKs ship all architectures. Most devices require only one.

Android App Bundles allow Play Store to generate device-specific builds, reducing download size.


![Flowchart of APK structure with stacked rectangles. Base APK at top, dynamic features and configuration APKs below in green and blue. Arrows indicate relationships.](https://cdn.sanity.io/images/53loe8pn/production/3f81f2186ce4907874c6f2b9d18674bbf699033d-1706x1473.png?w=1200&fit=max&auto=format)


For manual splits:


```text
splits {
    abi {
        enable true
        reset()
        include "armeabi-v7a", "arm64-v8a"
        universalApk false
    }
}
```


Native libraries are heavy. Shipping unnecessary architectures multiplies binary size.

## Stability as a Measurement Discipline

Endurance problems are invisible without measurement.

The metrics required for stability differ from startup or smoothness metrics.

You need to monitor:


| Heap growth over session time | Detect memory leaks |
| --- | --- |
| Allocation rate | Identify churn |
| GC pause duration | Predict frame spikes |
| ANR rate | Detect UI thread blocking |
| Crash-free sessions | Measure release stability |
| App size per release | Track bloat trends |
| P95 / P99 session performance | Capture tail-risk behavior |


Average performance often hides instability. Tail metrics like P95 and P99, reveal where endurance breaks down.

> Stability problems live in the edges, not the averages.

## Production Is the True Environment

Local testing rarely replicates real-world usage. Developers test fresh installs on modern devices with stable networks and limited session duration.

Real users operate under:

- Background app competition
- Limited RAM
- Slower CPUs
- Extended session times
- Unstable network conditions

Production monitoring systems, including crash reporting tools and performance dashboards, expose:

- Device-specific memory pressure
- OS-version-specific crash patterns
- ANR spikes after releases
- Performance regressions in long sessions


![Dashboard with app analytics: installs, active users, sessions, and media spends. Graphs and charts show data trends. Blue and green colors.](https://cdn.sanity.io/images/53loe8pn/production/b3130cf0d2df25bcabee2ba1877daccbda0675dd-1583x888.png?w=1200&fit=max&auto=format)


Without production visibility, teams optimize the beginning of the journey while ignoring what happens later.

## Preventing Long-Term Degradation

Stability is not a single fix. It is operational discipline.

Move heavy work off the main thread. Ensure background tasks are properly scoped. Clear references in lifecycle callbacks. Avoid static context retention. Audit listeners and subscriptions. Reuse objects where possible. Monitor large lists and media-heavy screens carefully.

Before every release, teams should ask:

- Did memory footprint increase?
- Did crash-free sessions decrease?
- Did ANR rates change?
- Did binary size grow?
- Did long-session performance degrade?

Performance must be evaluated per release, not per feature.

## The Real Meaning of Reliability

Startup speed earns attention. Runtime smoothness earns comfort. Screen load speed earns engagement.

Stability earns trust.

If an app slows down after twenty minutes, crashes during checkout, or freezes while typing, users do not think about heap fragmentation or allocation churn.

They leave.

Performance is not just about how fast an app begins. It is about how reliably it continues.

An app that survives long sessions, heavy interaction, and scale without degradation is not just fast. It is dependable.
