Тестирование производительности мобильного приложения
Приложение «работает нормально» на iPhone 15 Pro у разработчика. На Xiaomi Redmi 10A с Android 11, которым пользуется треть аудитории, главный экран загружается 4 секунды, а список при быстром скролле заикается до 20 fps. Firebase Crashlytics не покажет — это не крэш. App Store Reviews покажут через неделю после релиза.
Профилирование производительности — это не «запустили и посмотрели». Это воспроизводимые измерения с конкретными числами, сравнение до и после, локализация причины до конкретного метода.
iOS: Xcode Instruments
Instruments — основной инструмент на iOS. Шаблоны, которые используем:
Time Profiler — где CPU тратит время. Запускаем «тяжёлый» сценарий (скролл, загрузка экрана), смотрим Call Tree с Invert Call Tree + Hide System Libraries. Видим собственный код с процентами нагрузки.
Core Animation (Rendering) — FPS и причины просадок. Commit — время формирования слоёв, Render — время GPU. Если Commit высокий — проблема на main thread. Красная линия в 16.67 ms (60 fps) или 8.33 ms (120 fps, ProMotion) — наглядная граница.
Allocations — паттерны выделения памяти. Запускаем, совершаем действие, смотрим Generation Analysis. Если память после Release навигации не падает — утечка.
Пример реальной проблемы: в одном проекте экран с коллекцией фото тормозил при скролле. Time Profiler показал 23% времени на UIImage(data:) в cellForItemAt. Синхронная декодировка JPEG на main thread. Решение: ImageIO + kCGImageSourceShouldCacheImmediately: false + декодировка на background queue с DispatchQueue.global(qos: .userInitiated). FPS вырос с 35 до 58.
Метрики запуска
MetricKit (iOS 13+) собирает production-метрики с реальных устройств пользователей:
class AppMetricsObserver: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let launchMetrics = payload.applicationLaunchMetrics {
// resumeTime — время при background→foreground
// timeToFirstDraw — cold start
let coldStart = launchMetrics.histogrammedTimeToFirstDraw
// отправляем в аналитику
}
}
}
}
Это не синтетические измерения в тестах, а реальные данные с устройств пользователей. Дополняет профилирование в Instruments.
Android: Android Profiler и Macrobenchmark
В Android Studio — Android Profiler. CPU profiler в режиме Sample Java/Kotlin Methods для общей картины, Trace Java/Kotlin Methods для точного трассирования (с overhead'ом). System Trace — для взаимодействия с GPU, Choreographer, RenderThread.
Janky frames (кадры > 16 ms): adb shell dumpsys gfxinfo com.example.app | grep "Janky frames". Более 5% janky frames — проблема.
Macrobenchmark — библиотека из Jetpack для воспроизводимых измерений:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
}
}
Запускается на реальном устройстве (не эмуляторе), возвращает timeToInitialDisplay и timeToFullDisplay в миллисекундах. Результаты стабильны между запусками — это измерения, а не «запустили и засекли секундомером».
Slow rendering: Jetpack Compose
Для Compose — Recomposition счётчик в Layout Inspector. Но точнее — ComposeUiTest с measureRepeated:
@Test
fun scrollPerformance() {
benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
) {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// скроллим список
device.findObject(UiSelector().resourceId("com.example.myapp:id/feed_list"))
.flingForward()
}
}
FrameTimingMetric собирает данные о каждом кадре: frameOverrunMs — насколько кадр вышел за пределы бюджета.
Flutter: DevTools и flutter_driver
Flutter DevTools → Performance view показывает Frame chart с UI thread и Raster thread. Красные кадры — UI thread занят дольше 16 ms. Жёлтые — Raster thread.
Частая причина красных кадров: setState() перестраивает слишком большое поддерево. Решение — const конструкторы там, где данные не меняются, RepaintBoundary для изолирования перерисовки анимированных элементов.
// Плохо: весь экран перестраивается при каждом тике таймера
class CounterScreen extends StatefulWidget { ... }
// Лучше: только счётчик изолирован
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
const HeaderWidget(), // const — не перестраивается
CounterWidget(), // только эта часть ребилдится
]);
}
}
Что входит в работу
- Профилирование запуска приложения (cold start, warm start)
- Анализ FPS при скролле и навигации
- Поиск утечек памяти через Allocations / Memory Profiler
- Анализ CPU-профиля на тяжёлых операциях
- Macrobenchmark-тесты для Android, MetricKit-интеграция для iOS
- Отчёт с конкретными числами до/после и рекомендациями
Сроки
3–5 дней — профилирование, локализация проблем, отчёт. Если нужна ещё и реализация оптимизаций — оцениваем отдельно по объёму правок. Стоимость рассчитывается индивидуально.







