Оптимизация скорости рендеринга UI мобильного приложения

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Оптимизация скорости рендеринга UI мобильного приложения
Сложный
~3-5 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    562

Недавно наш клиент столкнулся с проблемой: на iPhone 13 приложение показывало 58–60 FPS в большинстве экранов, но один экран с кастомным UICollectionViewLayout стабильно проседал до 42–45 FPS при скролле. Мы запустили Instruments и обнаружили, что 14 мс из 16 доступных уходило на layoutAttributesForElementsInRect — метод пересчитывал все позиции ячеек без кэширования. Это классика: UI-рендеринг не тормозит «вообще», он тормозит в конкретном месте по конкретной причине. После внедрения простого кэша FPS вернулся к 60, и клиент получил плавный интерфейс за один день работы. Оптимизация скорости рендеринга UI — это задача, требующая системного подхода.

Тормоза рендеринга — одна из самых коварных проблем, потому что они видны пользователю немедленно, но диагностируются долго. FPS-метрика говорит «плохо», а где именно — нужно раскапывать. Мы работаем с данными, а не с догадками.

Почему main thread блокирует рендеринг?

Золотое правило — 16 мс на кадр (60 FPS) или 8 мс (120 Hz на Pro-устройствах). Всё, что выполняется на main thread сверх этого, блокирует рендер. Типичные виновники:

На iOS: синхронная работа с CoreData через viewContext прямо в cellForItemAt, декодирование UIImage без preparingForDisplay(), NSAttributedString с вычислением размера в sizeForItemAt без кэша.

На Android: блокирующий I/O в onBindViewHolder, Bitmap.decodeResource() на main thread, тяжёлые Drawable анимации через AnimationDrawable на бюджетных устройствах с Mali GPU.

Особняком стоит проблема с measure/layout pass. На Android Jetpack Compose ConstraintLayout внутри LazyColumn с глубокой вложенностью запускает два полных прохода measure на каждую ячейку. На сложном списке с 50+ элементами это заметно даже на Pixel 7.

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

Диагностику начинаем с профилирования на реальном устройстве. На iOS используем Xcode Instruments (шаблон Core Animation), на Android — GPU Inspector или Android Studio Profiler. Для Flutter — flutter run --profile с DevTools Performance overlay. Собираем baseline: средний FPS, количество janky frames, frame time distribution. Если средний FPS ниже 55 — ищем точки блокировки.

Почему main thread — главный враг плавности? — оптимизация скорости рендеринга

Каждая операция на main thread сверх 16 мс (или 8 мс для 120 Hz) вызывает пропуск кадра. Например, вызов sd_setImageWithURL: без флага SDWebImageAvoidAutoSetImage жрёт 8 мс на main thread — мы это видели на проекте, где FPS упал с 55 до 47. Замена на правильную опцию вернула плавность.

Что такое overdraw и как его найти?

Overdraw — когда один пиксель рисуется несколько раз за кадр. На Android включается через «Developer Options → Show GPU Overdraw»: синий — 1×, зелёный — 2×, розовый — 3×, красный — 4×+. Красный экран на бюджетном Xiaomi с Adreno 610 — гарантированный jank.

Частая причина — вложенные ViewGroup с непрозрачными фонами, где каждый слой рисует фон поверх предыдущего. На iOS аналог — CALayer с opaque = false там, где прозрачность не нужна, или shouldRasterize без явного rasterizationScale.

Оптимизация скорости рендеринга UI: пошаговый план

Что входит в работу

  • Аудит с отчётом: скриншоты профилей, список проблемных мест, baseline-метрики.
  • Правки в коде с комментариями для разработчиков.
  • Рекомендации по архитектуре и инструментам для долгосрочного поддержания FPS.
  • Мониторинг — интеграция Firebase Performance или собственного FPS-монитора.
  • Гарантия на достижение целевого FPS в течение 30 дней после сдачи.

Как мы это делаем: конкретный кейс

Типичный сценарий на iOS-проекте: клиент жалуется на «тормоза в ленте». Открываем Time Profiler, записываем скролл 5 секунд. В call tree сразу видно: [SDWebImage sd_setImageWithURL:] жрёт 8 мс на main thread потому что кто-то убрал options:SDWebImageAvoidAutoSetImage и изображения применяются синхронно после загрузки. Один флаг — и FPS вырос с 47 до 59.

На Android был кейс с RecyclerView + DiffUtil: разработчик вызывал submitList() из ViewModel, но DiffUtil работал на main thread (использовался ListAdapter без AsyncListDiffer). На списке из 200 элементов diff занимал ~18 мс. Мы перевели вычисление diff на фоновый поток через AsyncListDiffer — проблема исчезла. AsyncListDiffer в 3 раза быстрее синхронного DiffUtil на списках из 500 элементов.

Конкретные инструменты и техники

iOS:

  • CADisplayLink + кастомный FPS-монитор в debug-сборке для постоянного мониторинга
  • UIView.setNeedsLayout() vs UIView.layoutIfNeeded() — понимание разницы критично при анимациях
  • drawRect: почти всегда заменяем на CALayer sublayers — Core Animation рендерит их на GPU без участия CPU
  • UIGraphicsImageRenderer вместо устаревшего UIGraphicsBeginImageContextWithOptions для offscreen rendering
  • Prefetching через UICollectionViewDataSourcePrefetching — декодируем изображения до того, как ячейка появится на экране

Android / Compose:

  • Modifier.graphicsLayer {} для аппаратного ускорения трансформаций вместо программного
  • remember {} и derivedStateOf {} — предотвращают лишние рекомпозиции
  • key() в LazyColumn — без него Compose не может переиспользовать ноды при изменении списка
  • Bitmap.Config.RGB_565 вместо ARGB_8888 там, где альфа-канал не нужен — вдвое меньше памяти GPU

Flutter:

  • RepaintBoundary вокруг виджетов, которые часто перерисовываются независимо
  • const конструкторы — виджет не пересоздаётся при rebuild родителя
  • flutter run --profile + DevTools → Performance overlay — обязательный инструмент перед релизом

Кейс из нашей практики: 120 Hz на iPad Pro

Наш клиент сделал кастомную анимацию через UIViewPropertyAnimator с preferredFrameRateRange. Анимация работала на 60 FPS вместо 120. Оказалось — один CALayer с shouldRasterize = true без явного указания rasterizationScale = UIScreen.main.scale * 2. Core Animation ограничивал весь subtree до 60 FPS из-за несоответствия масштаба растеризации. После правки анимация заработала на 120 FPS с заметной разницей в ощущениях Core Animation Programming Guide рекомендует всегда явно задавать rasterizationScale для растрируемых слоёв.

Сравнение инструментов профилирования

Инструмент Платформа Типичное использование Сложность
Xcode Instruments (Core Animation) iOS Замер FPS, выявление overdraw и блокировок main thread Средняя
Android GPU Inspector Android Кадровая трассировка, анализ нагрузки на GPU Высокая
Android Studio Profiler (Rendering) Android Быстрый просмотр frame time и рекомендации Низкая
Flutter DevTools Performance Flutter Overlay FPS и шкала времени с ребилдами Низкая

Этапы работы

  1. Аудит — записываем сессии в Instruments / Android Profiler, собираем baseline-метрики FPS, janky frames, frame time.
  2. Анализ — выявляем узкие места: main thread блокировки, overdraw, лишние layout passes.
  3. Правки — итерационно, с замером после каждого изменения.
  4. Регрессионный прогон — проверяем, что правка не сломала соседние экраны.
  5. Мониторинг — интегрируем Firebase Performance или собственный FPS-монитор для отслеживания в продакшене.

Оцениваем объём после аудита — иногда проблема решается за день, иногда требует переписывания кастомного layout. Мы имеем 8+ лет опыта в мобильной разработке и выполнили более 50 проектов по оптимизации UI. Наши клиенты экономят до 40% бюджета на доработках по сравнению с полной перепиской кода.

Ориентиры по срокам

Точечная правка (один экран, понятная причина) — 1–3 дня. Системный аудит и оптимизация нескольких экранов — 1–3 недели. Если проблема в архитектурных решениях (неправильное использование main thread по всему приложению) — закладывайте 3–6 недель с поэтапной миграцией.

Получите консультацию по вашему проекту — оценим объём работ и предложим оптимальное решение. Свяжитесь с нами для старта аудита.

Оптимизация мобильных приложений: cold start, память, батарея, FPS, профилирование

Приложение с временем холодного старта 4+ секунды теряет пользователей ещё до первого экрана. Android Vitals в Google Play Console прямо влияют на ранжирование в поиске: приложения с плохими метриками получают меньший organic reach. Apple аналогично мониторит crash rate и время запуска через MetricKit. Оптимизация — это не «сделать быстрее», а понять где именно теряется время и что с этим делать.

Cold Start: где убивается время до первого кадра

Cold start — запуск приложения, когда процесс не существует в памяти. На Android это время от нажатия на иконку до Activity.onWindowFocusChanged(hasFocus = true). На iOS — от tap до viewDidAppear первого экрана.

Android: main thread перегружен при инициализации

Application.onCreate() — главный враг быстрого старта на Android. Разработчики инициализируют здесь всё подряд: Firebase, Analytics, базу данных, HTTP-клиент, DI-контейнер. Каждый SDK добавляет 20–200 мс на main thread.

Инструмент для диагностики: Android Studio Profiler → App Startup. Показывает граф инициализации с временем каждого компонента. Альтернатива — Tracing.beginSection("MyInitTag") в коде + systrace.

Решение: App Startup Library (Jetpack) с явным графом зависимостей инициализаторов. Компоненты, нужные только в конкретных сценариях, инициализируются лениво — by lazy {} или initializer с флагом lazyInit. Firebase Analytics, например, не нужен до первого пользовательского действия — его инициализацию можно отложить.

ContentProvider-ы, автоматически добавляемые SDK через AndroidManifest merge, тоже запускаются при старте. tools:node="remove" в манифесте позволяет отключить конкретный провайдер и инициализировать SDK вручную в нужный момент.

Ещё одна грабля: Room.databaseBuilder().build() на main thread. Это синхронная операция создания/открытия файла БД — на медленных устройствах занимает 50–300 мс. Переносим в coroutine с Dispatchers.IO, в ViewModel через viewModelScope.launch.

iOS: Dyld linking и +load

На iOS cold start делится на pre-main (до вызова main()) и post-main. Pre-main — время загрузки dylib, rebase/binding, Objective-C runtime initialization и выполнения +load методов.

Xcode Instruments → App Launch template показывает время pre-main и post-main раздельно. DYLD_PRINT_STATISTICS=1 в схеме запуска выводит детальное время загрузки в консоль.

Что убивает pre-main:

  • Много динамических библиотек (каждая dylib — накладные расходы на линковку). CocoaPods добавляет отдельную dylib на каждый pod. Решение: Swift Package Manager со статической линковкой (type: .static) или use_frameworks! :linkage => :static в CocoaPods.
  • +load методы в Objective-C — выполняются синхронно при загрузке класса, до main(). Сторонние SDK могут злоупотреблять этим. +initialize — ленивый аналог, вызывается при первом обращении к классу.

Post-main — application(_:didFinishLaunchingWithOptions:). Та же история что на Android: синхронная инициализация всего. lazy var для сервисов, которые не нужны немедленно. SwiftUI @StateObject инициализирует объект только когда View появляется — это уже встроенная ленивость.

Целевые метрики (App Store рекомендации): cold start < 400 мс для простых приложений, < 2 секунды для сложных. Warm start (процесс в памяти, но Activity/Scene пересоздаётся) — < 1 секунда.

Память: утечки, OOM, excessive pressure

Утечка памяти в iOS — retention cycle: объект A держит ссылку на B, B держит на A, ни один не освобождается. Классика: Timer с self в замыкании без [weak self]. Timer удерживает замыкание, замыкание удерживает self (ViewController), ViewController не освобождается при закрытии. Instruments → Leaks или Memory Graph Debugger в Xcode — находит живые объекты, которых не должно быть.

На Android garbage collector управляет памятью, но утечки всё равно случаются. Activity или Fragment, удерживаемые через статическую ссылку, singleton, или Handler/Runnable после onDestroy — классика. LeakCanary — обязательный инструмент в debug-сборке. Добавляется одной зависимостью debugImplementation "com.squareup.leakcanary:leakcanary-android" и автоматически детектирует утечки с полным стектрейсом.

OutOfMemoryError чаще всего происходит из-за загрузки изображений. Bitmap в памяти занимает ширина × высота × 4 байта. Изображение 4000×3000 px — 48 МБ в памяти, независимо от размера файла на диске. Glide / Coil правильно обрабатывают это: загружают с даунсемплингом под размер View, кешируют в LRU-кеш. Загружать в ImageView без Glide/Coil через BitmapFactory.decodeFile — путь к OOM на устройствах с 2 ГБ RAM.

На Flutter Dart VM имеет свой GC, но нативные ресурсы (изображения, текстуры) не управляются Dart GC. Image.network кеширует изображения в памяти без автоматического освобождения при выходе из дерева виджетов — при длинных списках с картинками используем cached_network_image с правильным memCacheWidth/memCacheHeight.

FPS и UI Performance

60 FPS — 16.67 мс на кадр. 120 FPS (ProMotion) — 8.33 мс. Всё что занимает больше на main thread — джанк.

Типичные причины просадок FPS:

На iOS: синхронная декодировка изображений в cellForRowAt. Когда ячейка таблицы появляется, UIImage(contentsOfFile:) декодирует JPEG/PNG на main thread — видно как заторможенный скролл на длинных списках. Решение: UIImage.preparingForDisplay() (iOS 15+) или ImageIO с kCGImageSourceCreateThumbnailWithTransform в background queue, результат через DispatchQueue.main.async.

На Android: RecyclerView.Adapter.onBindViewHolder с синхронными операциями. Базы данных, файловая система, синхронные сетевые запросы на main thread — StrictMode.ThreadPolicy с detectAll().penaltyLog() в debug-сборке покажет все нарушения.

На Flutter: build() метод вызывается часто, он должен быть дешёвым. setState() на верхнем виджете пересобирует всё дерево. const конструкторы, RepaintBoundary, разбиение на мелкие виджеты с локальным стейтом — основные инструменты. Flutter DevTools → Performance показывает janky frames (красные) с причинами.

Профилирование Compose: Recomposition Highlighter и трассировка через Trace.beginSection в @Composable. remember для дорогих вычислений, derivedStateOf для computed values, LazyColumn вместо Column + forEach для длинных списков.

Батарея: Wake locks, WorkManager, сетевые запросы

Приложение в топе по расходу батареи — пользователь видит это в настройках и удаляет. Android Battery Historian (из ADB bug report) показывает детальный timeline: wake locks, wakeups, network activity, sensor usage.

Основные потребители энергии:

  • Постоянный GPS (разбираем в maps-geo)
  • Polling сети каждые N секунд вместо push
  • Holding wake lock дольше необходимого
  • Excessive AlarmManager wakeups

WorkManager с Constraints — правильный способ планировать фоновые задачи: setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging. ОС батчирует задачи и выполняет в удобное время.

На iOS BGTaskScheduler с BGProcessingTaskRequest (для тяжёлых задач при зарядке) и BGAppRefreshTaskRequest (для лёгких обновлений) — система решает когда выполнять, разработчик только регистрирует и реализует логику.

Батчинг сетевых запросов: вместо 10 отдельных запросов в течение минуты — один батч запрос. Меньше радио-активностей (LTE radio потребляет много при инициализации соединения), меньше wakeups.

Инструменты профилирования

Платформа Инструмент Что показывает
iOS Xcode Instruments (Time Profiler) CPU, call stack, горячие методы
iOS Allocations Живые объекты, пики памяти
iOS Leaks Retention cycles
iOS MetricKit Производственные метрики (crash rate, hang rate, launch time)
Android Android Profiler CPU, Memory, Network, Energy
Android Systrace / Perfetto System-level трейсы
Android LeakCanary Утечки памяти
Android Battery Historian Энергопотребление
Flutter Flutter DevTools Recomposition, frame rendering, memory
Flutter Dart Observatory Dart VM profiling

MetricKit на iOS — особенно ценен: реальные данные с устройств пользователей, а не симулятора. MXMetricManager получает агрегированные метрики раз в сутки: MXAppLaunchMetric, MXHangDiagnostic, MXCPUExceptionDiagnostic. Диагностики по hang и CPU-exceptions содержат стектрейс с реального устройства — золото для диагностики production-проблем.

Процесс оптимизации

Начинаем с измерения, не с предположений. Инструменты выше дают цифры: конкретное время cold start, конкретный объём памяти, конкретные кадры с просадкой. Потом — приоритизация по impact: что больше всего влияет на пользовательский опыт именно в этом приложении.

Аудит производительности существующего приложения: 3–5 рабочих дней. Реализация оптимизаций — от недели до двух месяцев в зависимости от запущенности проблем и архитектуры кода.