Optimizing Network Requests in Mobile Apps
Main app screen makes 14 parallel requests on opening. Seems parallel, so fast. But HTTP/1.1 limited to 6 connections to one host, and 8 requests queue. On weak LTE with 180 ms RTT — total wait before screen ready exceeds 2 seconds. Switch to HTTP/2 with multiplexing or aggregating requests on BFF layer (Backend for Frontend) solves it without client code changes.
Where Time Gets Lost
Unnecessary requests. Most common — missing client-side caching. URLSession on iOS respects Cache-Control headers by default, but only if server sets them. If API returns Cache-Control: no-store "for safety" — each reference data call (categories, settings, configuration) goes over network. URLCache with 50 MB limit and manual URLRequest.cachePolicy = .returnCacheDataElseLoad for read-only endpoints works as quick patch.
Excessive payload. REST endpoint for user list returns 40 fields, UI uses 4. On 100-element list that's 60–80 KB extra JSON per request. GraphQL solves this at protocol level, but without GraphQL — ?fields=id,name,avatar_url as query-parameter filtering at least partially saves.
Repeated requests on screen rotation. On Android ViewModel + LiveData / StateFlow holds request result and doesn't restart on Activity recreation. But if request lives in Fragment.onViewCreated without checking — each rotation makes new network call. Diagnosed via Charles Proxy or OkHttp EventListener with logging.
Tools and Solutions
iOS (URLSession / Alamofire / Moya):
Alamofire RequestInterceptor — convenient place for retry logic with exponential backoff:
func retry(_ request: Request, for session: Session, dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
let delay = min(pow(2.0, Double(request.retryCount)), 30.0)
completion(.retryWithDelay(delay))
}
URLSession with waitsForConnectivity = true — request auto-waits for network recovery instead of immediate error. Critical for offline-first apps.
Android (OkHttp / Retrofit):
OkHttp CacheInterceptor already built-in, just pass Cache when creating client:
val cache = Cache(context.cacheDir, 50L * 1024 * 1024)
val client = OkHttpClient.Builder().cache(cache).build()
Retrofit + suspend fun — auto-cancels request when coroutine scope dies. Main thing — bind scope to viewModelScope, not GlobalScope.
Request deduplication. If multiple components request same resource simultaneously — execute request once. On iOS — Combine with share() operator on Publisher. On Android — StateFlow in Repository: first subscriber starts request, others get result from same flow.
Request Prioritization
On iOS URLSession supports URLRequest.networkServiceType: .responsiveData for user actions, .background for analytics and prefetch. System prioritizes traffic accordingly — analytics doesn't compete with user request for bandwidth.
On Android WorkManager with NetworkType.CONNECTED and EXPEDITED priority vs standard — for background data sync without blocking main request thread.
Case: GraphQL N+1 on Mobile
App used GraphQL but queries built "as convenient" — separate query per card in list on detail view. 20 cards = 20 requests. Implementing DataLoader pattern on client via @defer directive (Apollo iOS / Apollo Android support) enabled batching. Detail screen load time — 2.8 s to 0.6 s.
Timelines
Network layer audit and point optimizations — 3–5 days. Implementing caching, retry logic and deduplication throughout — 1–2 weeks.







