Оптимізація мобільних додатків: холодний старт, память, батарея, FPS, профілювання
Додаток із часом холодного старту 4+ секунди втрачає користувачів ще до першого екрану. Android Vitals в Google Play Console прямо впливає на ранжування в пошуку: додатки з поганими метриками отримують менший organic reach. Apple аналогічно моніторить crash rate та час запуску через MetricKit. Оптимізація—це не «зробити швидше», а зрозуміти де саме теряється час та що з цим робити.
Холодний старт: де теряється час до першого кадру
Холодний старт—запуск додатку, коли процес не існує в памяті. На Android це час від натиску на іконку до Activity.onWindowFocusChanged(hasFocus = true). На iOS—від tap до viewDidAppear першого екрану.
Android: головний потік перевантажений при ініціалізації
Application.onCreate()—головний враг швидкого старту на Android. Розробники ініціалізують тут все підряд: Firebase, Analytics, базу даних, HTTP-клієнт, DI-контейнер. Кожен SDK додає 20–200 мс на main thread.
Інструмент для діагностики: 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: Dyld linking та +load
На iOS холодний старт ділиться на pre-main (до виклику main()) та post-main. Pre-main—час завантаження dylib, rebase/binding, ініціалізація Objective-C runtime та виконання методів +load.
Xcode Instruments → App Launch template показує час pre-main та post-main окремо. DYLD_PRINT_STATISTICS=1 в схемі запуску виводить детальний час в консоль.
Що вбиває pre-main:
- Багато динамічних бібліотек (кожна dylib—накладні витрати на linkovку). CocoaPods додає окрему dylib на кожен pod. Рішення: Swift Package Manager зі статичною linkovкою (
type: .static) абоuse_frameworks! :linkage => :staticв CocoaPods. - Методи
+loadв Objective-C—виконуються синхронно при завантаженні класу, доmain(). Сторонні SDK можуть зловживати цим.+initialize—ленивий аналог, викликається при першому зверненні до класу.
Post-main—application(_:didFinishLaunchingWithOptions:). Та ж історія що на Android: синхронна ініціалізація всього. lazy var для сервісів, які не потрібні негайно. SwiftUI @StateObject ініціалізує об'єкт тільки коли View з'являється—це вже вбудована ліниість.
Цільові метрики (рекомендації App Store): холодний старт < 400 мс для простих додатків, < 2 секунди для складних. Теплий старт (процес в памяті, але 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 правильно обробляють це: завантажують з downsampling під розмір View, кешують в LRU-кеш. Завантажувати в ImageView без Glide/Coil через BitmapFactory.decodeFile—шлях до OOM на пристроях з 2 ГБ RAM.
На 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
AlarmManagerwakeups
WorkManager з Constraints—правильний способ планувати фонові завдання: setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging. ОС батчирує завдання та виконує у зручний час.
На iOS BGTaskScheduler з BGProcessingTaskRequest (для важких завдань при зарядці) та BGAppRefreshTaskRequest (для легких оновлень)—система вирішує коли виконувати, розробник тільки реєструє та реалізує логіку.
Батчинг сітьових запитів: замість 10 окремих запитів протягом хвилини—один батч запит. Менше radio-активностей (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-проблем.
Процес оптимізації
Починаємо з вимірювання, не з припущень. Інструменти вище дають цифри: конкретний час холодного старту, конкретний обсяг памяти, конкретні кадри з просадкою. Потім—пріоритизація за impact: що більше за все впливає на користувацький досвід саме в цьому додатку.
Аудит продуктивності існуючого додатку: 3–5 робочих днів. Реалізація оптимізацій—від тижня до двох місяців залежно від запущеності проблем та архітектури коду.







