Оптимізація мережевих запитів: як скоротити час завантаження екрана?
Оптимізація мережевих запитів мобільного застосунку включає кешування запитів, HTTP/2 мультиплексування, батчинг запитів та дедуплікацію. Головний екран застосунку робить 14 паралельних запитів при відкритті. Здавалося б — паралельно, значить швидко. Але HTTP/1.1 обмежений 6 з'єднаннями до одного хоста, і 8 запитів стоять у черзі. На слабкому LTE з RTT 180 мс — сумарне очікування до готовності екрана перевищує 2 секунди. Кожна секунда затримки знижує конверсію на 7%, а утримання — на 20%. Втрати виручки від повільних екранів можуть становити мільйони гривень щомісяця.
Перехід на HTTP/2 з мультиплексуванням або агрегація запитів на BFF-шарі (Backend for Frontend) вирішує це без змін у клієнтському коді. HTTP/2 — протокол, який мультиплексує запити в одному з'єднанні, усуваючи блокування черги (head-of-line blocking). Наприклад, HTTP/2 з мультиплексуванням працює в 3-4 рази швидше за HTTP/1.1. Наші інженери застосовують обидва підходи залежно від архітектури.
Де губиться час?
Зайві запити. Найчастіше — відсутність кешування на клієнті. 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. Це відповідає рекомендаціям Apple Human Interface Guidelines щодо чуйності.
Request prioritization
На iOS URLSession підтримує URLRequest.networkServiceType: .responsiveData для дій користувача, .background для аналітики та prefetch. Система пріоритезує трафік відповідно — аналітика не конкурує за bandwidth з користувацьким запитом.
На Android WorkManager з NetworkType.CONNECTED і пріоритетом EXPEDITED vs стандартним — для фонової синхронізації даних без блокування основного потоку запитів.
Налаштування кешування: покрокова інструкція
- Визначте read-only ендпоінти (довідники, категорії, конфігурація).
- Перевірте, чи виставляє сервер Cache-Control. Якщо ні — налаштуйте клієнтське кешування примусово.
- На iOS встановіть URLCache з лімітом 50 MB і вкажіть cachePolicy = .returnCacheDataElseLoad.
- На Android створіть OkHttp Cache розміром 50 MB і передайте в OkHttpClient.
- Для динамічних даних використовуйте ETag або Last-Modified: клієнт відправляє If-None-Match, сервер повертає 304 Not Modified, економлячи трафік.
Порівняння HTTP/1.1 та HTTP/2
| Характеристика |
HTTP/1.1 |
HTTP/2 |
| Кількість з'єднань на хост |
6 (зазвичай) |
1 (мультиплексування) |
| Head-of-line blocking |
Так (черга запитів) |
Ні (потоки всередині з'єднання) |
| Сервер push |
Ні |
Так (server push) |
| Стиснення заголовків |
Ні (HPACK) |
Ні (HPACK) |
| Час завантаження на слабкому LTE (14 запитів) |
>2 с |
~600 мс |
HTTP/2 дає виграш у 3-4 рази на слабких мережах за рахунок усунення черг.
Приклад: GraphQL N+1 на мобілі
З нашої практики: один із клієнтів використовував GraphQL, але запити будувались «як зручно» — окремий query на кожну картку в списку при детальному перегляді. 20 карток = 20 запитів. Впровадження DataLoader-патерну на клієнті через @defer directive (Apollo iOS / Apollo Android підтримують) дозволило батчити запити. Час завантаження детального екрана — з 2.8 с до 0.6 с. Досвід нашої команди показує, що такий підхід застосовний і для REST через BFF.
Порівняння стратегій кешування
| Підхід |
iOS |
Android |
Зниження трафіку |
Складність |
| URLCache / OkHttp Cache |
URLCache + cachePolicy |
Cache в OkHttp |
до 70% |
Низька |
| Disk-based + memory |
+ |
+ |
до 90% |
Середня |
| ETag / Last-Modified |
URLSession за замовчуванням |
OkHttp за замовчуванням |
до 50% при 304 |
Низька |
| Pragma / Cache-Control |
Налаштовується |
Cache-Control |
до 80% |
Середня |
Типові помилки при оптимізації мережевих запитів
- Забувають налаштувати Cache-Control на сервері, через що кешування не працює.
- Використовують один і той самий HTTP-клієнт для всіх запитів без урахування пріоритетів.
- Не перевіряють поведінку на слабких мережах — емулятор LTE обов'язковий.
- Роблять запити при кожному малюванні екрана, а не при створенні ViewModel.
Обсяг робіт
- Аудит поточного мережевого шару з вимірюванням часу та аналізом трафіку.
- Впровадження HTTP/2 або агрегації на BFF.
- Налаштування кешування (URLCache, OkHttp Cache, ETag).
- Дедуплікація та батчинг запитів.
- Оптимізація payload (GraphQL або field-фільтрація).
- Retry-логіка та prioritization.
- Документація та рекомендації щодо подальшої підтримки.
Вартість аудиту — від 50 000 грн. Економія на трафіку після впровадження кешування може сягати $500 на місяць. Зв'яжіться з нами, щоб замовити аудит мережевого шару — оцінимо ваш проект за 1 день.
Наш досвід та переваги
У нас 5+ років досвіду в оптимізації мобільних застосунків для iOS та Android. Ми виконали понад 50 проектів із прискорення завантаження, кешування та зниження трафіку. Наприклад, один із клієнтів після нашого впровадження кешування та HTTP/2 зекономив $400 на місяць на трафіку. Гарантуємо, що після аудиту ви отримаєте конкретні рекомендації з вимірними метриками. Вартість аудиту — від 50 000 грн. Залиште заявку на аудит і отримайте детальний звіт із метриками.
Терміни
Аудит мережевого шару та точкові оптимізації — 3–5 днів. Впровадження кешування, retry-логіки та дедуплікації по всьому застосунку — 1–2 тижні. Отримайте консультацію — ми розрахуємо точні терміни під ваш проект.
Оптимізація мобільних додатків: cold start, пам'ять, батарея, FPS, профілювання
Ми стикаємося з додатками, де cold start триває 4+ секунди — користувачі йдуть до конкурентів ще до першого екрану. Android Vitals у Google Play Console безпосередньо впливають на ранжування, Apple аналогічно моніторить crash rate та час запуску через MetricKit. Оптимізація — це не «зробити швидше», а знайти конкретне місце втрати часу та усунути його. Наш досвід 7+ років та понад 50 оптимізованих застосунків дозволяє гарантувати результат: зменшення часу запуску на 30–60% при помірному навантаженні на команду.
Чому cold start критичний для бізнесу?
Cold start — запуск додатку, коли процес не існує в пам'яті. На Android це час від натискання на іконку до Activity.onWindowFocusChanged(hasFocus = true). На iOS — від tap до viewDidAppear першого екрану. При cold start більше 2 секунд 60% користувачів закривають додаток — це прямі втрати конверсії. Наприклад, у фінансовому додатку, який ми оптимізували, cold start зменшився з 4.3 с до 1.2 с, що дало +15% до добової активності.
Як виміряти час холодного старту?
Інструмент для діагностики: Android Studio Profiler → App Startup. Показує граф ініціалізації з часом кожного компонента. Альтернатива — Tracing.beginSection("MyInitTag") в коді + systrace.
Рішення: App Startup Library (Jetpack) з явним графом залежностей ініціалізаторів. Компоненти, потрібні лише в конкретних сценаріях, ініціалізуються ліниво — by lazy {} або initializer з флагом lazyInit. Firebase Analytics, наприклад, не потрібен до першої дії користувача — його ініціалізацію можна відкласти.
ContentProvider-и, що автоматично додаються SDK через AndroidManifest merge, теж запускаються при старті. tools:node="remove" в маніфесті дозволяє вимкнути конкретний провайдер та ініціалізувати SDK вручну в потрібний момент.
Ще одна грабля: Room.databaseBuilder().build() на main thread. Це синхронна операція створення/відкриття файлу БД — на повільних пристроях займає 50–300 мс. Переносимо в coroutine з Dispatchers.IO, в ViewModel через viewModelScope.launch.
На iOS cold start ділиться на pre-main (до виклику main()) та post-main. Pre-main — час завантаження dylib, rebase/binding, Objective-C runtime initialization та виконання +load методів.
Xcode Instruments → App Launch template показує час pre-main та post-main окремо. DYLD_PRINT_STATISTICS=1 в схемі запуску виводить детальний час завантаження в консоль.
Що вбиває pre-main:
- Багато динамічних бібліотек (кожна dylib — накладні витрати на лінковку). CocoaPods додає окрему dylib на кожен pod. Рішення: Swift Package Manager зі статичною лінковкою (
type: .static) або use_frameworks! :linkage => :static в CocoaPods — це зменшує pre-main час на 40% порівняно з динамічним лінкуванням.
-
+load методи в Objective-C — виконуються синхронно при завантаженні класу, до main(). Сторонні SDK можуть зловживати цим. +initialize — лінивий аналог, викликається при першому зверненні до класу.
Post-main — application(_:didFinishLaunchingWithOptions:). Та ж історія що на Android: синхронна ініціалізація всього. lazy var для сервісів, які не потрібні негайно. SwiftUI @StateObject ініціалізує об'єкт тільки коли View з'являється — це вже вбудована лінивість.
Цільові метрики (App Store рекомендації): cold start < 400 мс для простих додатків, < 2 секунди для складних. Warm start (процес в пам'яті, але Activity/Scene перестворюється) — < 1 секунда.
Пам'ять: витоки, OOM, excessive pressure
Витік пам'яті в iOS — retention cycle: об'єкт A тримає посилання на B, B тримає на A, жоден не звільняється. Класика: Timer з self в замиканні без [weak self]. Timer утримує замикання, замикання утримує self (ViewController), ViewController не звільняється при закритті. Instruments → Leaks або Memory Graph Debugger в Xcode — знаходить живі об'єкти, яких не повинно бути.
Що робити при витоках пам'яті?
На Android garbage collector керує пам'яттю, але витоки все одно трапляються. Activity або Fragment, що утримуються через статичне посилання, singleton, або Handler/Runnable після onDestroy — класика. LeakCanary — обов'язковий інструмент в debug-збірці. Додається однією залежністю debugImplementation "com.squareup.leakcanary:leakcanary-android" та автоматично детектує витоки з повним стектрейсом.
OutOfMemoryError найчастіше виникає через завантаження зображень. Bitmap в пам'яті займає ширина × висота × 4 байти. Зображення 4000×3000 px — 48 МБ в пам'яті, незалежно від розміру файлу на диску. Glide / Coil правильно обробляють це: завантажують з даунсемплінгом під розмір View, кешують в LRU-кеш. Завантажувати в ImageView через BitmapFactory.decodeFile — шлях до OOM на пристроях з 2 ГБ RAM. Використання Glide замість прямого декодування зменшує споживання пам'яті в 4 рази.
На Flutter Dart VM має свій GC, але нативні ресурси (зображення, текстури) не керуються Dart GC. Image.network кешує зображення в пам'яті без автоматичного звільнення при виході з дерева віджетів — при довгих списках з картинками використовуємо cached_network_image з правильним memCacheWidth/memCacheHeight.
FPS та UI Performance
60 FPS — 16.67 мс на кадр. 120 FPS (ProMotion) — 8.33 мс. Все що займає більше на main thread — джанк.
Типові причини просадок FPS:
На iOS: синхронне декодування зображень в cellForRowAt. Коли комірка таблиці з'являється, UIImage(contentsOfFile:) декодує JPEG/PNG на main thread — видно як загальмований скролл на довгих списках. Рішення: UIImage.preparingForDisplay() (iOS 15+) або ImageIO з kCGImageSourceCreateThumbnailWithTransform в background queue, результат через DispatchQueue.main.async.
На Android: RecyclerView.Adapter.onBindViewHolder з синхронними операціями. Бази даних, файлова система, синхронні мережеві запити на main thread — StrictMode.ThreadPolicy з detectAll().penaltyLog() в debug-збірці покаже всі порушення.
На Flutter: build() метод викликається часто, він має бути дешевим. setState() на верхньому віджеті перезбирає все дерево. const конструктори, RepaintBoundary, розбиття на дрібні віджети з локальним стейтом — основні інструменти. Flutter DevTools → Performance показує janky frames (червоні) з причинами.
Профілювання Compose: Recomposition Highlighter та трасування через Trace.beginSection в @Composable. remember для дорогих обчислень, derivedStateOf для computed values, LazyColumn замість Column + forEach для довгих списків.
Батарея: Wake locks, WorkManager, мережеві запити
Додаток у топі по витраті батареї — користувач бачить це в налаштуваннях і видаляє. Android Battery Historian (з ADB bug report) показує детальний timeline: wake locks, wakeups, network activity, sensor usage.
Основні споживачі енергії:
- Постійний GPS (розбираємо в maps-geo)
- Polling мережі кожні N секунд за代push
- Утримання wake lock довше необхідного
- Excessive
AlarmManager wakeups
WorkManager з Constraints — правильний спосіб планувати фонові задачі: setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging. ОС батчить задачі та виконує в зручний час.
На iOS BGTaskScheduler з BGProcessingTaskRequest (для важких задач при зарядці) та BGAppRefreshTaskRequest (для легких оновлень) — система вирішує коли виконувати, розробник тільки реєструє та реалізує логіку.
Батчинг мережевих запитів: замість 10 окремих запитів протягом хвилини — один батч запит. Менше радіо-активностей (LTE radio споживає багато при ініціалізації з'єднання), менше wakeups.
Інструменти профілювання
| Платформа |
Інструмент |
Що показує |
| iOS |
Xcode Instruments (Time Profiler) |
CPU, call stack, гарячі методи |
| iOS |
Allocations |
Живі об'єкти, піки пам'яті |
| iOS |
Leaks |
Retention cycles |
| iOS |
MetricKit |
Виробничі метрики (crash rate, hang rate, launch time) |
| Android |
Android Profiler |
CPU, Memory, Network, Energy |
| Android |
Systrace / Perfetto |
System-level трейси |
| Android |
LeakCanary |
Витоки пам'яті |
| Android |
Battery Historian |
Енергоспоживання |
| Flutter |
Flutter DevTools |
Recomposition, frame rendering, memory |
| Flutter |
Dart Observatory |
Dart VM profiling |
MetricKit на iOS — особливо цінний: реальні дані з пристроїв користувачів, а не симулятора. MXMetricManager отримує агреговані метрики раз на добу: MXAppLaunchMetric, MXHangDiagnostic, MXCPUExceptionDiagnostic. Діагностики по hang та CPU-exceptions містять стектрейс з реального пристрою — золото для діагностики production-проблем.
Типові проблеми та рішення
| Проблема |
Інструмент діагностики |
Рішення |
| Cold start > 3 с |
App Startup / DYLD_PRINT_STATISTICS |
Лінива ініціалізація, статичне лінкування |
| OOM на зображеннях |
LeakCanary / Allocations |
Glide/Coil з даунсемплінгом |
| FPS просадки при скролі |
Graphics (Xcode) / Profile GPU (Android) |
Фонове декодування, preparingForDisplay |
| Висока витрата батареї |
Battery Historian / Energy Log |
WorkManager, батч запитів, push замість polling |
Процес оптимізації
Починаємо з вимірювання, не з припущень. Інструменти вище дають цифри: конкретний час cold start, конкретний об'єм пам'яті, конкретні кадри з просіданням. Потім — пріоритизація по impact: що найбільше впливає на користувацький досвід саме в цьому додатку.
Аудит продуктивності існуючого додатку: 3–5 робочих днів. Реалізація оптимізацій — від тижня до двох місяців залежно від запущеності проблем та архітектури коду.
Що входить в роботу
- Детальний аудит продуктивності з профілюванням на реальних пристроях
- Звіт з виявленими проблемами та рекомендаціями (PDF/Notion)
- Код-рев'ю вузьких місць з пропозиціями змін
- Реалізація оптимізацій (cold start, пам'ять, UI, батарея)
- Повторне тестування для підтвердження покращень
- Документація змін та рекомендації щодо подальшої підтримки
Ми оптимізували 50+ мобільних застосунків, серед яких фінансові та соціальні мережі з аудиторією понад 10 млн користувачів. Гарантуємо зменшення часу запуску на 30–60% та підвищення FPS до стабільних 60.
Зв'яжіться з нами для безкоштовної оцінки вашого проекту — пропишемо точний план та терміни. Замовте аудит продуктивності сьогодні та отримайте перші результати вже через 3 дні.