Когда пользователь видит в App Store приложение весом 200+ МБ, он десять раз подумает, прежде чем нажать «Загрузить». Особенно если сидит на мобильном интернете. Apple ещё в 2017 году подняла лимит для загрузки по сотовой сети — сначала до 150 МБ, потом до 200 МБ, в iOS 13 убрала его совсем. Но психология никуда не делась: тяжёлое приложение — это плохой первый контакт.

По данным исследования Sensor Tower, каждые лишние 10 МБ снижают конверсию в установку примерно на 1-2%. Для приложения с тысячей потенциальных установок в день это уже ощутимо.

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

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

Первый шаг — понять, что именно раздувает бинарник. Xcode умеет это показывать.

Архивируй проект (Product → Archive), затем открой Window → Organizer, выбери архив и нажми Distribute App. В процессе экспорта Xcode сгенерирует App Thinning Size Report — текстовый файл, где расписано, сколько весит приложение для каждого варианта устройства.

Второй инструмент — отчёт о размере бинарника. В Xcode открой Report Navigator (⌘+9), найди последнюю сборку и нажми на иконку документа рядом с ней. Во вкладке Build увидишь размер каждой секции: __TEXT, __DATA, __LINKEDIT и так далее. Секция __TEXT — это скомпилированный код. Если она занимает 80 МБ при приложении на 10 экранов, что-то явно не так.

Ещё один способ — команда в терминале:

size -m -l -x YourApp.app/YourApp

Покажет разбивку по сегментам и секциям Mach-O бинарника.

Ассеты — главный источник лишнего веса

По опыту, картинки и видео съедают от 40 до 70% итогового размера приложения. Здесь проще всего выиграть сразу много.

Форматы изображений. PNG — хорош для иконок и графики с прозрачностью, но для фотографий и иллюстраций это расточительство. Переходи на HEIC (Apple поддерживает его с iOS 11) или WebP (с iOS 14). Одна и та же иллюстрация в HEIC весит в 2-3 раза меньше, чем в PNG, при сопоставимом качестве.

Для статичной графики без прозрачности используй JPEG с качеством 80-85% — глаз разницу не уловит, а файл похудеет в разы.

Векторная графика вместо растра. Иконки, кнопки, декоративные элементы — всё это должно быть в SVG или PDF. В Asset Catalog нужно выставить для PDF-ассета «Preserve Vector Data» и «Single Scale». Тогда Xcode будет использовать один файл вместо трёх растровых версий (@1x, @2x, @3x).

Убери дубликаты. В проектах с историей часто оказывается, что одно и то же изображение лежит в нескольких местах под разными именами. Утилита fdupes или скрипт на Python быстро их найдёт.

Asset Catalog Compiler. Убедись, что в Build Settings включена опция ASSETCATALOG_COMPILER_OPTIMIZATION = space. По умолчанию там стоит time — оптимизация скорости компиляции, а не размера.

Видео. Если в приложении есть вводные ролики или туториалы — сжимай их через HandBrake или ffmpeg с кодеком H.265 (HEVC). Поддерживается с iPhone 7 и iOS 11. Выигрыш в размере — 40-50% по сравнению с H.264.

ffmpeg -i input.mp4 -c:v libx265 -crf 28 -c:a aac output.mp4

On-Demand Resources

Если приложение большое — игра, обучающая платформа, приложение с кучей контента — не нужно тащить всё это при первой установке.

On-Demand Resources (ODR) позволяют разбить ассеты на теги и загружать только то, что нужно прямо сейчас. Пользователь установил приложение, прошёл первый уровень — только тогда подгрузились ассеты второго.

Настройка в Xcode простая: в Asset Catalog выдели нужные ресурсы, задай тег в правой панели. В коде запрашивай их через NSBundleResourceRequest:

let request = NSBundleResourceRequest(tags: ["level_2_assets"])
request.beginAccessingResources { error in
    guard error == nil else { return }
    // ресурсы загружены, используй их
}

Apple хранит ODR-ресурсы на своих серверах. Бесплатно, без лишних телодвижений с вашей стороны.

ОДР особенно хорошо подходит для:

  • уровней в играх
  • обучающих материалов по разделам
  • языковых пакетов
  • сезонного контента

App Thinning

App Thinning — это механизм App Store, который автоматически доставляет пользователю только то, что нужно именно его устройству. Работает в трёх режимах.

Slicing. App Store создаёт отдельные варианты .ipa для каждой архитектуры и разрешения экрана. Пользователь iPhone 15 не получит ассеты для старых экранов. Ничего делать не нужно — достаточно хранить ресурсы в Asset Catalogs, а не просто в папке с файлами.

Bitcode. Компилируй приложение в промежуточный код LLVM, а не в финальный машинный код. App Store перекомпилирует его под конкретную архитектуру при доставке. Это позволяет Apple применять новые оптимизации компилятора без перезаливки приложения. Включается в Build Settings: ENABLE_BITCODE = YES. Правда, с Apple Silicon и Xcode 14 Apple фактически перестала его требовать — следи за актуальными рекомендациями в документации.

ODR — описан выше.

Важный момент: App Thinning работает только при распространении через App Store или TestFlight. Если тестируешь через Ad Hoc — получишь полный «жирный» IPA.

Настройки компилятора

Несколько опций в Build Settings существенно влияют на размер бинарника.

Уровень оптимизации. Для Release-конфигурации должно стоять SWIFT_OPTIMIZATION_LEVEL = -Osize (оптимизация под размер) вместо -O (оптимизация под скорость). Разница может быть 5-10% от размера бинарника.

Для Objective-C: GCC_OPTIMIZATION_LEVEL = s.

Dead Code Stripping. DEAD_CODE_STRIPPING = YES — убирает неиспользуемые функции и переменные из бинарника. По умолчанию включено для Release, но лучше проверить.

Link-Time Optimization (LTO). LLVM_LTO = YES_THIN включает межмодульную оптимизацию. Компилятор видит весь код целиком и может убрать то, что на уровне одного файла казалось нужным. Thin LTO быстрее в компиляции, чем полный LTO, и при этом даёт хороший результат.

Strip Debug Symbols. STRIP_INSTALLED_PRODUCT = YES и DEPLOYMENT_POSTPROCESSING = YES для Release. Отладочные символы не нужны в продакшн-бинарнике — они должны лежать в отдельном dSYM-файле.

Strip Swift Symbols. STRIPFLAGS = -x убирает локальные Swift-символы.

Аудит зависимостей

Сторонние библиотеки — тихий убийца размера приложения. Особенно когда проект живёт несколько лет и накопил зависимости по принципу «один раз добавили, забыли».

Алгоритм простой:

  1. Составь список всех зависимостей (Podfile, Package.swift, Cartfile).
  2. Для каждой зависимости ответь: используется ли она сейчас? Нельзя ли заменить нативным API?
  3. Удали неиспользуемые.
  4. Для оставшихся — проверь, можно ли подключить только нужную часть.

Пример: Alamofire занимает ~2 МБ. Если ты делаешь два-три сетевых запроса — напиши обёртку над URLSession на 50 строк. iOS 15 добавила async/await-методы прямо в URLSession, которые покрывают 90% нужд:

let (data, response) = try await URLSession.shared.data(from: url)

Аналогично с SDWebImage или Kingfisher для загрузки картинок — AsyncImage в SwiftUI и URLSession с кешированием решают задачу без внешних зависимостей.

Инструмент для анализа. Утилита cocoapods-size от Google показывает, сколько каждый под добавляет к финальному бинарнику. Это точнее, чем смотреть на размер исходников — некоторые библиотеки с маленьким кодом тащат за собой тяжёлые ресурсы.

Локализация

Если приложение поддерживает несколько языков — строки и ресурсы для каждого языка попадают в бандл. App Slicing вырезает ненужные языковые пакеты при доставке через App Store. Но есть нюанс: это работает только если ресурсы лежат в .lproj-директориях, а не собраны в один общий файл.

Проверь в Build Settings: STRIP_SWIFT_SYMBOLS не должен убивать строки локализации.

Также убедись, что Base Internationalization включена — она убирает дублирование storyboard-файлов для каждого языка.

Шрифты

Кастомные шрифты — ещё один источник лишнего веса. Одно семейство в нескольких начертаниях может весить 10-15 МБ.

Что делать:

  • Используй только те начертания, которые реально применяются в интерфейсе.
  • Рассмотри subsetting — вырезку только нужных символов из шрифта. Если приложение только для русского языка, латиница и иероглифы не нужны. Инструмент — pyftsubset из пакета fonttools:
pyftsubset font.ttf --unicodes="U+0000-007F,U+0400-04FF" --output-file=font_subset.ttf
  • iOS 13+ поддерживает системный SF Pro через UIFont.systemFont. Если дизайн допускает — не тащи сторонний шрифт.

Как проверить результат

После каждого изменения замеряй итог. Экспортируй архив с App Thinning и смотри на App Thinning Size Report. Именно там — реальный вес, который скачает пользователь конкретного устройства, а не общий размер IPA.

Для автоматизации в CI можно парсить этот файл и падать с ошибкой, если размер превысил пороговое значение. Пишется за полчаса на bash или Python.

Чеклист того, что стоит проверить:

  • Все изображения в Asset Catalogs, не в папке
  • Для PDF-иконок включён Preserve Vector Data
  • ASSETCATALOG_COMPILER_OPTIMIZATION = space
  • SWIFT_OPTIMIZATION_LEVEL = -Osize (Release)
  • DEAD_CODE_STRIPPING = YES
  • LLVM_LTO = YES_THIN
  • Debug-символы в отдельном dSYM, не в бинарнике
  • Неиспользуемые зависимости удалены
  • Тяжёлый контент вынесен в On-Demand Resources

Сколько реально можно выиграть

По опыту, пройдя этот чеклист на типичном приложении с 2-3 годами истории, можно срезать от 20 до 50% исходного размера. Приложение на 80 МБ легко становится 45-50 МБ без потери функциональности.

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

Если ты разрабатываешь мобильное приложение с нуля или хочешь модернизировать существующее — в REEXY занимаются в том числе мобильной разработкой и могут помочь с аудитом и оптимизацией. Контакты — на r3xy.ru.

Маленькое приложение — это не только про App Store. Это про уважение к пользователю: его трафику, месту на устройстве и времени ожидания загрузки.