Тупит — это пользовательское описание ситуации, когда мобильное приложение работает медленно и непостоянно: то откликается нормально, то внезапно подвисает на несколько секунд. В техническом контексте «тупит» означает комбинацию лагов и микрозависаний, вызванную частыми 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("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
Устранение тупления требует адресной работы с каждой причиной. Универсального решения нет — нужен анализ конкретных профилей производительности.
Если список содержит 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("Sync", "Syncing data in background thread")
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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также