2026 год. У вас 8–16 ядер в ноутбуке. M-чипы летают.
Swift компилируется параллельно.
Вы нажимаете ⌘B – и одно ядро уныло крутится на 100% загрузки, пока остальные отдыхают.
Добро пожаловать в мир однопоточной компиляции Asset Catalog’а.
В этой статье я покажу, почему Asset Catalog тормозит сборку и как заставить Xcode использовать больше ядер CPU. Пошагово и с примерами.
Когда ресурсы начинают тормозить проект
Пока ассетов немного – всё незаметно. Но как только проект вырастает:
сотни иконок
десятки цветов
обилие иллюстраций
внезапно ресурсы становятся бутылочным горлышком, сильно увеличивая общее время сборки.
В одном из моих прошлых проектов сборка занимала ~145 секунд. Из них около 75 секунд уходило только на компиляцию ассетов.
Половина времени. На картинки.
Это никуда не годится.
Это был момент, когда стало ясно, что проблема не в коде. Проблема в структуре сборки.
В результате оптимизации удалось задействовать все ядра CPU и получить:
холодная сборка – 55 секунд
горячий билд – 3–4 секунды на M1
После прочтения вы сможете
Понять, почему Asset Catalog компилируется в один поток
Распараллелить компиляцию через Bundle target’ы
Что не так с Asset Catalog
Xcode строит граф сборки. Каждый target – отдельная задача. Внутри одного target’а:
код компилируется параллельно
но компиляция ассетов выполняется в 1 поток
Если у вас один большой Assets.xcassets, то происходит простая вещь:
создаётся одна задача
она использует одно ядро
остальные ждут
Как следствие, весь конвйер сборки ожидает завершения долгой компиляции ассетов. Именно поэтому большой каталог начинает тормозить весь проект даже на очень мощном железе.
Ключевая идея
Разные target’ы могут компилироваться параллельно. А Bundle – это отдельный target. Следовательно:
💡 Один большой Asset Catalog → медленно
💡 Несколько Asset Catalog’ов в разных Bundle target’ах → параллельно
Вот и вся магия. Вся оптимизация строится на этом принципе.
Мы не ускоряем Asset компилятор. Мы даём ему возможность работать параллельно.
Верхнеуровнево выглядит это так:
Создаем отдельный модуль (.framework) в котором будут лежать ресурсы.
Разделяем Asset каталог на несколько
Кладем каждый Asset каталог в отдельный Bundle
Все Bundle’ы кладем в модуль из пункта 1
Те кто хорошо знаком с модуляризацией проекта, могут сразу ознакомиться с примером на гитхабе: AssetsParallelCompilation · iDmitriyy
Конфигурация сборки: до и после
Изначально у нас один большой Assets.xcassets каталог:
App └── Assets.xcassets
Один target. Одна задача. Один поток.
После у нас получится:
App └── UIResources.framework ├── ColorsBundle ├── IconsBundle ├── IllustrationsBundle └── ...
Каждый Bundle – отдельный target.
Каждый target – отдельная задача компиляции.
Задачи выполняются параллельно.
Делается всё это не самым очевидным образом, поэтому опишу процесс подробно.
Пошаговая реализация
Шаг 1. Создаём модуль с ресурсами
Создаём отдельный framework, я называю его UIResources. В нём будут расположены Asset каталоги и связанные с ними Bundle таргеты.
В XCode выбираем File → New → Target → Framework, нажимаем next, вводим имя UIResources. Так же настраиваем минимальный deployment target.
Почему framework:
Он станет контейнером для всех bundle’ов
Его можно импортировать в приложение и UI-модули
Он изолирует ресурсы от бизнес-логики
Рекомендую указывать deployment target в project file и использовать $(inherited) во всех target’ах – это позволяет менять минимальную версию iOS/MacOS/… в одном месте, а не для каждого таргета отдельно.

Шаг 2. Делим Asset Catalog
Большой Assets каталог делим на несколько. Разделяй и властвуй. Я обычно использую такую структуру:
Colors – основные цвета дизайн-системы
AltColors – цвета, которые не входят в основную палитру дизайн системы. При всяких интеграциях и экспериментальных фичах бывает нужно добавить цвета, которые мы не хотим класть в основную палитру, чтобы не превращать её в свалку.
ChromaticIcons – монохромные иконки, которые красятся в нужный цвет в коде.
ColoredIcons – цветные иконки
DesignImages – декоративные элементы дизайна, которые мы не хотим рисовать кодом. Например, какая-то нетривиальная волнистая линия или звездочка с искрами. Можно конечно нарисовать такое через BezierPath и градиенты, но зачем тратить время, если можно использовать нарисованное дизайнерами векторное изображение.
Illustrations – крупные изображения.
Это не только про ускорение. Это ещё и про порядок.
Для создания Asset каталога в Project Navigator кликаем правой кнопкой по папке UIResources, в меню выбираем New File From Template..., в появившемся окне выбираем Asset Catalog.

Разделение на Colors и AltColors позволяет по исходному коду понять, с какого рода цветом мы имеем дело, а так же проводить ревизию в дальнейшем. Это попросту упрощает поддержку.
Для всех иконок в ChromaticIcons сразу выставляем Render As: Template Image, чтобы каждый раз не писать: image.withRenderingMode(.alwaysTemplate)

Хроматические иконки по своей сути предназначены для того, чтобы красить их в нужный цвет по ситуации.
Шаг 3. Создаём Bundle target’ы
Теперь самое интересное. Нужно создать по одному Bundle target на каждый Asset Catalog.
В демонстрационном проекте я создал 4 каталога, для наглядности этого достаточно.
По умолчанию, таргет типа Bundle недоступен для iOS. Найти его можно во вкладке macOS.
Создаём нужное количество bundle’ов через File → New → Target → macOS → Bundle, например:
ColorsBundle
ChromaticIconsBundle
ColoredIconsBundle
IllustrationsBundle
Да, через macOS. Да, так можно. Далее просто поменяем Supported Platforms на iOS.
Каждый Bundle станет отдельной задачей в build graph.
После создания всем бандлам нужно поменять платформу:
Открываем список таргетов
Выбираем все созданные бандлы + project файл к которому они относятся.
Открываем вкладку build settings и находим
Supported PlatformsВ
Supported Platformsуказываем нужные платформы. В демо проекте это iOS.

Шаг 4. Настраиваем зависимости
Теперь нужно правильно связать всё между собой. Созданные ранее Asset каталоги будут находиться непосредственно в модуле UIResources, и это нужно перенастроить.
Выбираем таргет UIResouces, переходим в Build Phases, открываем раздел Copy Bundle Resources и делаем 3 вещи:
Удаляем ассеты из
Copy Bundle ResourcesДобавляем туда созданные Bundle’ы
Добавляем Bundle’ы в
Target Dependencies

Далее по очереди открываем созданные Bundle таргеты, переходим в раздел Copy Bundle Resources, и добавляем соответствующий Asset каталог.

Один бандл – 1 каталог.
PS: Модуль UIResouces, как и любой .framework, сам по себе является бандлом. Другими словами, у нас получился бандл с бандлами. Вставьте купюроприёмник в купюроприёмник. Ну вы поняли.
Если все сделано правильно, то Asset каталог будет относиться к Bundle таргету:

Проверка
Собираем проект, открываем Build Timeline и видим что Asset каталоги компилируются параллельно:

Ура. Смотреть на загрузку CPU теперь гораздо приятнее. Процессор наконец начал задействовать больше ядер для компиляции ресурсов.
Почему это работает
Xcode компилирует каждый target отдельно
Asset compiler внутри target однопоточный
Но разные target’ы могут компилироваться параллельно
Bundle – это отдельный target
Следовательно получаем параллельную компиляцию
Более того, UIResources компилируется параллельно с модулями, содержащими код.
Мы не ускорили компилятор, мы иначе организовали пайплайн сборки.
Иногда оптимизация – это не микротюнинг, а правильная декомпозиция.
Доступ к ресурсам
Теперь ассеты лежат не в main bundle. Чтобы получить к ним доступ, нужно добавить код. В модуле UIResources создадим файл со следующим кодом:
fileprivate let currentBundle = Bundle(for: BundleToken.self) fileprivate final class BundleToken {} public enum ChromaticIcons { internal static let assetBundlePath: String = currentBundle.path(forResource: "ChromaticIconsBundle", ofType: "bundle")! internal static let assetBundle: Bundle = Bundle(path: assetBundlePath)! } @available(iOS 17.0, *) extension ImageResource { public static var chromaticIcons: ChromaticIcons.Type { ChromaticIcons.self } } extension ChromaticIcons { public static let sms = ImageResource(name: "SMS", bundle: Self.assetBundle) }
Понятное дело, что писать этот страшный код руками мы не хотим.
В следующей статье поговорим о кодогенерации. Это избавит нас от рутины, устранит проблему ошибок при использовании строковых литералов и даст удобный синтаксис, работающий через автокомплит:
Image(.chromaticIcons.sms) .foregroundStyle(.orange)
Когда не стоит применять этот подход
Не стоит усложнять структуру сборки, если:
В проекте мало ассетов
Их сборка занимает менее 15 секунд
Команда не готова поддерживать более сложный build graph
Итог
Мы:
Убрали bottleneck в сборке
Задействовали больше ядер CPU
Ускорили холодную сборку проекта
Оставили задел на дальнейшее масштабирование
