Лаг у мобільному додатку — це помітна затримка між дією користувача та реакцією інтерфейсу, яка виникає через перевантаженість головного потоку, витоків пам'яті або неоптимальних операцій введення-виведення. На відміну від глюків, пов'язаних з логічними помилками, лаг — це проблема продуктивності: додаток працює коректно, але повільно. За даними AppDynamics Mobile App Performance Report 2024, 62% користувачів видаляють додаток, якщо він гальмує більше 3 секунд. Діагностика лагів потребує профілювання CPU, пам'яті та мережі за допомогою Android Studio Profiler та Xcode Instruments.
Головне
Лаг у мобільному додатку — це суб'єктивно відчутна затримка між дією користувача (дотик, свайп, введення тексту) та реакцією інтерфейсу. Технічно лаг вимірюється як час між подією введення та повним рендером кадру: комфортний поріг — до 100 мс, помітний — від 200 мс, критичний — понад 500 мс.
У користувацькій термінології «лагає» і «гальмує» часто використовуються як синоніми, але технічно лаг — це фіксована затримка (наприклад, 300 мс при кожному натисканні), а «гальмує» — непостійне уповільнення: додаток то працює плавно, то зависає на секунду. Глюк, на відміну від лагу, пов'язаний не зі швидкістю, а з коректністю відображення.
Google Play та App Store враховують показники продуктивності при ранжуванні додатків. ANR rate, частота джиттера (jank) та час запуску впливають на видимість у пошуку та конверсію встановлень. Додаток з постійними лагами втрачає до 40% користувачів після першого запуску.
Лаги виникають, коли головний потік UI не встигає обробити кадри з частотою 60 FPS (16.6 мс на кадр) або 120 FPS (8.3 мс). Розглянемо основні джерела затримок.
Будь-яка синхронна операція в UI-потоці — читання з SharedPreferences, робота з базою даних через Room без suspend, декодування зображення в Bitmap — блокує відтворення кадру. На Android це призводить до пропуску кадрів (jank), на iOS — до затримки рендеру Core Animation.
Коли збирач сміття (Garbage Collector) на Android або ARC на iOS виконує звільнення пам'яті, всі потоки призупиняються. Часті GC паузи виникають при створенні великої кількості тимчасових об'єктів — наприклад, при кожному виклику адаптера списку створюється новий екземпляр ViewHolder. Це проявляється як сіпане прокручування.
Вкладені ConstraintLayout, множинні LinearLayout, перекриті View — кожна вкладеність збільшує час на measure та layout pass. Xcode вказує, що глибока ієрархія шарів (більше 10 рівнів) викликає падіння FPS на 20-30%.
Для виявлення причин лагів використовуються профілювальники, вбудовані в IDE, та інструменти системного моніторингу. Кожен інструмент вирішує своє завдання.
CPU Profiler показує, які методи займають процесорний час та в яких потоках вони виконуються. Якщо метод з важкими обчисленнями виконується в main thread — це корінь проблеми. Запис трасування з увімкненим sample Java Method дозволяє побачити стек викликів у кожен момент часу та знайти «гарячі точки».
Аналогічний інструмент для iOS — Time Profiler — збирає вибірки стеку кожну мілісекунду та показує, який відсоток часу CPU зайнятий кожним методом. Комбінація з прапорцем Main Thread Only фільтрує лише операції на головному потоці, що прямо вказує на джерела лагів.
Повільні мережеві запити створюють враження лагів, навіть якщо UI-потік не заблокований. Network Profiler в Android Studio та Network Link Conditioner в Xcode дозволяють симулювати повільне з'єднання та виявити, як додаток поводиться в реальних умовах. Chunked-відповіді без прогресу та великі JSON-пейлоади — типові джерела уявних лагів.
Приклад профілювання мережевого запиту з OkHttp з виміром часу:
class TimingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val start = System.nanoTime()
val response = chain.proceed(chain.request())
val duration = (System.nanoTime() - start) / 1_000_000
Log.d("Timing", "Request took $duration ms")
return response
}
}
Усунення лагів потребує системної роботи: від оптимізації одного методу до архітектурних змін. Розглянемо найбільш ефективні техніки.
Kotlin Coroutines з диспетчером Dispatchers.IO для мережевих запитів та Dispatchers.Default для обчислень гарантують, що головний потік залишається вільним для UI. На iOS Grand Central Dispatch з queue .global(qos: .userInitiated) для фонових завдань та .main для оновлення UI — стандартний підхід. Уникайте sync-операцій між чергами.
RecyclerView на Android та UICollectionView на iOS потребують правильного налаштування: ViewHolder з мінімальним створенням об'єктів в onBindViewHolder, DiffUtil для обчислення змін, prefetching для завантаження даних заздалегідь. На iOS використовуйте diffable data source для анімованих оновлень без ручного керування.
Завантаження одного й того ж зображення при кожному прокручуванні — гарантований лаг. Coil (Android) та Kingfisher (iOS) кешують зображення в пам'яті та на диску, забезпечуючи миттєве відображення при повторному запиті. Для даних використовуйте Room з кешуючим шаром на основі Flow або Combine.
Приклад налаштування кешування зображень з Coil на Android:
val imageLoader = ImageLoader(context) {
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
crossfade(true)
size(512, 512)
}
// Loading with auto-caching enabled
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
}
Запобігти лагам дешевше, ніж виправляти їх у продакшні. Профілактичні заходи вбудовуються в процес розробки на рівні інструментів та архітектури.
StrictMode — вбудований інструмент Android, який виявляє випадкові операції введення-виведення та мережеві виклики на головному потоці на етапі розробки. Увімкніть його в Application.onCreate з політикою penaltyDeath для критичних порушень. Це єдиний спосіб гарантувати, що розробник побачить проблему до коміту.
Аналог для iOS — Main Thread Checker в Xcode, що входить до Runtime Sanitization — автоматично перевіряє, що всі виклики UIKit та AppKit виконуються з головного потоку. Увімкніть його в схемі збірки Debug та досягніть нульових попереджень в CI.
Додайте в CI-пайплайн прогін Macrobenchmark (Android) та XCTMetrics (iOS) для вимірювання часу запуску, FPS прокручування та використання пам'яті. Встановіть пороги: якщо новий коміт збільшує час запуску більш ніж на 5% — збірка падає.
Часті запитання
Лаг — це суб'єктивне відчуття затримки, яке може виникати і при високому FPS, якщо затримка викликана часом обробки введення, а не рендеру. Низький FPS (менше 30 кадрів/с) — одна з причин лагів, але не єдина.
Використовуйте Frame Timing API на Android (Choreographer) та CADisplayLink на iOS для вимірювання часу між кадрами. Google Play Vitals показує jank rate в реальних умовах. Для точних вимірювань застосовуйте Macrobenchmark зі сценаріями прокручування.
Старі пристрої мають менше ядер CPU, менший об'єм RAM та повільнішу пам'ять. Операція, яка виконується за 5 мс на флагмані, на бюджетному пристрої може зайняти 50 мс. Тестуйте продуктивність на пристроях нижнього сегмента та встановлюйте Baseline Profiles для AOT-компіляції.
Так, це один із найефективніших способів. Зображення високої роздільної здатності займають багато пам'яті та CPU часу на декодування. Використовуйте downscale до розміру View, формати WebP (Android) та HEIC (iOS), а також кешування через Coil або Kingfisher.
SwiftUI автоматично оптимізує оновлення через diffing, що знижує ризик лагів при зміні даних. Однак складні ієрархії та часті перебудови body можуть викликати падіння FPS. UIKit дає більше контролю над продуктивністю, але потребує ручної оптимізації.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також