4.04. Работа с размером приложений
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Почему программы стали весить больше
Системный анализ роста объёма программного обеспечения
Тезис для размышлений:
Современное программное обеспечение не растёт в объёме потому, что «добавляются новые функции» — оно растёт, потому что изменилась сама модель его создания: экономическая, технологическая и социальная.
Если провести мысленный эксперимент и вернуться в начало 2000-х, можно с изумлением зафиксировать, что тогдашний полноценный текстовый редактор, электронная таблица и система управления базами данных комплектовались в одном установочном пакете размером ~30 МБ — и работали без сетевого подключения, без регистрации, без предзагрузки модулей. Сегодня одно мобильное приложение, реализующее лишь часть функций того же редактора — скажем, только просмотр и редактирование документов — может весить 600 МБ–1,2 ГБ. При этом интерфейс не стал сложнее, вычислительная логика — не выросла, а пользовательский сценарий — не расширился в десятки раз. Более того, в некоторых случаях фактический исполняемый код, непосредственно реализующий бизнес-логику, остаётся неизменным, а его объём — измеряется единицами мегабайт.
Вопрос, который ставит в тупик: почему?
Не «как» — это можно обойти перечислением «растёт разрешение экранов», «добавляют аналитику», «используют фреймворки».
А почему эти факторы стали доминирующими? Почему они перестали быть второстепенными? Почему их рост не привёл к компенсаторной оптимизации, а, напротив, к систематическому игнорированию эффективности?
Ответ на этот вопрос — в системе, в которой этот код создаётся.
1. Что вообще «весит» в приложении?
Прежде чем говорить о причинах роста размера, необходимо понять его структуру — то, из чего формируется конечный установочный или развёртываемый артефакт.
Современное приложение — даже если внешне оно реализует простую задачу (например, «калькулятор» или «чек-лист») — состоит из множества независимых и частично пересекающихся компонентов:
1.1. Исполняемый код (binary / bytecode)
Это — непосредственно программная логика: функции, классы, алгоритмы, обработчики событий. В «идеальном мире» — это единственное, что должно весить что-то. Уже в 1990-х профессиональные программисты стремились ужать исполняемый код до минимума: например, игра DOOM (1993) содержала полностью отрендеренный 3D-мир, физику и сетевой код в ~2 МБ.
Сегодня же исполняемый код, даже на современных языках (Rust, Swift, Kotlin), при разумной архитектуре и минимизации зависимостей, может умещаться в ~15–50 МБ — для приложений средней сложности (онлайн-торговля, соцсеть, банк).
Однако в реальности он составляет, как правило, менее 10% объёма финального пакета.
1.2. Статические ресурсы (assets)
К этой категории относятся:
- изображения (иконки, иллюстрации, аватары, скриншоты, баннеры);
- аудио- и видеоклипы (анимации, обучающие ролики, звуковые эффекты);
- векторная графика (SVG, PDF, шрифты, Lottie-анимации);
- локализации (строки интерфейса для десятков языков);
- предварительно сгенерированные данные (картография, каталоги, оффлайн-кэши).
С появлением экранов высокой плотности пикселей (Retina, 4K, AMOLED с поддержкой HDR) требование к разрешению и битовой глубине изображений выросло кратно. Если в 2010 году иконка 64×64 в 8-битной PNG-палитре весила ~1 КБ, то сейчас — одна и та же иконка, представленная в 6 вариантах (1×, 2×, 3×, Light/Dark mode, для iOS/Android) — может занимать >200 КБ.
Масштабируется не только размер отдельного ассета, но и разнообразие:
- в мобильном приложении банка можно встретить >2 000 уникальных изображений, включая анимированные сценарии ошибок, загрузок, подтверждений;
- в играх — 4K-текстуры для 3D-моделей: одна карта нормалей при разрешении 8192×8192 в формате BC7 может занимать
>32 МБ.
При этом, даже если устройство использует изображения только в 2× разрешении, сборка по умолчанию часто включает все варианты — для совместимости и упрощения CI/CD-процесса.
1.3. Третьесторонние зависимости (библиотеки, SDK, фреймворки)
Здесь — ключевой сдвиг парадигмы.
В эпоху «напиши сам» (1970–1990-е), разработчик писал всё: от драйвера клавиатуры до компилятора шрифтов. В эпоху «собери из кубиков» (2010–н.в.), он включает готовые блоки:
- для работы с сетью — OkHttp, Alamofire, Axios;
- для UI — React Native, Flutter, Jetpack Compose, SwiftUI/UIKit;
- для аналитики — Firebase Analytics, AppMetrica, Yandex.Metrica;
- для авторизации — Google Sign-In, OAuth2 SDK, VK SDK;
- для уведомлений — OneSignal, Firebase Cloud Messaging;
- для кэширования — Glide, Picasso, Coil, SDWebImage;
- для криптографии — Bouncy Castle, libsodium, OpenSSL (встраиваемая версия);
- для отладки — Flipper, Sentry, Crashlytics.
Каждая такая зависимость влечёт за собой:
- статический код (нативные
.so,.dll,.dylib,.framework,.aar,.jar); - метаданные (манифесты, ProGuard-правила, ресурсы локализации);
- транзитивные зависимости (если вы добавляете
A, аAтребуетB,C,D, — они подтягиваются автоматически); - мёртвый код (функционал, который используется в библиотеке, но не вызывается в вашем приложении).
Давайте рассмотрим эти понятия.
Исполняемый код — последовательность инструкций, представленная в форме, пригодной для непосредственного выполнения целевой вычислительной системой (процессором или виртуальной машиной). Включает скомпилированный машинный код (например, .exe, .elf), байт-код JVM (.class, .jar), или интерпретируемый исходный код, загружаемый и выполняемый средой выполнения (например, Python-скрипты в контексте CLI-утилит). Да, тот самый основной код приложения, включая все его внутренние модули.
Статический код — код, не содержащий динамических конструкций, поведение которого полностью определяется на этапе компиляции и не меняется в ходе выполнения. Включает:
- код без динамической загрузки классов/модулей;
- без рефлексии (кроме статически разрешимых случаев);
- без генерации кода во время выполнения (
eval,Function(),Reflection.Emit); - без внешних модификаций (например, monkey-patching).
Используется, например, в анализе потока данных, проверке типов, оптимизации компиляторами.
Мёртвый код (dead code) — участки исходного кода, недостижимые в ходе выполнения приложения при любых допустимых входных данных. Включает:
- непосредственно недостижимые инструкции (
return; unreachable();); - методы/классы, на которые отсутствуют ссылки из «живого» кода (включая отсутствие ссылок через рефлексию, сериализацию, DI-контейнеры);
- неиспользуемые переменные, импорты, объявления.
Выявление мёртвого кода проводится статическими анализаторами (например, ReSharper, SonarQube, detekt, ESLint no-unused-*), а также инструментами tree-shaking (Webpack, Rollup) и оптимизаторами (R8, Terser) на этапе сборки.
Статические ресурсы — файлы, не подвергающиеся обработке или интерпретации непосредственно процессом выполнения, а используемые приложением в «готовом» виде: изображения (.png, .svg), стили (.css), клиентские скрипты (.js, .mjs), звук, шрифты и т.п. Характеризуются тем, что их содержимое не генерируется динамически при запросе (в отличие от динамически формируемых HTTP-ответов).
Динамический ресурс — ресурс, содержание или форма которого формируется в момент запроса (во время выполнения), а не заранее. В отличие от статических ресурсов, его формирование может зависеть от:
- контекста запроса (пользователь, роль, параметры URL);
- состояния сервера или внешних систем (БД, API);
- логики приложения (рендеринг шаблонов, генерация изображений, SSR-HTML).
Примеры: HTML, сгенерированный через Razor/Thymeleaf/Jinja; изображение, нарезанное под размер экрана; JSON, составленный на основе запроса к БД.
Ассет (asset, от англ. asset — актив) — обобщённое понятие, обозначающее любой ресурс, управляемый в рамках жизненного цикла приложения и используемый в его работе. Включает как статические ресурсы (изображения, шрифты), так и мета-компоненты (конфигурационные файлы, схемы, локализованные строки, скомпилированные шаблоны). В системах сборки (например, Gradle, Webpack, Unity) ассеты проходят этапы сборки, оптимизации, кэширования.
Мета-компоненты — компоненты, описывающие или управляющие другими компонентами, но не реализующие непосредственную предметную логику. Включают:
- схемы данных (JSON Schema, Protobuf
.proto, XSD); - декларативные конфигурации (BPMN-диаграммы, DSL-описания в ELMA365/BPMSoft);
- атрибуты/аннотации, влияющие на поведение фреймворков (например,
[BpProcess],@RestController); - манифесты, конфигурации DI-контейнеров, правила маршрутизации.
Мета-компоненты позволяют отделить декларацию от реализации и обеспечивают инверсию управления.
Третьесторонние зависимости (third-party dependencies) — компоненты (библиотеки, фреймворки, утилиты), разрабатываемые и распространяемые сторонними организациями или сообществами, а не командой проекта. Используются через системы управления зависимостями (Maven, npm, NuGet и др.) и подключаются в виде артефактов, как правило, по публичным координатам (group/artifact/version, package@version и т.п.).
Транзитивные зависимости — зависимости, подключаемые неявно, как зависимости зависимостей. При указании прямой зависимости A→B, если B декларирует зависимость от C, то C может быть автоматически включена в сборку как транзитивная зависимость A→C. Управление транзитивными зависимостями требует контроля версионности и совместимости (например, через dependency constraints в Gradle или resolutions в Yarn).
Публичные координаты — уникальный идентификатор артефакта в системе управления зависимостями, позволяющий однозначно его локализовать и версионировать. Структура зависит от экосистемы:
- Maven/Gradle:
groupId:artifactId:version(например,org.springframework:spring-core:5.3.21); - npm:
name@versionили@scope/name@version(например,lodash@4.17.21); - NuGet:
Id+Version(например,Newtonsoft.Json 13.0.3); - PyPI:
name==version(например,requests==2.31.0).
Координаты публикуются в центральных или корпоративных репозиториях (Maven Central, npmjs.com, NuGet Gallery).
Метаданные — данные, описывающие структуру, свойства, контекст или поведение других данных или программных сущностей. Примеры:
- атрибуты/аннотации в C# (
[Serializable]), Java (@Entity); package.json,pom.xml,.csproj;- описания API (OpenAPI/Swagger);
- схемы БД (DDL), XSD/DTD-описания XML;
- теги в контейнерных образах (Docker labels);
- информация о версии, лицензии, авторе.
Метаданные не участвуют в исполняемой логике напрямую, но влияют на поведение инструментов сборки, развёртывания, анализа и среды выполнения.
Атрибуты / Аннотации — языковые конструкции, добавляющие метаданные к элементам кода (классам, методам, параметрам, сборкам). Не влияют на исполняемую логику напрямую, но используются:
- компиляторами (например,
[Obsolete]); - средой выполнения через рефлексию (например,
@Autowired,[HttpGet]); - инструментами анализа и генерации кода (например,
[ProtoContract],@Entity).
В C# — attributes, в Java/Kotlin — annotations, в TypeScript/JavaScript (через декораторы с ограничениями) — decorators (стадия stage 3 proposal).
Описание API — формализованная спецификация интерфейсов взаимодействия между компонентами, в первую очередь внешних (публичных) API. Стандарты:
- OpenAPI (ранее Swagger) — для REST/HTTP;
- AsyncAPI — для событийных (message-driven) систем;
- gRPC + Protobuf — для RPC;
- GraphQL SDL — для схем GraphQL.
Содержит: пути, методы, параметры, тела запросов/ответов, статус-коды, схемы типов, примеры, аутентификацию. Используется для генерации клиентов, серверных заглушек, документации и валидации.
Теги — вспомогательные метки, применяемые для классификации, группировки или аннотирования сущностей. Контексты:
- контейнеризация:
Dockerfile→LABELили теги образов (myapp:v1.2.0); - документирование:
@param,@returnsв JSDoc/DocFX; - CI/CD: теги Git-репозиториев для релизов;
- логирование и мониторинг: теги в OpenTelemetry (например,
service.name,http.status_code); - API: теги в OpenAPI для группировки эндпоинтов (например,
tag: "users").
Несут семантическую нагрузку, но не определяют поведение напрямую.
Манифесты — структурированные файлы, содержащие декларативные метаданные, критически важные для идентификации, разрешения, сборки или запуска программных компонентов. Примеры:
AndroidManifest.xml(описывает компоненты приложения, разрешения, точки входа);MANIFEST.MFв JAR-архивах (определяет точку входа, версии зависимостей);Cargo.toml,package.json,pyproject.toml;- Kubernetes-манифесты (описание состояния желаемой конфигурации кластера);
.appxmanifestв Windows-приложениях.
Манифесты являются формализованными контрактами между компонентами и средой.
Декларативные метаданные — данные, описывающие что должно быть, а не как это достигается. Противопоставляются императивным инструкциям. Примеры:
@Controller(Spring) — объявляет класс контроллером, без указания способа регистрации;android:layout_width="match_parent"— задаёт поведение, но не алгоритм измерения;- BPMN-диаграммы — декларируют последовательность шагов, без кода их реализации;
- Kubernetes-манифесты — описывают желаемое состояние, reconciler приводит реальное состояние в соответствие.
Используются для повышения выразительности, абстракции и автоматизации.
Формализованные контракты — строго определённые соглашения между компонентами, обеспечивающие предсказуемое взаимодействие. Включают:
- интерфейсы (в языковом смысле:
interface,abstract class); - сигнатуры API (в OpenAPI — схемы и статус-коды);
- протоколы обмена (HTTP-методы + семантика, gRPC-сервисы);
- соглашения о формате данных (JSON Schema, Avro schema);
- контракты событий (Event Schema Registry в Kafka/Pulsar).
Контракты могут валидироваться статически (на этапе сборки) или динамически (runtime schema validation).
ProGuard-правила — директивы конфигурации для утилиты ProGuard (либо аналогов: R8, DexGuard), управляющие поведением обфускации, минификации и оптимизации Java/Kotlin-кода (в первую очередь для Android). Правила задают:
- какие классы/методы/поля должны быть сохранены (например, используемые через рефлексию или сериализацию:
-keep class com.example.DataModel); - какие сигнатуры не должны быть переименованы;
- какие оптимизации разрешены/запрещены.
Используются для предотвращения некорректного удаления или переименования «живого» кода.
Директивы конфигурации — управляющие инструкции, задающие поведение инструментов (компиляторов, сборщиков, анализаторов) без изменения исходной логики приложения. Примеры:
#pragma warning disable CS0618(C#);/* eslint-disable no-console */(JavaScript);-keep class ...в ProGuard/R8;optimization = "speed"вtsconfig.json.
Являются мета-уровнем управления и часто интерпретируются на этапе сборки или статического анализа.
Обфускация — преобразование исполняемого кода с целью затруднения его анализа и обратной инженерии, без изменения функциональности. Методы:
- переименование идентификаторов (классов, методов — в короткие/случайные имена);
- удаление отладочной информации (имён переменных, строк исходного кода);
- встраивание ложных ветвлений, control-flow flattening.
Применяется в мобильной (Android/R8), десктопной (Dotfuscator) и веб-разработке (частично — через минификацию и encoding строк). Может нарушать работу кода, использующего рефлексию или сериализацию, если не сопровождается исключениями.
Минификация — процесс сокращения объёма кода (размера передачи и загрузки) за счёт удаления несущественных элементов:
- пробелов, переносов, комментариев;
- сокращения имён переменных и параметров (в пределах области видимости);
- оптимизации литералов (например,
true→!0).
Отличается от обфускации: минификация не ставит целью затруднение понимания (хотя эффект может быть побочным), а ориентирована на производительность. Инструменты: Terser (JS), cssnano (CSS), ProGuard/R8 (Java/Kotlin bytecode).
Оптимизация — преобразование кода или данных с целью улучшения его характеристик (производительность, размер, энергопотребление, читаемость при определённых условиях), без изменения наблюдаемого поведения. Уровни:
- на этапе компиляции (inlining, dead code elimination, constant folding);
- на этапе сборки (tree-shaking, code splitting, DCE);
- на этапе выполнения (JIT-компиляция в JVM/V8, speculative execution);
- на уровне инфраструктуры (сжатие Gzip/Brotli, кэширование).
Оптимизация может быть функционально нейтральной (code size) или поведенчески нейтральной (time/space complexity preserved).
Ресурсы локализации — файлы, содержащие текстовые (и иногда медиа-) компоненты, адаптированные под конкретные языки, регионы или культуры. Реализуются через:
.resx(C#),strings.xml(Android),.properties(Java),.arb(Flutter),*.po/*.mo(gettext);- структурированные по коду локали (например,
ru,en-US,zh-Hans); - с поддержкой fallback-механизмов (если
en-GBотсутствует — использоватьen).
Обеспечивают интернационализацию (i18n) и локализацию (l10n) интерфейсов.
Интернационализация (internationalization, i18n) — проектирование и разработка приложения таким образом, чтобы его можно было адаптировать к различным языкам и регионам без инженерных переделок. Включает:
- вынос текстов во внешние ресурсы;
- поддержку Unicode и bidirectional text (BiDi);
- параметризованные форматы даты/времени/чисел/валют (через
Intl,java.time.format,CultureInfo); - адаптивные макеты (избегание фиксированных размеров, поддержка языков с разной длиной слов).
i18n — предварительная работа; локализация — последующая адаптация под конкретную локаль.
Локализация (localization, l10n) — процесс адаптации интернационализированного приложения под конкретную локаль (язык, регион, культуру). Включает:
- перевод текстовых ресурсов;
- адаптацию форматов (даты:
DD.MM.YYYY→MM/DD/YYYY); - корректировку изображений, цветов, иконок (культурные символы);
- соответствие юридическим требованиям (уведомления, политики).
Требует управления переводами (CAT-инструменты, платформы типа Crowdin/Lokalise), версионирования строк и контроля качества.
fallback-механизм — стратегия разрешения отсутствующих локализованных или конфигурационных ресурсов за счёт использования резервного варианта. Примеры:
- при отсутствии
ru-RU/strings.json→ использоватьru/strings.json, затемen/strings.json, затемdefault/strings.json; - в .NET:
new CultureInfo("ru-RU")→ fallback chain:ru-RU→ru→InvariantCulture; - в Android:
res/values-ru-rRU/→res/values-ru/→res/values/; - в HTTP:
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8→ сервер выбираетfr, еслиfr-CHнедоступен.
Реализуется на уровне фреймворков или явно в коде (через try…catch, ??, Optional.orElse()).
Например, добавление Firebase Analytics в Android-приложение увеличивает размер APK на ~20–35 МБ — несмотря на то, что API вызова logEvent() состоит из нескольких строк. Причина — в архитектуре SDK: оно включает полный стек обработки событий, оффлайн-буфер, синхронизацию, валидацию, сжатие, криптографию, и даже собственный HTTP-клиент — на случай, если системный сломается.
Согласно исследованиям Google, в типичном коммерческом Android-приложении (100+ зависимостей) до 60% исполняемого кода — не вызывается никогда.
Это не «лень» — это архитектурный компромисс:
- скорость разработки ↑
- кроссплатформенность ↑
- надёжность (на уровне SDK) ↑
- контроль над поведением ↓
- размер ↓ — нет
1.4. Платформенные требования (Apple, Google, Microsoft, Sony, Nintendo)
Производители платформ активно диктуют условия развёртывания приложений — и эти условия прямо влияют на объём.
| Платформа | Пример требования | Влияние на размер |
|---|---|---|
| iOS | Поддержка всех разрешений иконок (iPhone 4–16 Pro Max, iPad mini–Pro, Apple Watch) | +2–5 МБ только иконок в Asset Catalog |
| iOS | Bitcode (включён по умолчанию до iOS 14) | +15–30% к размеру бинарника (intermediate representation) |
| iOS | App Thinning / Slicing | Уменьшает установочный размер на устройстве, но архив (IPA) в App Store — хранит все варианты |
| Android | Поддержка ABI (armeabi-v7a, arm64-v8a, x86, x86_64) | Если не фильтровать, один нативный модуль ×4 версии = ×4 размер |
| Android | Множественные DPI-ресурсы (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi) | Без resConfigs в Gradle — все включаются |
| Web | Поддержка старых браузеров (IE11, Safari <14) | Babel-polyfills, regenerator-runtime, core-js: +300–800 КБ минифицированного JS |
| Windows | MSIX-упаковка с зависимостями от WinUI 3, .NET 6+ | +150–400 МБ к установочному пакету (runtime included) |
| Steam | Требование к DRM, античиту, обновляемому лаунчеру | +50–200 МБ даже для 2D-игры |
Эти требования обеспечивают стабильность, безопасность, совместимость. Но они необратимо увеличивают минимальный порог размера — даже для «Hello World».
1.5. Механизмы динамического поведения в статической упаковке
Самая тонкая причина роста — закладка вариативности.
Современное приложение — это не монолит, а множество потенциальных состояний, из которых активируется лишь часть. Например:
- A/B-тестирование фич и UI — код для 3 альтернативных экранов загружается сразу, хотя пользователь увидит только один;
- Feature flags — логика новой функции присутствует в релизе за месяц до включения, закомментированная через
if (FeatureFlags.isNewCheckoutEnabled()); - Географическая сегментация — модули для Китая (Alipay, WeChat SDK), для ЕС (GDPR-консент, DSGVO), для США (адвокаты, compliance) — включены во все сборки;
- Обратная совместимость — поддержка API v1, v2, v3 в одном клиенте, чтобы не разрывать связь со старыми серверами;
- Debug-инструменты — Flipper, Stetho, LeakCanary, даже в релизных сборках (по ошибке или «на всякий случай»).
Эти элементы не отражаются в пользовательском интерфейсе, но они исполняемы, загружаются в память, занимают место на диске — и их удаление требует ручного, осознанного вмешательства, а не автоматизированной оптимизации.
2. Экономика разработки: почему «тяжело» стало выгоднее «лёгкого»
Технические факторы (ассеты, SDK, требования платформ) — лишь инструменты. Они не определяют направление эволюции ПО. Направление задаёт экономика: отношения затрат, прибыли, рисков и сроков. Чтобы понять, почему объём приложений растёт быстрее, чем их полезность, — надо перейти от уровня кода к уровню организации труда и рыночных стимулов.
2.1. Стоимость разработчика vs стоимость железа
На заре вычислительной индустрии (1970–1990-е) лимитирующим ресурсом была память устройства. ОЗУ стоила $1 000 за мегабайт в 1980 году (в ценах того времени). Процессоры выполняли сотни тысяч операций в секунду. При этом программистов было мало, но они — единственные, кто мог реализовать функционал. В таких условиях оптимизация была экономически обязательна: ошибка в алгоритме могла сделать приложение просто невыполнимым.
Сегодня ситуация диаметрально противоположна:
- Стоимость ОЗУ в смартфоне: менее $0.01 за МБ;
- Стоимость SSD: менее $0.005 за МБ;
- Стоимость разработчика (senior, средняя по РФ):
>₽100 000 за человеко-месяц, т.е. ~₽500–700 за МБ кода/ассетов, если он лишь удаляет мёртвый код.
Рациональное решение для бизнеса:
«Потратить $2 на дополнительную память в устройстве — дешевле, чем $200 на инженерное время для сокращения приложения на 2 МБ».
Это рациональный экономический выбор в условиях избыточных ресурсов на клиентских устройствах, дефицита квалифицированных кадров, высокой конкуренции за время выхода на рынок (time-to-market).
2.2. Time-to-market и технический долг
В современной разработке доминирует модель feature-driven delivery — поставки управляются не качеством, а списком функций, которые должны появиться к дедлайну.
Технический долг (technical debt) здесь — не метафора, а бухгалтерская категория. Он позволяет:
- сократить сроки первого релиза на 30–50% за счёт использования готовых решений без адаптации;
- делегировать задачи менее опытным разработчикам, используя высокоуровневые фреймворки;
- избежать рисков, связанных с «изобретением велосипеда» (например, написанием собственного HTTP-клиента).
Но, как и финансовый долг, технический долг начисляет проценты:
- каждое новое изменение требует больше времени из-за хрупкой архитектуры;
- тестирование усложняется — больше путей выполнения, больше условий;
- размер и потребление памяти монотонно растут, даже при отсутствии новых фич.
Ключевой момент: проценты платит не бизнес, а пользователь.
Компания «60 имён» или «Озон» не несут прямых издержек от того, что приложение весит 700 МБ. Пользователь теряет время, трафик, заряд аккумулятора. Но он не может точно связать это с конкретной фичей — он лишь ощущает ухудшение UX. И если альтернативы нет — он терпит.
2.3. Модель монетизации как источник раздувания
Бесплатные приложения — не альтруизм. Это модель конверсии внимания в денежный поток. В ней объём приложения напрямую связан с количеством возможных точек монетизации.
| Компонент | Пример | Прирост веса | Экономическое обоснование |
|---|---|---|---|
| Аналитика (AppMetrica, Firebase) | Сбор событий, A/B-тесты, funnel-анализ | +20–40 МБ | Повышение конверсии на 0.5% = +₽10 млн/год при 1 млн DAU |
| Рекламные SDK (AppLovin, Yandex Mobile Ads) | Pre-roll, interstitial, rewarded video | +15–30 МБ | CPM от $1 до $15 — окупаемость даже при 0.1% кликов |
| Внутренние маркетинговые движки | Push-кампании, dynamic feature flags, personalization engine | +10–25 МБ | Удержание +2% = отсрочка оттока на 1.5 мес ≈ +₽5 млн LTV |
| Интеграции партнёров | Страховки, кредиты, cashback-сервисы | +5–15 МБ на интеграцию | Комиссия от 5% до 25% с каждой транзакции |
Суммарно: одно банковское приложение, поддерживающее 3 банковских партнёра, 2 страховщиков, 1 кредитный сервис и 1 cashback-платформу — легко накапливает +120–180 МБ «инфраструктурного жира», не связанного с основной логикой.
И здесь возникает парадокс оптимизации:
Если вы уберёте рекламный модуль, приложение станет легче и быстрее — но вы потеряете $200 000 в месяц выручки.
Если вы уберёте аналитику, вы потеряете возможность A/B-тестировать — и ваша conversion rate упадёт на 3–7%.
Если вы замените React Native на нативную реализацию — вы сократите размер на 150 МБ, но потеряете 6 месяцев разработки и единый кодбейз.
Оптимизация не является свободной. Она требует осознанного отказа от монетизационного потенциала.
2.4. Эффект супераппа и «защищённого контекста»
Apple и Google поощряют «замкнутые экосистемы» — приложения, которые максимизируют время пользователя внутри себя. Это выгодно платформам (больше in-app purchases → 30% комиссии), и это выгодно разработчикам (выше LTV, ниже CAC).
Отсюда — тренд на «супераппы»: одно приложение вместо трёх.
Пример: «СберБанк Онлайн» — это не только переводы и карты. Это:
- новостная лента (аналог Яндекс.Дзен);
- маркетплейс (аналог Ozon);
- ТВ-плеер (аналог ivi);
- мессенджер (аналог WhatsApp);
- инвестиционная платформа (аналог Tinkoff Invest);
- автоплатёж за ЖКХ (аналог Портала Госуслуг);
- кредитный калькулятор (аналог Credit.Club);
- ИИ-ассистент (аналог Алисы).
Каждая из этих подсистем — отдельный продуктовый блок, со своей командой, своим стеком, своими зависимостями. Они не интегрируются в один код, они объединяются в один установочный пакет. Возникает структурная избыточность:
- 3 разных JSON-парсера (из-за разных SDK);
- 2 реализации HTTP-клиента (OkHttp + Retrofit + Alamofire в гибридных модулях);
- дублирование иконок для одинаковых действий («оплата», «пополнение», «перевод»);
- 5 разных систем кэширования (для новостей, транзакций, акций, видео, профиля).
Это не архитектурная ошибка — это естественное следствие организационной автономии команд.
И пока «суперапп» приносит больше прибыли, чем три отдельных — он будет расти.
3. Платформенные и инфраструктурные ловушки: как «стандарты» раздувают приложения
Даже если разработчик стремится к минимализму, он сталкивается с системными требованиями, которые делают лёгкость невозможной без жертвования совместимостью или функциональностью.
3.1. Поддержка множества конфигураций
Современное мобильное приложение должно работать на:
- 2 ОС (iOS, Android) + 3–5 крупных кастомных прошивок (MIUI, EMUI, ColorOS);
- 5 архитектурах (arm64-v8a, armeabi-v7a, x86_64, x86, arm64-v8a + Apple Silicon);
- 6 DPI-уровнях (mdpi–xxxhdpi) + 2 режимах темы (Light/Dark);
- 50+ локализациях (включая rtl-языки: арабский, иврит).
Если не использовать динамическую доставку (Google Play Feature Delivery, Apple On-Demand Resources), — все эти артефакты пакуются в один APK/IPA.
Пример: приложение с 50 иконками.
- Для Android: 5 DPI × 50 = 250 файлов;
- Для iOS: 3 scale × 2 устройства (iPhone/iPad) × 50 = 300 файлов;
- Итого: 550 изображений, хотя реально используется 50.
Даже при сжатии WebP/LZ4 суммарный объём: 15–25 МБ.
При этом App Thinning и Play Asset Delivery позволяют сократить это до 2–3 МБ на устройство — но требуют:
- настройки CI/CD (Gradle flavors, Xcode schemes);
- тестирования на 10+ конфигурациях;
- мониторинга ошибок доставки.
Многие команды отказываются — проще «закинуть и забыть».
3.2. Обязательные SDK как условие существования
Некоторые зависимости — не опциональны. Они требуются для публикации или базовой функциональности:
| SDK | Почему обязателен | Размер |
|---|---|---|
| Google Play Services (Android) | Авторизация, Location, SafetyNet (anti-bot), Play Billing | 30–50 МБ (в APK) |
| Firebase Crashlytics | Требование инвесторов/страховщиков — мониторинг крашей | +5–8 МБ |
| Google Maps SDK | Отображение точек на карте (банкоматы, отделения) | +12–20 МБ |
| Яндекс.Метрика | Юридическое требование — сбор статистики для отчётов РКН | +7–10 МБ |
| VK SDK / Sber ID / Госуслуги | Единая авторизация по федеральному указу | +3–6 МБ каждая |
Это непрерывная нагрузка, не зависящая от желания разработчика. Особенно критично для государственных и финансовых приложений: там требования регуляторов — жёстче, чем здравый смысл.
3.3. «Защита от дурака» и defensive programming
Современные языки (Swift, Kotlin, TypeScript) поощряют defensive coding — проверки, валидации, fallback-пути. Это повышает надёжность, но:
- Swift: generic specialization → дублирование кода для разных типов;
- Kotlin: inline-функции с лямбдами → раздутие bytecode;
- TypeScript: sourcemaps, enum metadata, decorator polyfills → +20–40% к JS-bundle;
- Java: ProGuard/R8 не удаляют всё — особенно если есть reflection или dynamic classloading.
Пример: функция validateEmail(String email) в Java + Retrofit + Gson + OkHttp + Room:
- Исходный код: 12 строк;
- Скомпилированный байткод: ~1.2 КБ;
- С учётом зависимостей (Gson TypeAdapter, Retrofit Converter, Room Entity): +8 КБ;
- С учётом строковых ресурсов локализации (ошибки): +4 КБ;
- Итого: 13.2 КБ на 12 строк.
Это не «непрофессионализм». Это цена безопасности и сопровождаемости.
4. Почему рефакторинг не происходит (и когда он возможен)
Рефакторинг — не «чистка кода». Это проект по снижению стоимости владения. Но он требует временных и финансовых инвестиций без немедленного ROI.
4.1. Условия, при которых рефакторинг окупается
Рефакторинг целесообразен, если:
- приложение стабильно приносит прибыль
>₽5 млн/мес; - поддержка текущей версии требует
>30% инженерного времени; - ожидается рост аудитории в 2–3 раза (нагрузка на инфраструктуру растёт нелинейно);
- планируется выход на новые платформы (TV, Auto, Wear) — общий кодбейз упрощает масштабирование.
В остальных случаях — экономически выгоднее переписать с нуля, когда старая кодовая база становится «якорем».
4.2. Практические стратегии сдерживания роста
Даже в условиях feature-driven delivery возможны микроинвестиции в оптимизацию:
| Тактика | Эффект | Сложность |
|---|---|---|
| Tree shaking + code splitting | Удаление мёртвого кода на этапе сборки | Высокая (требует strict dependency graph) |
Asset deduplication (например, res-optimizer) | -5–15% размера APK/IPA | Низкая |
| Dynamic feature modules (Android), On-Demand Resources (iOS) | Загрузка модулей по запросу (кредиты, инвестиции) | Средняя |
| MicroG / open-source замены SDK | Замена Google Play Services на 3 МБ реализацию | Высокая (риск некомплаенса) |
| WebP/AVIF + crunch compression | -30–50% размера графики без потерь | Низкая |
| ProGuard/R8 aggressive shrinking | -10–25% bytecode | Средняя (требует тестирования) |
Ключевой принцип: автоматизация.
Оптимизация должна быть частью CI/CD — как unit-тесты. Если её нельзя включить в pipeline, — она не будет выполняться.
5. Роль менеджмента: как организационные решения формируют техническую реальность
Если рассматривать увеличение размера приложений исключительно через призму «некомпетентности разработчиков» или «жадности бизнеса», можно прийти к упрощённому и неверному выводу: нужно нанять лучших инженеров и поставить им «правильные KPI». На практике же архитектура ПО — это отражение организационной структуры, моделей принятия решений и системы ценностей, заложенной в компанию на уровне стратегии.
5.1. Продуктовый менеджмент как генератор технического долга
Продуктовый менеджер (PM) отвечает за ценность для пользователя. Его ключевые метрики — DAU/MAU, retention, LTV, conversion rate. Он не измеряет «размер APK» и не несёт ответственности за время компиляции. Его задача — найти гипотезу, проверить её, масштабировать успех.
Результат — feature creep: постепенное расширение функционала далеко за пределы первоначального назначения продукта.
Классический паттерн:
- Приложение решает одну задачу (калькулятор).
- Данные показывают, что 12% пользователей вводят суммы, кратные 10 000 ₽ → гипотеза: «Пользователи считают кредиты».
- Эксперимент: добавить кредитный калькулятор.
- Он даёт +0.8% retention → решение: оставить как постоянную фичу.
- Партнёрский отдел заключает договор с банком: «Если покажем ставки — они дадут нам 3 ₽ за лид».
- Интеграция → +5 МБ (новый клиент API, валидатор, UI-модалка).
- Аналитика: «15% пользователей кредитного калькулятора не завершают расчёт» → гипотеза: «Им не хватает данных о страховке».
- → интеграция со страховой компанией.
- → новая SDK.
- → ещё +3 МБ.
И так 20 итераций.
Из 50 МБ приложения:
- 5 МБ — ядро (калькуляция);
- 12 МБ — UI-фреймворк и базовые сервисы;
- 33 МБ — «ценность»: 7 интеграций, 3 A/B-эксперимента, 2 системы аналитики, 1 рекламный агрегатор.
Это не плохой PM. Это успешный PM.
Он растит метрики. Он закрывает OKR. Он получает премию.
Технические последствия его решений невидимы в его системе отчётности.
5.2. Организационная автономия vs архитектурная целостность
Крупные продукты (СберБанк, Wildberries, Ozon) разрабатываются десятью и более командами:
- Core Platform — фреймворк, авторизация, глобальный кэш;
- Loans — кредиты;
- Insurance — страховки;
- Marketplace — товары;
- Ads — реклама;
- Analytics — метрики;
- Notifications — пуш-система;
- и т.д.
Каждая команда:
- имеет свой бэклог;
- работает в своём ритме (2-недельные спринты);
- отвечает за свои KPI;
- использует свои зависимости («нам нужен Apollo Client v3.8, вы — v2.6, но мы не можем ждать»);
- хранит свои ассеты (логотип банка партнёра — в трёх местах папки
res/drawable).
Попытка централизовать:
- унификацию библиотек → требует синхронизации релизов → замедляет feature delivery;
- единый asset pipeline → требует CI/CD-революции → требует инженерного времени;
- shared code ownership → требует культуры инженерной ответственности → требует HR-политики.
Без сильного архитектурного лидера на уровне CTO — такие инициативы гасятся на этапе «это не входит в наш спринт».
5.3. Отсутствие метрик качества на уровне бизнеса
В отличие от DAU или revenue, техническое качество не имеет прямого финансового измерителя — до тех пор, пока оно не превращается в кризис.
Вот что не отслеживается в большинстве компаний:
| Показатель | Почему не измеряется | Последствия |
|---|---|---|
| APK/IPA size delta per release | «Маркетинг не видит в этом ценности» | Рост на 10–15% за квартал → через 2 года ×4 |
| Dead code ratio | Требует инструментов (R8, ProGuard stats, code coverage) | До 60% неиспользуемого кода — по данным Google |
| Asset duplication ratio | Нет автоматического аудита | +5–15% размера «бесплатно» |
| Dependency tree depth & breadth | Визуализация сложна (BOM, SBOM) | 200+ транзитивных зависимостей — норма для enterprise-app |
Технический долг — это невидимый актив, пока он не обрушивает CI, не ломает релиз или не вызывает регуляторную проверку (например, из-за устаревшей криптобиблиотеки).
5.4. «Эффективные менеджеры» и иллюзия контроля
В условиях высокой конкуренции и неопределённости менеджмент стремится к предсказуемости. Отсюда — давление на:
- стандартизацию стека («все на React Native») — даже если натив был бы эффективнее;
- повторное использование («возьмите UI-кит из библиотеки») — даже если он тащит 80% ненужного функционала;
- отчётность по задачам («сколько тикетов закрыто») — а не по результату.
Это создаёт локальную эффективность при глобальной неэффективности:
- Команда быстрее закрывает задачи → но каждая задача добавляет 2–3 МБ;
- Унификация ускоряет onboarding → но общий кодбейз раздувается;
- CI проходит за 10 минут → но половина тестов — ложноположительные, потому что mock-и перегружены.
6. Инженерная этика и ответственность: можно ли остановить «инфляцию»?
На фоне описанной системы возникает закономерный вопрос: имеет ли индивидуальный разработчик право (и возможность) сопротивляться?
Да — но только если он осознаёт свою роль не как «исполнителя задач», а как стейкхолдера архитектуры.
6.1. Право на «нет»
В профессиональной инженерной практике (например, в гражданском строительстве) существует понятие профессиональной ответственности. Инженер может и должен отказаться от выполнения работ, если они угрожают безопасности, даже под давлением заказчика.
В IT такой нормы нет.
Но её можно ввести — начиная с малого:
- «Я не добавлю этот SDK, пока мы не оценим его impact на размер и безопасность»;
- «Я не приму PR, где дублируются ассеты без оправдания»;
- «Я внесу в Definition of Done: проверка размера APK/IPA и сравнение с baseline».
Это не «саботаж». Это профессиональное суждение.
6.2. Микрооптимизации как акт сопротивления
Даже в условиях feature-driven delivery возможны действия, не требующие «разрешения»:
- Использовать
shrinkResources trueиminifyEnabled trueв Gradle — даже если в проекте «так не делают»; - Предлагать замену
Glide→Coil(на 5 МБ легче); - Переводить PNG → WebP → AVIF (экономия до 50%);
- Отказываться от
implementationв пользуapiтолько при необходимости; - Проверять, используется ли
android:allowBackup="true"— и отключать, если не требуется; - Удалять
debugImplementationиз релизных сборок.
Эти действия:
- не нарушают сроков;
- не требуют согласования с PM;
- снижают технический долг накопительно.
6.3. Архитектурная гигиена как культура
Компании, где техническое качество — часть культуры (например, Basecamp, Linear, Figma на ранних этапах), придерживаются нескольких принципов:
- Минимализм как default: «Если нельзя доказать необходимость — не добавляй».
- Оценка стоимости владения: каждая фича → TCO (Total Cost of Ownership): разработка + поддержка + инфраструктурная нагрузка + риски.
- Бюджет на «невидимое»: 10–20% спринта — на рефакторинг, документацию, тесты.
- Открытая метрика качества: размер APK, время cold start, crash-free % — на дашборде рядом с DAU.
Такие компании не «оптимизируют ради оптимизации». Они защищают margin, который даёт им гибкость:
— возможность быстро реагировать на регуляторные изменения;
— масштабироваться на новые платформы (TV, Auto) без переписывания;
— сохранять контроль над производительностью при росте аудитории.
7. Будущее: сценарии развития
7.1. Пессимистичный сценарий: «инфляция» как норма
- Средний размер мобильного приложения к 2030 году — 1.5–2 ГБ;
- Базовые модели смартфонов — от 256 ГБ флэш-памяти;
- «Лёгкие» альтернативы (Progressive Web Apps, Instant Apps) — маргинальны, так как не дают доступа к монетизации;
- Рынок делится:
- Enterprise-grade apps — тяжёлые, feature-rich, с полной аналитикой;
- Gov/edu apps — субсидируемые, оптимизированные, но устаревающие за 2 года;
- PWA-lite — для развивающихся рынков, но без push, offline, deep linking.
7.2. Оптимистичный сценарий: «возрождение эффективности»
- Регуляторы вводят требования к энергоэффективности ПО (как в ЕС для устройств);
- Потребители начинают выбирать приложения по «eco score» (размер, фоновая активность, трафик);
- Платформы (Apple/Google) вводят лимиты на размер обновления для App Store / Play Market;
- Появляются новые архитектуры:
- Edge-first apps — большая часть логики на edge-нодах, клиент — thin shell;
- Modular app stores — пользователь сам выбирает, какие модули установить («только переводы», «без рекламы», «только оффлайн-карты»);
- WebAssembly-based runtimes — один бинарник для всех платформ, без нативных зависимостей.
7.3. Реалистичный сценарий: дифференциация
Качество и эффективность станут премиальными характеристиками, как:
- шумоизоляция в автомобилях;
- энергопотребление в технике;
- состав в продуктах питания.
Люди будут платить за:
- приложения с гарантией «
<100 МБ»; - подписки «без аналитики и трекинга»;
- open-source-альтернативы с прозрачной архитектурой.
Именно здесь откроются возможности для:
- нишевых разработчиков;
- государственных инициатив (например, «Госуслуги Лайт»);
- образовательных проектов (как ваша «Вселенная IT»), объясняющих:
«ПО не должно быть тяжёлым. Оно становится тяжёлым — потому что мы позволяем этому происходить. И мы можем это изменить».