12 апр. 2026 г.·7 мин чтения

Аудит времени сборки Android перед покупкой более быстрого ноутбука

Аудит времени сборки Android показывает, связана ли медленная сборка с границами модулей, annotation processors или слишком широкими тестами, прежде чем вы потратите деньги на железо.

Аудит времени сборки Android перед покупкой более быстрого ноутбука

Почему медленная сборка не всегда проблема ноутбука

Более быстрый ноутбук может сэкономить несколько секунд на сборке. Но обычно он не решает главную потерю времени: лишнюю работу, которую сборке вообще не нужно было делать.

Если одно маленькое изменение кода заставляет пересобираться половину приложения, новое железо лишь на время скрывает проблему. Настоящая причина часто в структуре проекта, в том, как модули зависят друг от друга, или в лишних инструментах, которые запускаются на каждую правку.

Сначала разделите полные сборки и инкрементальные сборки. Полные сборки важны, но большинство разработчиков не запускают их весь день. Они меняют один файл, нажимают сборку, исправляют ошибку, запускают тесты и повторяют. Если инкрементальные сборки медленные, команда платит эту цену снова и снова.

Полная сборка за 12 минут — это неприятно. Инкрементальная сборка, которая занимает 90 секунд после каждого небольшого изменения, — ещё хуже. Десять коротких ожиданий за день могут навредить сильнее, чем одна долгая пауза перед обедом. И дело не только во времени на часах. Люди теряют концентрацию. После паузы в 45 секунд часто нужно ещё минуту, чтобы вернуться в задачу.

Во многих проектах основную задержку создают всего несколько модулей. Один общий модуль с широкими зависимостями может запускать пересборку по всему приложению. То же самое может делать тяжёлый annotation processor. Тогда команда винит «Android-сборки» в целом, хотя проблема начинается в одном-двух местах.

Полезно также разделить локальные задержки и работу только для релиза. Некоторые шаги медленные, но запускаются только в CI или только в релизных сборках — например, shrink, signing или большие тестовые наборы. Эти задачи важны, но они не объясняют, почему обычная работа с кодом ощущается медленной.

Хорошо работает простая схема:

  • Время полной сборки показывает верхнюю границу.
  • Время инкрементальной сборки показывает ежедневную цену.
  • Небольшое число модулей часто вызывает большую часть пересборок.
  • Релизные задачи и локальные задачи нужно анализировать отдельно.

Сначала измерьте именно это. Многие команды могут сократить время сборки Android, не покупая ни одного нового ноутбука.

Что измерить до любых изменений

Одна медленная сборка почти ничего не говорит. Команды часто помнят худший день и потом винят ноутбуки. Это плохая выборка.

Возьмите одну обычную неделю и записывайте те сборки, которые люди реально запускают во время работы. Оставьте задачи одинаковыми. Если один разработчик измеряет assembleDebug на feature-ветке, а другой — полную clean build после слияния main, такие цифры не помогут.

Достаточно короткого набора повторяемых проверок:

  • инкрементальная сборка приложения после небольшого изменения Kotlin-кода
  • сборка после изменения ресурса или manifest
  • полная debug-сборка
  • unit-тесты для одного модуля
  • один CI-пайплайн для обычного pull request

Общее время сборки важно, но важнее фазы Gradle. Configuration, выполнение задач, компиляция Kotlin, annotation processing, упаковка и тесты замедляются по разным причинам. Если тормозит configuration, проблема может быть в настройке проекта. Если время компиляции резко растёт после крошечной правки, стоит внимательнее посмотреть на границы модулей или сгенерированный код.

Записывайте, что именно изменилось перед каждой пересборкой. Маленькая правка UI не должна будить половину репозитория. Изменение dependency injection, схемы базы или общего model-объекта часто запускает больше работы.

Храните локальные результаты отдельно от результатов CI. Локальные сборки зависят от тёплых кешей, недавних команд, фоновых приложений и состояния машины. В CI ограничения другие. Свежие раннеры, сетевые загрузки, более строгие проверки и промахи кеша могут полностью изменить картину.

Обычной таблицы достаточно. Записывайте дату, ветку, команду, статус clean или incremental, тип изменения, общее время и то, какая фаза Gradle дала всплеск.

Через неделю причина обычно выглядит гораздо менее загадочной. Многие команды обнаруживают, что один processor, один слишком большой модуль или одна слишком широкая тестовая задача съедают больше времени, чем старое железо.

Сначала посмотрите на границы модулей

Маленькая правка не должна будить половину проекта. Если одно небольшое изменение запускает пересборки во многих модулях, проблема часто в форме проекта, а не в ноутбуке.

Начните с нескольких дешёвых проверок. Измените одну строку в экране фичи, одну общую утилиту, одно значение ресурса и один config-флаг. Потом посмотрите, какие модули пересобираются каждый раз. Вам нужен fan-out: локальная правка, которая расходится гораздо дальше, чем ожидалось.

Слишком широкие shared-модули обычно и создают самую сильную боль. Команды часто складывают в одно место общие модели, UI-части, networking-код, helpers для аналитики и настройку dependency injection, потому что это кажется удобным. Со временем такой модуль становится транспортным узлом. Поскольку от него зависят многие фичи, даже маленькое изменение заставляет пересобирать всё приложение.

На бумаге такая структура выглядит аккуратно, а в ежедневной работе ощущается ужасно.

Хорошие границы модулей делают одну вещь особенно хорошо: они оставляют общие изменения локальными. Правка экрана фичи обычно должна пересобирать эту фичу и app-модуль, который её упаковывает. Она не должна затрагивать другие фичи, общий слой данных и тестовые модули, если изменение действительно не пересекает эти границы.

Во время аудита времени сборки Android не спрашивайте только, сколько у вас модулей. Спросите, что происходит, когда меняется один из них. Больше модулей может помочь, но только если граф зависимостей остаётся узким и предсказуемым.

Проверьте annotation processors и сгенерированный код

Annotation processors часто добавляют больше времени к сборке, чем команды ожидают. В типичном Android-проекте они работают во время тех же задач, которые разработчики запускают весь день: build, install и test. Если локальные сборки медленные, сначала посмотрите, какие processors запускаются на этих обычных путях, а уже потом вините ноутбук.

Начните с processors, которые затрагивают много модулей или генерируют много кода. Небольшой processor в одном feature-модуле может почти не влиять на время. Но processor, связанный с dependency injection, схемами базы, загрузкой изображений или JSON adapters, может влиять почти на каждую правку.

KAPT заслуживает отдельного внимания. Он всё ещё работает, но часто ухудшает инкрементальные сборки, потому что крошечное изменение кода может запускать больше работы, чем ожидается. Если модуль использует KAPT, проверьте, не пересобирается ли он слишком сильно после маленьких правок. Команды часто замечают, что один processor заставляет ждать дольше, чем несколько обычных Kotlin-файлов.

Быстрый обзор обычно показывает одни и те же проблемы:

  • processors, которые работают во многих модулях, но экономят мало времени
  • старые библиотеки, которые оставили по привычке
  • сгенерированный код, скрывающий простую логику, которую команда могла бы написать вручную
  • настройки KAPT, которые можно перевести на KSP или обычный Kotlin

Сгенерированный код сам по себе не плох. Оставляйте его, если он убирает скучную и подверженную ошибкам работу или если библиотека решает реальную проблему поддержки. Room — частый пример. Некоторые схемы сериализации и dependency injection тоже оправданы. Но если processor экономит несколько строк, а к каждой сборке добавляет секунды, обмен слабый.

Проверяйте изменения по одному. Уберите один processor или переведите один модуль с KAPT, а потом измерьте ту же задачу ещё раз. Если менять пять вещей сразу, вы не поймёте, что именно помогло.

Такой медленный и скучный метод работает. В одной команде, с которой я работал, локальная debug-сборка заметно ускорилась после удаления устаревшего generator для view binding и замены небольшой схемы AutoValue на обычные Kotlin data class. Код стал проще, а сборка — быстрее.

Уменьшите объём тестов, не теряя безопасность

Ускорьте локальные сборки
Уберите тяжёлую работу из стандартного dev-цикла и оставьте рядом только быстрые проверки.

Многие Android-команды теряют время на тестах, которые не соответствуют тому, что разработчик только что изменил. Если человек правит подпись кнопки на одном экране, сложно оправдать запуск всех unit-, integration- и device-тестов в приложении.

Но безопасность важна. Объём тестов всё равно должен соответствовать риску.

Разделяйте типы тестов осознанно. Unit-тесты должны проверять небольшие куски логики и быстро запускаться при обычных локальных изменениях. Integration-тесты должны проверять, как несколько частей работают вместе, обычно на уровне модуля. UI-тесты должны доказывать, что реальные пользовательские сценарии по-прежнему работают, но они самые медленные и хрупкие, поэтому использовать их нужно осторожнее.

Практическое правило простое:

  • Запускайте локально unit-тесты для того модуля, который вы изменили.
  • Запускайте nearby integration tests, когда изменение затрагивает поток данных, навигацию или wiring.
  • Запускайте локально UI-тесты только когда вы меняете поведение экрана или сам пользовательский сценарий.
  • Полный набор device-тестов запускайте в CI, а не на каждом ноутбуке.
  • Широкие регрессионные прогоны планируйте ежедневно или перед релизом, а не после каждой мелкой правки.

Уже этого достаточно, чтобы убрать неожиданно много ожидания. Многие команды соглашаются на медленную обратную связь, потому что никто не хочет урезать покрытие и потом отвечать за проблему. Это понятный страх, но он часто приводит к ленивому стандарту: запускать всё, всегда, для любого изменения.

Это не безопаснее. Это просто медленнее.

Проведите недельный аудит по шагам

Полезный аудит времени сборки Android должен быть небольшим. Не начинайте с измерения каждой сборки на каждом компьютере. Возьмите одну задачу, которая раздражает команду, и повторяйте только её в течение недели.

Хороший пример — обычный локальный сценарий: изменить один экран фичи, поменять строку или layout, пересобрать и запустить приложение на устройстве. Именно этот цикл люди ощущают на себе.

Зафиксируйте базовую линию до любых изменений. Измерьте одну и ту же задачу три раза и запишите среднее значение и самый медленный прогон. Если одна сборка занимает 2 минуты, а другая — 5, это уже говорит о многом. Возможно, у вас не одна проблема.

Дальше работайте по неделе маленькими шагами.

В первый день зафиксируйте сценарий. Используйте одну и ту же ветку, один и тот же ноутбук и одни и те же шаги каждый раз. Записывайте полное время сборки, время инкрементальной сборки и то, сколько времени добавляют тесты.

На второй день проверьте границы модулей. Если небольшая UI-правка запускает пересборку в несвязанных модулях, исправьте одну проблему с границей и остановитесь на этом. Потом снова запустите тот же сценарий и запишите новое число.

На третий день проверьте annotation processors и сгенерированный код. Уберите или замените один тяжёлый processor, если можете, или ограничьте место, где он работает. Измерьте ту же задачу ещё раз.

На четвёртый день сократите локальный объём тестов. Если каждое маленькое изменение UI запускает широкие тестовые наборы, перенесите часть этой работы из стандартного локального пути.

На пятый день посмотрите на журнал и оставьте только те изменения, которые действительно экономят время. Правка, которая экономит 8 секунд, может быть приятной. Правка, которая экономит 2 минуты, меняет способ работы команды.

Есть правило, которое важнее, чем многие думают: меняйте только одну вещь за раз. Если в один день вы правите модули, меняете processors и урезаете тесты, вы не поймёте, что именно помогло.

Сохраняйте заметки в простой таблице. Через неделю вы должны знать, какие правки стоит оставить, какие почти не влияют на цифры, и остаётся ли hardware частью проблемы.

Частые ошибки, которые скрывают настоящую причину

Приведите в порядок границы модулей
Сократите широкие пересборки, перемещая нестабильный код ближе к тем функциям, которые меняются.

Команды часто винят Gradle или старые ноутбуки, но замедление обычно начинается в самом проекте. Аудит сборки часто вскрывает несколько привычек, которые поначалу кажутся разумными, а потом тихо прибавляют минуты к ежедневной работе.

Одна частая ошибка — дробить код на всё большее число модулей, не проверяя dependency graph. Больше модулей не всегда означает более быстрые сборки. Если одно изменение в shared-модуле заставляет пересобираться половину приложения, дополнительная структура не помогла. Я видел команды, которые превратили одно приложение в 30 модулей и всё равно пересобирали почти все после небольшой UI-правки.

Другая ловушка — добавлять code generation ради небольшой ручной экономии. Annotation processors могут быть дорогими, особенно когда они работают по большой части проекта. Сгенерированный класс может сэкономить десять строк кода, но если он добавляет 20 секунд к инкрементальной сборке, обмен плохой.

Команды также часто путают задержки локальной сборки с задержками CI. Обычно это разные проблемы. Локальные задержки могут быть связаны с annotation processors, неудачными границами модулей или слишком большими debug test tasks. Задержки CI могут быть связаны с clean build, медленным восстановлением кешей, эмуляторами или политикой ветки, которая запускает слишком много проверок на каждый push. Если считать это одной проблемой, можно неделями чинить не то.

Объём тестов разрастается очень обычно: никто не хочет быть тем, кто убрал тестовую задачу. Поэтому каждая ветка запускает всё. Unit-тесты, integration-тесты, UI-тесты, lint, screenshots и полные release-сборки стартуют одновременно, даже если изменилась одна подпись. Безопасность важна, но привычка — не стратегия тестирования.

Самая дорогая ошибка — сначала покупать железо. Более быстрые ноутбуки могут временно скрыть боль, но редко убирают лишнюю работу. Если плохие границы модулей или медленные processors добавляют минуту к каждой сборке, эта минута переезжает с командой на любое новое устройство.

Сначала измерьте. Тратьте позже.

Быстрые проверки перед покупкой нового железа

Более быстрый ноутбук может срезать несколько секунд. Он не исправит настройку сборки, которая пересобирает слишком много, запускает code generation не там, где нужно, или включает тяжёлые тесты для маленьких правок.

Начните с простого вопроса: что должно пересобираться после крошечного изменения? Измените одну строку или один layout-файл и посмотрите, что затронет Gradle. Если это изменение тянет за собой много модулей, у вас слишком свободные границы или зависимости направлены не туда.

Прежде чем кто-то откроет каталог ноутбуков, проверьте несколько базовых вещей:

  • Измените один файл в feature-модуле и убедитесь, что Gradle пересобирает только этот модуль и app-модуль, который от него зависит.
  • Посмотрите на shared-модули. Если в них одновременно лежат UI-хелперы, networking-код, test fixtures и общие модели, они будут часто меняться и заставлять пересобирать всё вокруг.
  • Проверьте annotation processors, особенно задачи KAPT или KSP. Оставляйте их только в тех модулях, которым действительно нужен сгенерированный код.
  • Разделите локальную debug-работу и медленные device-тесты.
  • Оставьте широкие regression-проверки в CI, где им и место.

Простой пример сразу делает это очевидным. Если разработчик меняет подпись кнопки на экране оформления заказа, сборка должна пересобрать feature оформления заказа и затем упаковать приложение. Она не должна заново запускать генерацию базы в чужих модулях, пересобирать общий код, который почти не меняется, и стартовать device-тесты для login, search и settings.

Команды покупают железо, потому что боль реальная. Ошибка в том, чтобы считать, будто её вызвал ноутбук.

Реалистичный пример команды

Пересмотрите объём тестов
Сделайте локальные проверки лёгкими и перенесите широкие device-прогоны туда, где им место.

Небольшая Android-команда уверена, что ей нужны более быстрые ноутбуки. Один разработчик меняет экран оформления заказа, нажимает Run и ждёт, пока пересоберутся шесть модулей. Изменение крошечное. Ожидание — нет.

Когда команда измеряет сборку, медленный участок оказывается не в коде экрана. Он в одном shared-модуле, от которого зависит почти каждая фича. Со временем туда положили слишком многое, потому что это казалось удобным: общие UI-хелперы, wrappers для аналитики, request-модели и utility-код. Теперь небольшая правка в одном месте будит большую часть приложения.

Два annotation processors добавляют ещё больше задержки. Один отвечает за dependency injection. Другой генерирует код базы данных. По отдельности ни один не выглядит проблемой, но оба стоят близко к shared-слою, поэтому небольшое изменение фичи снова и снова запускает лишнюю работу.

Команда пока не покупает новое железо. Сначала они выносят нестабильный код из shared-модуля и перемещают его ближе к тем фичам, которые часто меняются. В shared-слое остаётся только код, который меняется редко. После этого правка на экране оформления заказа обычно остаётся внутри этой фичи.

Они также меняют локальное тестирование. Раньше у них был простой и дорогой обычай: каждый локальный debug build сопровождался device-тестами. Это казалось безопасным, но замедляло ежедневную работу. Теперь быстрые unit-тесты остаются в локальном цикле, а device-тесты запускаются тогда, когда изменение затрагивает навигацию, хранилище, permissions или реальное поведение устройства.

Результат не волшебный. Просто меньше лишней работы. Сборка, которая занимала около 4 минут, при обычной правке фичи сокращается примерно до 90 секунд. Полные сборки по-прежнему занимают время, и так и должно быть. Но ежедневный цикл ощущается намного легче.

Ноутбуки не менялись. После аудита те же машины кажутся достаточно быстрыми, потому что проект перестаёт пересобирать код, который никто не трогал.

Что делать дальше

Превратите заметки аудита в короткий план, который команда сможет закрыть за две-три недели. У каждого пункта должен быть один ответственный, одна дата и одна метрика для контроля. Если за изменение никто не отвечает, оно отложится. Если его никто не измеряет, люди начинают гадать.

Начните с самого большого триггера пересборки, который вы нашли. Чаще всего это слабая граница модуля, дорогой annotation processor или тесты, которые заходят гораздо дальше изменённого кода. Выберите одно исправление, внедрите его и снова измерьте на той же ветке, с той же задачей и в той же конфигурации машины.

Простой план обычно выглядит так:

  • сначала исправить самый большой триггер пересборки
  • убрать, заменить или изолировать один дорогой annotation processor
  • вынести широкие тесты из стандартного локального workflow
  • сравнить время сборки до и после каждого изменения
  • вернуться к вопросу обновления ноутбуков только после того, как цифры перестанут двигаться

Отложите покупку ноутбуков, пока ваши метрики не останутся на месте хотя бы неделю или две. Более быстрое железо может помочь, но может и скрыть лишнюю работу. Если маленькое изменение кода всё ещё заставляет пересобираться половину проекта, новый ноутбук лишь немного быстрее закончить ту же ошибку.

Если вашей команде нужен внешний взгляд, Oleg на oleg.is предлагает помощь Fractional CTO для аудита сборки, чистки архитектуры и практичных AI-first workflows разработки. Такой внешний взгляд помогает, когда команда слишком близко к коду и каждая задержка начинает казаться нормой.

Хороший аудит времени сборки Android должен завершаться коротким списком исправлений, понятной ответственностью и меньшим ожиданием каждый день.

Часто задаваемые вопросы

Нужны ли нам действительно более быстрые ноутбуки, чтобы исправить медленные Android-сборки?

Обычно нет. Сначала проверьте, что пересобирается после маленького изменения. Если одно небольшое правка будит много модулей, новый ноутбук лишь на время скроет лишнюю работу.

Что стоит измерить первым в аудите времени сборки?

Смотрите на время инкрементальной сборки для обычного цикла правка-сборка. Именно это число показывает, сколько времени команда теряет каждый день, а не только во время полной пересборки.

Почему инкрементальные сборки важнее clean build?

Потому что разработчики большую часть дня делают маленькие правки, а не запускают clean build. Ожидание в 60–90 секунд после каждого изменения быстрее ломает концентрацию, чем редкий длинный clean build.

Как понять, что проблема на самом деле в границах модулей?

Измените одну строку в feature и посмотрите, какие модули пересобираются. Если вместе с нужным кодом пересобираются чужие фичи или общие слои, значит, граф зависимостей слишком широкий.

Какой тип модуля обычно сильнее всего увеличивает число пересборок?

Чаще всего это shared-модули. Когда в одном модуле лежат общие UI-утилиты, модели, аналитика и хелперы, от него зависит слишком много фич, и даже мелкое изменение расходится по всему приложению.

Часто ли annotation processors становятся причиной медленных сборок?

Очень часто — да. Процессоры для dependency injection, базы данных и сериализации могут добавлять много работы в обычные пути сборки, особенно если они запускаются во многих модулях.

Нужно ли сразу переводить всё с KAPT?

Если модуль использует KAPT, а небольшие изменения вызывают слишком много повторной компиляции, переход на KSP или обычный Kotlin может помочь. Проверяйте по одному модулю, чтобы увидеть, действительно ли это экономит время.

Как сократить объём локальных тестов и не стать менее безопасными?

Оставляйте unit-тесты рядом с тем кодом, который вы меняли, а более широкие тесты запускайте, когда изменение затрагивает поток данных, навигацию, хранилище или поведение на реальном устройстве. Полный набор device-тестов пусть выполняется в CI для обычной работы с ветками.

Сколько времени занимает практичный аудит сборки?

Для полезного аудита достаточно одной недели. Используйте одну и ту же машину, один и тот же сценарий и одни и те же команды, а затем меняйте только один фактор за раз и записывайте результат.

Когда имеет смысл покупать более быстрые ноутбуки?

Подождите, пока метрики перестанут улучшаться после правок модулей, процессоров и тестов. Покупайте железо только тогда, когда проект уже пересобирает нужное, а оставшаяся задержка всё ещё мешает ежедневной работе.