Mobile App Performance Optimization Tips
Mobile app optimization involves efficient memory handling, reduced network payloads, and unblocked main threads to ensure stable frame rates and minimize iOS and Android crashes.

ON THIS PAGE
0% read
- The Business Imperative of Mobile Application Performance
- Unblocking the Main Thread for Stable Frame Rates
- Optimizing Memory Management and State Handling
- Streamlining Network Payloads and API Efficiency
- Refining Application Architecture and Asset Delivery
- Minimizing Platform-Specific Crash Rates
- Continuous Performance Monitoring and QA
- Prioritizing Proactive Performance Engineering
Mobile app optimization involves efficient memory handling, reduced network payloads, and unblocked main threads to ensure stable frame rates and minimize iOS and Android crashes.
Delivering a resilient, responsive digital product requires moving beyond basic feature development into proactive engineering discipline. Executive stakeholders and engineering leaders frequently discover that technical friction directly translates into customer churn, diminished conversion rates, and negative app store ratings. Implementing actionable Mobile App Performance Optimization Tips across the entire development lifecycle enables engineering teams to eliminate frame drops, optimize battery consumption, and streamline network bandwidth. By treating performance as a core product feature rather than an afterthought, organizations can protect their digital revenue streams, lower infrastructure overhead, and maintain an edge in highly competitive mobile marketplaces.
The Business Imperative of Mobile Application Performance
Technical debt in mobile architectures does not merely create engineering friction; it directly damages bottom-line commercial metrics. Modern mobile consumers have zero tolerance for sluggish user interfaces, unresponsive payment gateways, or unexplained application termination. In transactional apps such as e-commerce, fintech, and on-demand delivery, an operational latency increase of even 100 milliseconds can measurably depress checkout conversions. When product teams prioritize rapid feature delivery at the expense of runtime efficiency, they incur invisible performance penalties that progressively erode organic user acquisition and brand equity.
Correlating Load Times with User Retention
Cold start times—the duration required for an application to initialize its process, load dependencies, render the initial view hierarchy, and become interactable—serve as the first critical touchpoint for user retention. Telemetry data across both iOS and Android ecosystems confirms that applications taking longer than two seconds to achieve Time to Interactive (TTI) experience immediate drop-off spikes during user onboarding. If an application requires five seconds or more to launch, day-one retention can drop by up to 40% compared to apps that launch within sub-second thresholds.
Long-term user engagement (measured via Daily Active Users and Monthly Active Users) relies heavily on perceived responsiveness during everyday workflows. When a user experiences micro-delays while navigating product catalogs or executing core workflows, cognitive friction accumulates. This friction leads users to abandon sessions early, engage less frequently, and ultimately uninstall the application. Conversely, maintaining fluid, instant transitions creates a sense of reliability that reinforces user habituation and increases Lifetime Value (LTV).
The Cost of Unhandled Exceptions and App Crashes
Application stability directly influences distribution efficiency through algorithmic App Store Optimization (ASO). Both Apple's App Store and Google Play utilize crash-free user session rates as primary ranking factors. Google Play actively penalizes apps that exceed platform-defined bad behavior thresholds—such as an overall crash rate above 1.09% or an Application Not Responding (ANR) rate exceeding 0.47%—by suppressing their search visibility and removing them from curated category rankings.
Beyond store discoverability, unhandled runtime exceptions during transactional funnels inflict direct financial loss. A crash during a credit card authorization or authentication step generates immediate customer service tickets, disputes, and permanent churn to competing services. Engineering leadership must establish strict Service Level Agreements (SLAs) regarding crash-free users, treating any regression below 99.5% crash-free sessions as a critical blocker for release pipelines.
Unblocking the Main Thread for Stable Frame Rates
In both iOS (UIKit/SwiftUI) and Android (View System/Jetpack Compose), the main thread—often designated as the UI thread—is responsible for processing user inputs, measuring layout bounds, executing drawing passes, and updating the display buffer. Mobile displays typically refresh at 60 Hz (requiring a new frame every 16.67 milliseconds) or 120 Hz on modern ProMotion and Dynamic AMOLED panels (requiring frame completion within 8.33 milliseconds). If any computational operation blocks the main thread beyond these strict time windows, the system drops frames, resulting in visible UI jank and sluggish gesture tracking.
Mitigating UI Stutters and Frame Drops (Achieving 60+ FPS)
Eliminating UI stutters requires strict avoidance of heavy computation, synchronous disk access, and complex view hierarchy calculations during layout passes. When rendering complex lists or feeds, view recycling mechanisms must execute in microsecond intervals. In Android's @@CODE0@@ or iOS's @@CODE1@@, complex view inflation and dynamic auto-layout constraints should be simplified. Heavy subview hierarchies must be flattened to avoid deep recursive traversal during measurement cycles.
For modern declarative frameworks like SwiftUI and Jetpack Compose, excessive recomposition or body recalculations represent a frequent source of dropped frames. Developers must leverage immutable data structures and fine-grained state observation (@@CODE0@@, @@CODE1@@, derivedStateOf) to ensure that only the specific visual components undergoing state modifications are redrawn, rather than triggering recursive updates across the entire visual tree.
Offloading Heavy Computations to Background Threads
Any workload involving disk I/O, database querying, JSON serialization, image filtering, or cryptographic operations must be explicitly routed to background execution contexts. In modern iOS development, Swift Concurrency (@@CODE0@@/@@CODE1@@, @@CODE2@@, and custom @@CODE3@@) provides structured concurrency primitives that automatically prevent thread explosion while keeping compute off the @@CODE4@@. In Android, Kotlin Coroutines paired with appropriate dispatchers (@@CODE5@@ for network/disk operations, Dispatchers.Default for CPU-intensive data transformations) ensure non-blocking execution.
// iOS: Structured background processing using Swift Concurrency
actor DataProcessingService {
func parseAndPersistPayload(_ rawData: Data) async throws -> [DomainModel] {
// CPU-bound JSON parsing off the Main Actor
let decoded = try JSONDecoder().decode([RemoteDTO].self, from: rawData)
let domainEntities = decoded.map { $0.toDomain() }
// Disk I/O executed on background actor isolation
try await DatabaseManager.shared.insertBatch(domainEntities)
return domainEntities
}
}// Android: Coroutine-driven background dispatching
class InventoryRepository(
private val localDb: InventoryDao,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun processIncomingCatalog(payload: String): List<Item> = withContext(defaultDispatcher) {
// CPU-bound transformation on Dispatchers.Default
val items = Json.decodeFromString<List<ItemDto>>(payload).map { it.toDomain() }
withContext(ioDispatcher) {
// Disk write on Dispatchers.IO
localDb.insertItems(items)
}
items
}
}Caution: Managing Asynchronous Tasks Without Creating Race Conditions
While offloading work to background workers is essential, poorly architected concurrency introduces race conditions, state inconsistencies, and thread deadlocks. When multiple asynchronous background tasks read and mutate shared mutable state concurrently, unpredictable data corruption can occur.
To safely manage concurrent mutations without locking the UI, teams must adopt explicit synchronization patterns. Using thread-safe transactional databases (like SQLite via Room or CoreData/SwiftData), applying thread confinement, or adopting actor models ensures that state mutations are strictly serialized. Furthermore, background tasks must observe lifecycle cancellations: if a user navigates away from a screen, running coroutines or asynchronous tasks associated with that view must be cancelled immediately to prevent orphaned workloads from consuming memory and CPU cycles.
Optimizing Memory Management and State Handling
Mobile operating systems enforce rigid memory limits due to physical hardware constraints and the imperative to conserve battery power. Unlike desktop environments with extensive virtual memory paging, mobile OS architectures terminate apps that consume excessive RAM to protect foreground system stability. Managing heap allocations, avoiding retaining references to discarded screens, and minimizing allocation velocity are vital disciplines for ensuring app longevity and responsive performance.
Identifying and Resolving Memory Leaks in iOS and Android
A memory leak occurs when an application retains references to objects that are no longer needed in the user workflow, preventing the operating system from reclaiming their allocated memory. In iOS, memory is managed via Automatic Reference Counting (ARC). Retain cycles (strong reference cycles) occur frequently when closures capture @@CODE0@@ strongly, or when parent and child objects maintain mutual strong references. Implementing @@CODE1@@ capture lists in asynchronous closures and delegating relationships via weak protocols resolves these retention traps.
In Android, the Garbage Collector (GC) automatically reclaims unreferenced heap allocations. However, leaks commonly arise when long-lived objects (such as static singletons, background handlers, or global event buses) inadvertently hold strong references to short-lived @@CODE0@@ or @@CODE1@@ contexts. When an activity is destroyed during screen rotation or navigation, retaining its context leaks the entire view tree, bitmaps, and attached view models. Using weak references, scoping singletons to the ApplicationContext, and integrating tools like LeakCanary ensure that context leaks are identified before production deployments.
Managing Garbage Collection Pauses (Android Specifics)
On Android devices, the Android Runtime (ART) employs generational concurrent garbage collection algorithms to reclaim unused heap memory. While modern ART GC pauses are significantly shorter than legacy Dalvik implementations, high allocation velocity—allocating thousands of short-lived objects per second inside high-frequency execution paths like onDraw(), scroll listeners, or list bind methods—forces the runtime to trigger frequent, aggressive GC cycles.
When the Garbage Collector runs concurrently, it competes for CPU cores with the UI thread; under extreme allocation spikes, it can temporarily suspend application threads (Stop-the-World pauses). To minimize GC overhead:
Avoid object allocations (e.g., @@CODE0@@, @@CODE1@@, @@CODE2@@, intermediate string builders) inside custom view @@CODE3@@ or Compose measure blocks.
Utilize primitive collections (e.g., Android's @@CODE0@@, @@CODE1@@) instead of standard Java boxing collections (
HashMap<Integer, Object>) to eliminate wrapper object allocation overhead.Implement reusable object pools for high-frequency models in graphics, audio, or continuous sensor-streaming modules.
Efficient Data Structuring to Reduce Active Memory Footprint
The structure of in-memory data caches significantly impacts base memory consumption. Storing uncompressed, raw server responses or massive unstructured JSON objects in singleton repositories needlessly inflates heap usage. Product teams must enforce normalized in-memory models, storing only the specific fields required for rendering UI layers.
For image-heavy applications, bitmap allocations represent the largest fraction of heap memory. Bitmaps should never be loaded into memory at their native camera or server resolution. Instead, developers must downsample bitmaps during the decoding phase to match the exact physical pixel dimensions of the target display view. Modern image management libraries (such as Coil or Glide on Android, and Kingfisher or Nuke on iOS) automatically calculate downsampling ratios and manage dual-layer (memory and disk) caches, preventing runaway bitmap allocation.
Streamlining Network Payloads and API Efficiency
Mobile devices operate under variable, hostile network conditions, frequently shifting between 5G, congested public Wi-Fi, and low-throughput edge cellular networks. Radio hardware on mobile chipsets consumes substantial battery power when transitioning from idle to high-power transmission states. Minimizing round-trips, compressing payloads, and adopting proactive caching strategies directly accelerates perceived speed while extending battery endurance.
Compressing Data and Reducing Payload Sizes (GZIP/Brotli)
Standardizing API transport compression is one of the highest-ROI optimizations available to mobile engineering teams. Web servers and API gateways must be configured to negotiate modern compression algorithms via standard Accept-Encoding headers. Brotli compression frequently outperforms standard GZIP, yielding an additional 15% to 25% reduction in transfer sizes for structured text formats like JSON.
For high-throughput, latency-critical applications (such as financial tickers, real-time messaging, or IoT telemetry), text-based JSON encoding can be replaced with binary serialization protocols such as Protocol Buffers (Protobuf) or FlatBuffers. Binary formats eliminate field name overhead, enforce strict schema validation, and drastically reduce both network payload byte size and client-side CPU deserialization latency.
Implementing Intelligent Caching and Offline-First Architectures
Relying entirely on live network round-trips for every screen transition results in a fragile, sluggish user experience. A resilient mobile architecture employs an offline-first data model, where local disk storage (e.g., SQLite, Room, CoreData) acts as the single source of truth for the presentation layer, while background synchronizers update the local cache incrementally.
+--------------------------------------------------------------------+
| Mobile Presentation Layer |
+--------------------------------------------------------------------+
|
v
+--------------------------------------------------------------------+
| Local Single Source of Truth (SQLite / Room) |
+--------------------------------------------------------------------+
^ |
Background | Cache Updates | Query Subscriptions
Sync Layer | (ETags / Delta) | (Flow / Combine)
| v
+-----------------------------+ +----------------------------+
| Remote API / CDN Endpoint | | UI Screen Rendering (0ms) |
+-----------------------------+ +----------------------------+To maximize bandwidth efficiency, network clients must implement standard HTTP caching mechanisms:
ETags & Conditional Requests: Transmit @@CODE0@@ headers with stored entity tags; servers respond with an empty @@CODE1@@ payload when data is unchanged, saving download bandwidth.
Cache-Control Directives: Respect
stale-while-revalidatepolicies to render cached local records immediately while refreshing content asynchronously in the background.Delta Syncing: Design API endpoints that accept a
last_updated_attimestamp parameter, returning only records modified since the previous synchronization pass.
Caution: Preventing Stale Data and Cache Invalidation Failures
Aggressive local caching without rigorous cache invalidation logic can corrupt business logic and display outdated information, such as incorrect pricing or inventory status. Teams must establish clear invalidation boundaries. Mutating actions (such as submitting an order, updating a profile, or toggling user settings) must execute immediate local cache invalidation and trigger targeted UI re-fetches. For sensitive domains like banking or medicine, data with volatile shelf-lives must bypass disk caches entirely or enforce short-lived Time-to-Live (TTL) policies.
Utilizing GraphQL and Pagination for Precise Data Fetching
Over-fetching—retrieving massive nested JSON objects containing dozens of fields when the client view only requires a title and thumbnail—unnecessarily consumes device memory and bandwidth. Implementing GraphQL allows client teams to request strictly the schema fields needed for a specific screen design.
Where RESTful architectures are retained, engineering teams must mandate cursor-based pagination for feeds and lists rather than offset-based pagination. Cursor pagination prevents duplicate or missing items when new records are inserted in real time, bounds payload memory overhead, and allows smooth incremental list loading via infinite scrolling architectures.
Refining Application Architecture and Asset Delivery
The structural composition of a mobile application binary affects its entire lifecycle, from download conversion rates over cellular connections to initial disk decompression and execution speeds. Large binaries filled with unreferenced third-party libraries, redundant localizations, and uncompressed assets elevate memory pressure and delay class-loading phases during startup.
Minimizing Application Bundle Size (APK/AAB and IPA)
Reducing binary footprint requires automated optimization pipelines integrated into Continuous Integration (CI) systems:
Android Code and Resource Shrinking: Enable @@CODE0@@ (R8 compiler) to strip unused classes, fields, and methods from dependencies. Pair this with @@CODE1@@ to remove unused drawables and assets from the merged build output.
Android App Bundles (AAB): Distribute builds via Google Play's AAB format, which dynamically delivers split APKs tailored specifically to the target device's screen density, CPU architecture (ABI), and language preferences.
iOS Dead Code Stripping & App Thinning: Leverage Xcode's asset catalogs, optimize compiler flags (
-Osize), and rely on Apple's App Slicing, which ensures end users only download the specific asset slices corresponding to their device scale factor (@2x or @3x).
On-Demand Resource Allocation and Asset Deferral
Not all application features are required during the initial launch or by every user. For example, an onboarding tutorial, heavy PDF export engine, or rarely used customer support video module can be decoupled from the primary base installation.
Both major mobile operating systems support dynamic delivery:
Android Dynamic Feature Modules: Allow specific feature code and resources to be downloaded on demand after app installation via the Play Core library.
iOS On-Demand Resources (ODR): Enable asset tags that host heavy supplementary assets (such as high-res 3D models, game levels, or auxiliary media) on Apple's servers, fetching them only when the user navigates to the relevant screen.
Optimizing Vector Graphics and Compressing Raster Images
Visual assets represent a substantial portion of app binary overhead. Static raster images (PNG, JPEG) should be audited and converted to modern, highly compressed formats or vector representations wherever feasible:
Replace static multi-density icons with native vector assets:
VectorDrawable(XML) on Android and SF Symbols / PDF/SVG vector assets in Xcode asset catalogs.For mandatory photographic content that cannot be converted to vector graphics, utilize WebP or AVIF encoding, which typically reduces file sizes by 30% to 50% compared to equivalent PNG/JPEG outputs without visible visual degradation.
Run automated image compression utilities (such as ImageOptim or optipng) in pre-commit git hooks or CI build jobs to strip metadata and compress binary headers.
Minimizing Platform-Specific Crash Rates
While core performance engineering principles apply universally, iOS and Android operate under fundamentally distinct execution constraints, kernel watchdogs, and hardware ecosystems. Engineering teams must design defensive code patterns that address the specific failure modes unique to each platform.
Adhering to iOS Memory Limits and Jetsam Event Prevention
On iOS, the operating system kernel utilizes a dedicated memory management daemon known as @@CODE0@@. Unlike systems that swap memory to disk, iOS terminates background and foreground processes abruptly when system-wide physical memory pressure crosses critical thresholds. These terminations (often logged with exception codes such as @@CODE1@@ for watchdog timeouts or EXC_RESOURCE for memory violations) do not generate standard stack traces within crash reporters, making them difficult to diagnose without proper telemetry.
To prevent Jetsam terminations:
Implement @@CODE0@@ observers and @@CODE1@@ lifecycle overrides to flush volatile caches, clear image buffers, and purge non-critical objects immediately when the OS signals memory strain.
Monitor memory footprint using Xcode Instruments (Allocations and Leaks templates) to ensure the steady-state baseline remains safely below device limits (typically under 150–200 MB on older iPhones).
Prevent memory spikes during camera operations or high-resolution photo processing by downsampling images on a streaming basis rather than loading full-resolution raw buffers into memory.
Navigating Android Device Fragmentation and Hardware Constraints
The Android ecosystem encompasses thousands of distinct device profiles spanning varied chipset capabilities, thermal profiles, and OEM-specific battery management software. A feature that executes smoothly on a flagship device may trigger an Application Not Responding (ANR) error on an entry-level device with 2 GB of RAM.
// Defensive memory threshold checks for Android background workers
fun executeMemorySafeBatchOperation(context: Context) {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
if (memoryInfo.lowMemory) {
// Drop cache allocations and process in smaller chunk sizes
processBatchInMicroChunks(chunkSize = 10)
} else {
processBatchStandard(chunkSize = 100)
}
}Key strategies for mitigating fragmentation-related failures include:
StrictMode Integration: Enable Android's
StrictModeduring debug builds to automatically catch accidental disk reads or network calls occurring on the main thread.Defensive Foreground Service Handling: Adhere strictly to Android background execution limits; use
WorkManagerfor guaranteed background persistence rather than long-running foreground services that can be killed by OEM battery-saver daemons.Thermal Throttling Awareness: Monitor device thermal states (
PowerManager.addThermalStatusListener) during prolonged AR, camera, or computational sessions to gracefully throttle frame rates and reduce resolution before the hardware forces a shutdown.
Continuous Performance Monitoring and QA
Performance optimization cannot be treated as an isolated, pre-launch checklist item. Mobile applications evolve rapidly with regular feature updates, SDK upgrades, and backend schema revisions. Establishing continuous performance governance through automated testing and production observability ensures that performance gains are sustained over time.
Utilizing Native Profiling Tools (Apple Instruments & Android Profiler)
Native platform profilers provide deep, non-intrusive visibility into hardware utilization, memory allocation graphs, and thread execution traces:
Apple Instruments: Utilize the Time Profiler to inspect CPU call trees and detect main-thread bottlenecks. Leverage the Allocations and Leaks instruments to track object lifespans and retain cycles. Use the Core Animation instrument to measure real-time FPS and debug offscreen rendering passes.
Android Studio Profiler: Use the CPU Profiler to record System Traces, identifying exact methods causing jank via Choreographer frame timing markers. Leverage the Memory Profiler to capture heap dumps and inspect retained references.
Implementing Real User Monitoring (RUM) and Crashlytics
While synthetic testing inside emulators and QA environments is necessary, it cannot capture the diverse real-world conditions experienced by global users. Implementing Real User Monitoring (RUM) platforms (such as Firebase Performance Monitoring, Datadog Mobile, or Sentry) provides continuous telemetry on production performance.
Key metrics that engineering leaders must track across production cohorts include:
Cold / Warm App Startup Duration (P50, P90, P95): Identifying launch regressions across specific device models and OS versions.
Slow / Frozen Frame Rates: Tracking the percentage of user sessions that experience frame rendering times exceeding 16 ms (slow) or 700 ms (frozen).
Network Request Latency & Failure Rates: Isolating regional API endpoint degradation, DNS lookup delays, and HTTP 5xx spikes.
Crash-Free Sessions and Users: Maintaining visibility into crash and ANR rates categorized by app version and device tier.
The Necessity of Testing on Low-Tier Physical Devices
A pervasive blind spot in modern mobile development is the reliance on high-end flagship devices for internal testing. Engineering and product teams equipped with modern hardware frequently fail to observe frame drops, memory thrashing, and thermal throttling that occur on mid-to-low-tier hardware.
Development teams must establish a physical device testing matrix that includes representative low-memory (2 GB to 3 GB RAM) devices running throttled 3G/4G network simulations. Incorporating automated performance test suites into CI/CD pipelines—running UI automation scripts on physical cloud device farms (such as AWS Device Farm or Firebase Test Lab)—ensures that performance regressions are detected and blocked before reaching production users.
Prioritizing Proactive Performance Engineering
Sustained mobile performance is the direct result of deliberate architectural choices, disciplined memory management, and continuous production monitoring. Organizations that treat optimization as an ongoing engineering practice rather than a reactive bug-fixing effort achieve higher conversion rates, superior app store visibility, and reduced infrastructure overhead.
Engineering leaders and product stakeholders must integrate performance benchmarks into their standard definition of done. By establishing strict SLAs for startup times, crash rates, and frame stability, teams can prevent technical debt from accumulating. As mobile applications continue to grow in functional complexity, maintaining an efficient, lightweight, and responsive core architecture remains one of the most effective strategies for securing sustained user loyalty and competitive market leadership.
Frequently Asked Questions
What is the single most effective way to improve mobile app cold startup time?
The most effective optimization is deferring non-critical SDK initializations and dependency injection setup from the main startup path to background threads. Additionally, flattening the initial view hierarchy and utilizing lazy-loading for secondary data ensures the application renders its first interactive frame within sub-second thresholds.
How can developers prevent memory leaks caused by closures in iOS?
Developers must use weak capture lists (@@CODE 0@@) within escaping closures and asynchronous task blocks to avoid strong reference cycles. Furthermore, ensuring that delegates are declared with the @@CODE 1@@ keyword prevents two interacting classes from permanently retaining each other in memory.
What causes Application Not Responding (ANR) errors on Android?
ANRs occur when the application's main UI thread is blocked for more than 5 seconds by long-running operations like synchronous disk I/O, database queries, heavy JSON parsing, or network calls. Offloading these tasks to background coroutines via @@CODE 0@@ or @@CODE 1@@ eliminates ANR triggers.
How much does mobile app bundle size affect user acquisition?
Studies show that every 6 MB increase in binary size can cause a measurable 1% drop in download conversions over cellular networks. Utilizing Android App Bundles (AAB), dynamic feature modules, and asset optimization formats like WebP significantly reduces installation friction.
What is the acceptable threshold for crash-free sessions in a production app?
Enterprise-grade mobile applications should target a minimum of 99.5% crash-free sessions, with top-tier products maintaining 99.9% or higher. Falling below 99.0% damages user retention and risks distribution penalties in Google Play and Apple App Store search algorithms.
When should a mobile app use Protocol Buffers instead of standard JSON?
Protocol Buffers (Protobuf) should be adopted when an application processes high-frequency data streams, operates under strict bandwidth constraints, or requires minimal CPU deserialization overhead. For standard CRUD apps, Brotli-compressed JSON is generally sufficient and easier to debug.
How does image rendering impact frame rate (FPS) during list scrolling?
Loading full-resolution images into memory without downsampling overwhelms the GPU and main thread during layout passes, causing dropped frames. Decoding images to match the exact view dimensions and caching bitmaps in memory via libraries like Glide or Kingfisher ensures smooth 60+ FPS scrolling.
Why is synthetic testing on emulators insufficient for mobile performance QA?
Emulators run on powerful desktop CPUs with stable high-speed internet, masking real-world mobile constraints like thermal throttling, memory pressure, and erratic cellular handoffs. Testing on mid-to-low-tier physical devices under throttled network conditions is necessary to reveal production bottlenecks.