Оптимизация сетевых запросов мобильного приложения
Главный экран приложения делает 14 параллельных запросов при открытии. Казалось бы — параллельно, значит быстро. Но HTTP/1.1 ограничен 6 соединениями к одному хосту, и 8 запросов стоят в очереди. На слабом LTE с RTT 180 мс — суммарное ожидание до готовности экрана превышает 2 секунды. Переход на HTTP/2 с мультиплексированием или агрегация запросов на BFF-слое (Backend for Frontend) решает это без изменений в клиентском коде.
Где теряется время
Лишние запросы. Самое частое — отсутствие кэширования на клиенте. URLSession на iOS по умолчанию уважает Cache-Control заголовки, но только если сервер их выставляет. Если API возвращает Cache-Control: no-store «для надёжности» — каждое обращение к справочным данным (категории, настройки, конфигурация) идёт по сети. URLCache с лимитом 50 MB и ручная установка URLRequest.cachePolicy = .returnCacheDataElseLoad для read-only эндпоинтов работает как быстрый патч.
Избыточные payload. REST-эндпоинт для списка пользователей возвращает 40 полей, из которых UI использует 4. На списке из 100 элементов это лишние 60–80 KB JSON на каждый запрос. GraphQL решает это на уровне протокола, но если GraphQL нет — ?fields=id,name,avatar_url как query-параметр фильтрации хотя бы частично спасает ситуацию.
Повторные запросы при ротации экрана. На Android ViewModel + LiveData / StateFlow держат результат запроса и не перезапускают его при пересоздании Activity. Но если запрос живёт в Fragment.onViewCreated без проверки — при каждой ротации идёт новый сетевой вызов. Диагностируется через Charles Proxy или OkHttp EventListener с логированием.
Инструменты и решения
iOS (URLSession / Alamofire / Moya):
Alamofire RequestInterceptor — удобное место для retry-логики с 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 с waitsForConnectivity = true — запрос автоматически ждёт восстановления сети вместо немедленной ошибки. Критично для offline-first приложений.
Android (OkHttp / Retrofit):
OkHttp CacheInterceptor уже встроен, достаточно передать Cache при создании клиента:
val cache = Cache(context.cacheDir, 50L * 1024 * 1024)
val client = OkHttpClient.Builder().cache(cache).build()
Retrofit + suspend fun — автоматическая отмена запроса при смерти coroutine scope. Главное — привязывать scope к viewModelScope, а не к GlobalScope.
Дедупликация запросов. Если несколько компонентов одновременно запрашивают один ресурс — выполнять запрос один раз. На iOS — Combine с share() оператором на Publisher. На Android — StateFlow в Repository: первый подписчик запускает запрос, остальные получают результат из того же flow.
Request prioritization
На iOS URLSession поддерживает URLRequest.networkServiceType: .responsiveData для пользовательских действий, .background для аналитики и prefetch. Система приоритизирует трафик соответственно — аналитика не конкурирует за bandwidth с пользовательским запросом.
На Android WorkManager с NetworkType.CONNECTED и приоритетом EXPEDITED vs стандартным — для фоновой синхронизации данных без блокировки основного потока запросов.
Кейс: GraphQL N+1 на мобиле
Приложение использовало GraphQL, но запросы строились «как удобно» — отдельный query на каждую карточку в списке при детальном просмотре. 20 карточек = 20 запросов. Внедрение DataLoader-паттерна на клиенте через @defer directive (Apollo iOS / Apollo Android поддерживают) позволило батчить запросы. Время загрузки детального экрана — с 2.8 с до 0.6 с.
Сроки
Аудит сетевого слоя и точечные оптимизации — 3–5 дней. Внедрение кэширования, retry-логики и дедупликации по всему приложению — 1–2 недели.







