Сначала диагностика

Прежде чем что-то оптимизировать — нужно понять где именно теряется время. Xcode имеет встроенные инструменты для этого.

Добавьте в Build Settings проекта:

OTHER_SWIFT_FLAGS = -Xfrontend -debug-time-function-bodies -Xfrontend -debug-time-expression-type-checking

После следующей сборки в логах появятся строки с временем компиляции каждой функции. Ищите значения больше 100ms — это кандидаты на оптимизацию.

Ещё удобнее — плагин BuildTimeAnalyzer для Xcode. Показывает топ медленных мест в виде отсортированного списка.

Проблема 1: тяжёлый вывод типов

Компилятор Swift тратит время на вывод типов. Самые проблемные места — длинные цепочки вызовов и сложные литералы:

// Медленно: компилятор выводит тип всей цепочки let result = items .filter { $0.isActive } .map { $0.name.uppercased() } .sorted() .joined(separator: ", ") // Быстро: явные типы в промежуточных переменных let active: [Item] = items.filter { $0.isActive } let names: [String] = active.map { $0.name.uppercased() } let result: String = names.sorted().joined(separator: ", ")

Проблема 2: избыточные import

Каждый import добавляет модуль в граф зависимостей. Особенно дорогой import Foundation в каждом файле — он тянет за собой сотни деклараций.

Проверьте каждый файл: действительно ли он использует всё что импортирует? В большом проекте часто находится 20-30% лишних импортов.

// Вместо этого: import Foundation // Если нужен только URL — используйте: import struct Foundation.URL

Проблема 3: монолитный таргет

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

Наша схема после рефакторинга:

Результат: После разбивки на модули инкрементальный билд при изменении одного экрана сократился с 4 минут до 40 секунд.

Про Whole Module Optimization

WMO (SWIFT_WHOLE_MODULE_OPTIMIZATION = YES) ускоряет финальный релизный билд, но замедляет debug-сборки. Не включайте его для Debug конфигурации — только для Release.

// В Build Settings: // Debug: Incremental // Release: Whole Module SWIFT_COMPILATION_MODE[config=Debug] = incremental SWIFT_COMPILATION_MODE[config=Release] = wholemodule

Кэш и DerivedData

Не удаляйте DerivedData без причины. Но если билд начал вести себя странно — удалите только папку конкретного проекта, не всю DerivedData целиком:

# Удалить кэш только вашего проекта rm -rf ~/Library/Developer/Xcode/DerivedData/MyApp-*

А что с CI?

На CI кэш DerivedData между сборками даёт огромный выигрыш. Если используете GitHub Actions — есть готовый action для кэширования. На Bitrise и CircleCI тоже есть встроенные механизмы.

Без кэша каждый билд на CI — холодный старт. С кэшем инкрементальный билд работает так же как локально.