Baseline Profiles в Android: что на самом деле влияет на время холодного запуска в 2026 году
Практическое руководство по Android Baseline Profiles — генерация через Macrobenchmark, подключение в Gradle, честное измерение выигрыша и ещё три вещи, которые сильнее влияют на холодный запуск.
Холодный запуск — это тот показатель производительности, который чувствует каждый пользователь и почти никто не измеряет. Вы нажимаете на иконку, на мгновение появляется пустой экран или растянутый splash-экран, а затем приложение открывается. Меньше 500 мс — никто не замечает. Больше секунды — воспринимается как “медленно”, даже если каждый следующий экран открывается мгновенно: первое впечатление запоминается, а иконка приложения — это и есть первое впечатление.
Бо́льшая часть этого показателя не имеет отношения ни к вашей верстке, ни к запросу к базе данных. Это JIT-компилятор выполняет холодную, интерпретируемую работу над самыми горячими участками кода приложения, не имея никакого профиля для оптимизации. Baseline Profiles существуют именно для того, чтобы пропустить этот разогрев, и это один из немногих инструментов производительности Android, который стоит один вечер работы, а окупается при каждой последующей установке.
Что такое Baseline Profile на самом деле
Baseline Profile — это текстовый файл baseline-prof.txt, перечисляющий классы и методы, к которым обращается приложение в своих самых распространённых сценариях: при холодном запуске и в любых других путях, которые вы решите профилировать. Вы поставляете его внутри APK/AAB, и Android Runtime (ART) использует его, чтобы скомпилировать эти методы заранее (ahead-of-time), а не интерпретировать или JIT-компилировать их при первом использовании.
Эффект узкий, но реальный: он не делает код быстрее, он убирает налог на интерпретацию именно того кода, который выполняется при запуске. По данным Google в Play, это обычно сокращает время холодного запуска на 20-30% у приложений, у которых профиля раньше не было — ваш реальный выигрыш полностью зависит от того, какая доля пути запуска действительно покрыта.
Генерация профиля с Macrobenchmark
Профиль должен получиться из реального, инструментированного запуска — написать его вручную с пользой не получится. Для этого используется библиотека androidx.benchmark.macro:
// build.gradle.kts (модуль baselineprofile)
plugins {
id("androidx.baselineprofile")
}
dependencies {
baselineProfile(project(":app"))
}
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(
packageName = "com.mfk.hydrame",
includeInStartupProfile = true,
) {
pressHome()
startActivityAndWait()
// Прогоняйте не только splash, а сценарии, которые стоит скомпилировать заранее.
device.findObject(By.text("Log a glass")).click()
device.waitForIdle()
}
}
Запустите Gradle-задачу :app:generateBaselineProfile на реальном устройстве или достаточно быстром эмуляторе (для генерации профиля нужна сборка с инструментацией почти на уровне root, но плагин берёт это на себя). Она фиксирует каждый класс, задействованный во время этого сценарного прогона, и записывает baseline-prof.txt в app/src/main/, откуда плагин AGP baseline profile автоматически подхватывает его для release-сборок.
Часть, которую многие пропускают: сценарий должен охватывать больше, чем splash-экран. Если реальное узкое место — это первый рендер списка с данными из Room или первый вызов провайдера виджета, включите это взаимодействие в блок collect. Профиль, покрывающий только onCreate(), упускает бо́льшую часть реального выигрыша.
Честное измерение выигрыша
Не доверяйте секундомеру и своему пальцу. Используйте StartupTimingMetric в отдельном macrobenchmark-тесте, запустите его на сборке без профиля и на сборке с профилем, и сравните медианы на достаточном числе итераций, чтобы шум усреднился в обе стороны:
@Test
fun startupCompilationModes() {
benchmarkRule.measureRepeated(
packageName = "com.mfk.hydrame",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require),
) {
pressHome()
startActivityAndWait()
}
}
Для базового сравнительного прогона замените compilationMode на CompilationMode.None(). Десять итераций — разумный минимум: тепловое состояние устройства и фоновые процессы добавляют достаточно шума, чтобы три прогона обманули вас в любую сторону.
Что Baseline Profiles не исправляют
Они не заменяют реальное сокращение работы при запуске. Если ваш Application.onCreate() инициализирует три SDK, заранее открывает базу данных и раздувает тяжёлый первый экран, профиль лишь ускорит интерпретацию этой работы — он не сделает так, чтобы работы стало меньше. Выигрыш складывается вместе с этим, а не вместо:
- Отложенная инициализация некритичного. Всё, что не нужно для первого кадра — аналитические SDK, второстепенное планирование WorkManager, получение удалённой конфигурации — выполняется в фоновом диспетчере после того, как UI уже виден, а не в
onCreate(). - Режим R8 full mode в release-сборках. Уменьшение и оптимизация байткода означают, что ART в принципе придётся проходить меньше кода, независимо от наличия профиля.
- Ленивые синглтоны вместо инициализируемых заранее. Экземпляр базы данных
by lazy, инициализируемый при первом обращении к DAO, ничего не стоит при запуске, если первому экрану он пока не нужен.
Я обнаружил это, разрабатывая виджет главного экрана для Hydrame: собственный холодный запуск виджета (отдельный контекст процесса, без тёплого Application, на который можно опереться) оказался медленнее, чем у приложения, потому что ни один путь кода виджета не попал в профиль. Добавление в скрипт Macrobenchmark отдельного сценария для виджета — а не только для главной activity приложения — закрыло бо́льшую часть этого разрыва.
Когда этот вечер стоит потраченного времени
Если приложение уже быстрое, Baseline Profile — приятное, но не обязательное дополнение. Если холодный запуск — единственная метрика, горящая жёлтым в панели vitals Play Console, это обычно самый выгодный вечер работы, который можно потратить: без изменений архитектуры, без функциональных рисков — только сгенерированный файл и плагин Gradle. Генерируйте его раз на релиз из реальных критических путей, держите тест Macrobenchmark в CI, чтобы редизайн незаметно не вышел за рамки того, что покрывает профиль, и относитесь к показателю холодного запуска как к любой другой регрессии — как к тому, за чем следят, а не к тому, что чинят один раз и забывают.
// По теме
Ещё из журнала
Jetpack DataStore в 2026 году: переход с SharedPreferences без потери ни одной настройки
Практическое руководство по переходу с SharedPreferences на Jetpack DataStore на Android — асинхронные ловушки, путь миграции, сохраняющий существующие значения, и как это протестировать.
Разработка Mintly: как удержать точность таймера фокуса, когда Android хочет убить процесс
У работающего таймера Pomodoro проблема с надёжностью сложнее, чем у одноразового напоминания. Вот как Mintly переживает режим Doze, гибель процесса и дрейф при выключенном экране благодаря foreground-сервису и времени окончания по системным часам.
Сканирование штрихкодов с ML Kit на Android в 2026 году: как Stocky добавляет товар в кладовую меньше чем за секунду
Практическое руководство 2026 года по сканированию штрихкодов на устройстве с ML Kit и CameraX: настройка форматов, офлайн-поиск товара и математика частичного расхода в приложении для кладовой.