Тупить — це користувацький опис ситуації, коли мобільний застосунок працює повільно та нерівномірно: то відповідає нормально, то раптово зависає на кілька секунд. У технічному контексті «тупить» означає комбінацію лагів і мікрозависань, спричинену частими GC-паузами, блокуванням головного потоку синхронними операціями та неоптимальними структурами даних. За даними Android Performance Benchmarking Guide, зниження часу відповіді з 300 мс до 100 мс підвищує утримання користувачів на 25%. Діагностика туплення потребує поєднання CPU та Memory-профілювання з аналізом частоти збирання сміття.
Головне
Тупить — неформальний термін, яким користувачі описують суб'єктивно повільну роботу застосунку. На відміну від лага, який проявляється як постійна затримка, туплення — це нерегулярні підвисання: застосунок може працювати ідеально кілька секунд, а потім «задуматися» на 1–3 секунди.
З точки зору профілювання, туплення проявляється як серія пропущених кадрів (jank) із піковими затримками понад 100 мс. На графіку FPS це виглядає як різкі провали: 60 → 20 → 55 → 10 кадрів на секунду. На відміну від лага з рівномірно низьким FPS, туплення має виражену варіативність.
Коли застосунок тупить, користувач не розуміє логіки сповільнень: екран може прокручуватися плавно, а потім раптово зупинитися на секунду. Це викликає фрустрацію та знижує довіру до застосунку. За даними Google, 53% користувачів залишають сайт або застосунок, якщо завантаження триває понад 3 секунди.
Непостійний характер туплення вказує на те, що проблема спричинена подієвими факторами, а не постійним перевантаженням. Розглянемо типові сценарії.
На Android у середовищі ART збирання сміття зупиняє всі потоки застосунку. Якщо в коді створюється багато тимчасових об'єктів — наприклад, під час кожного виклику onBindViewHolder створюється новий String через конкатенацію — GC запускається частіше. Пауза може тривати 5–50 мс залежно від розміру купи та покоління об'єктів. Користувач відчуває це як раптову «задумливість».
Room на Android та Core Data на iOS підтримують асинхронні запити, але розробники часто викликають getValue() або виконують запит через runBlocking для простоти. Важкий SELECT із джойнами на таблиці в 10 000 рядків може зайняти 200–500 мс, повністю блокуючи UI на цей час.
Завантаження зображення з камери (12 Мп, 4000x3000 px) без масштабування займає до 200 мс на декодування в Bitmap. Якщо зображення підвантажуються асинхронно, але без пулу потоків з обмеженням, одночасний запуск 5–6 декодувань може перевантажити CPU, спричинивши мігруючі гальмування.
Діагностика непостійних сповільнень складніша, ніж діагностика постійних лагів, оскільки проблема може не відтворюватися під час кожного запуску. Потрібен збір статистики за тривалий період.
Android Studio Memory Profiler показує не лише використання пам'яті, а й GC-події: частоту, тип (Concurrent, Full), тривалість. Якщо GC відбувається частіше ніж 1 раз на 5 секунд у спокійному стані — це ознака надмірної алокації. Запис heap dump у момент туплення дає змогу побачити, які об'єкти займають пам'ять.
На iOS використовуйте шаблон Allocations в Instruments для відстеження створення та звільнення об'єктів. Увімкніть покоління (Generations) — вони дають змогу робити знімки купи між діями та бачити, які об'єкти залишаються в пам'яті. Persistent objects, які не звільняються, — джерело накопичення пам'яті та подальших пауз.
JankStats — бібліотека Android, яка збирає метрики пропущених кадрів у реальному часі. Вона прив'язує кожен jank до поточного сценарію (наприклад, «прокрутка списку», «відкриття екрана»), що дає змогу зрозуміти, під час якої саме дії виникає туплення.
Приклад інтеграції JankStats для відстеження підвисань на Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Пропуски кадрів", "Тривалість=${frameData.durationMs}мс")
}
}
}
}
Усунення туплення потребує адресної роботи з кожною причиною. Універсального рішення немає — потрібен аналіз конкретних профілів продуктивності.
Якщо список містить 1000+ елементів і всі завантажуються одразу — це гарантоване туплення. Paging 3 на Android та NSFetchedResultsController на iOS завантажують дані порціями під час прокручування. Користувач бачить лише перші 10–20 елементів, решта підвантажується у фоні.
Room дає змогу профілювати запити через Inspection Tool в Android Studio: видно час виконання, кількість повернутих рядків і план запиту. Додавання індексів на стовпці WHERE та ORDER BY може скоротити час запиту з 300 мс до 5 мс. На iOS аналогічну перевірку виконує Core Data Profiler в Instruments.
Фонові синхронізації, завантаження файлів, обробка даних — усе це має виконуватися через WorkManager (Android) або Background Tasks (iOS). Якщо синхронізація запускається в UI-потоці, застосунок тупитиме на час виконання. WorkManager гарантує виконання у фоновому потоці з урахуванням стану батареї та мережі.
Приклад фонової синхронізації через WorkManager на Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Синхронізація", "Синхронізація даних у фоновому потоці")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
Запобігти тупленню можна на етапі написання коду, дотримуючись принципів ефективної роботи з пам'яттю та потоками.
Baseline Profiles — це список класів і методів, які Android компілює заздалегідь (AOT), а не JIT. Без профілю кожен новий екран компілюється під час першого відкриття, викликаючи затримку 100–500 мс. Підготуйте Baseline Profile для ключових екранів і увімкніть генерацію в Gradle через baseline-profile-gradle-plugin.
Hot path — це код, який виконується під час кожного кадра: onBindViewHolder, draw, layoutSubviews. Уникайте створення об'єктів у цих методах: використовуйте пул об'єктів, StringBuilder замість конкатенації, кешуйте сформовані рядки та форматери. Кожна зайва алокація наближає наступний GC.
Додайте в CI-пайплайн запуск Macrobenchmark зі сценарієм прокрутки списку та відкриття екрана. Установіть поріг: 99-й перцентиль часу кадра не має перевищувати 16 мс. Якщо поріг перевищено — збірка відхиляється до оптимізації.
Часті запитання
Лаг — це постійна затримка (наприклад, 200 мс на кожне натискання). Туплення — непостійне: застосунок працює нормально, потім раптово гальмує на 1–3 секунди, потім знову нормально. Причина — подієві фактори на кшталт GC-пауз або синхронних запитів до БД.
Використовуйте Memory Profiler в Android Studio: вкладка Memory показує GC-події з тривалістю. Для продакшн-моніторингу підключіть Firebase Performance Monitoring з кастомними трейсами. На iOS увімкніть Malloc Debug і позначте покоління алокацій в Instruments.
Опосередковано — так. Якщо відповідь сервера приходить із затримкою, а UI очікує її синхронно, застосунок зависає. Якщо запит асинхронний, але обробка відповіді виконується в UI-потоці — це теж спричинить туплення. Рішення — асинхронна обробка з корутинами та індикаторами прогресу.
За неправильного використання KMP може генерувати зайві об'єкти-обгортки для інтероперабельності. На iOS це збільшує частоту алокацій і, як наслідок, паузи ARC. Використовуйте @ObjCName, оптимізуйте expect/actual та уникайте частих викликів shared-коду з гарячих шляхів UI.
Збільшення heap через android:largeHeap="true" відкладає GC, але не усуває причину алокацій. Коли GC все ж запускається, пауза буде довшою, оскільки потрібно обійти більше об'єктів. Рішення — зменшити кількість алокацій, а не розширювати купу.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.