Одного разу наш клієнт зіткнувся з проблемою: на 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()vsUIView.layoutIfNeeded()— розуміння різниці критичне при анімаціях -
drawRect:майже завжди замінюємо наCALayersublayers — 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 та шкала часу з ребілдами | Низька |
Етапи роботи
- Аудит — записуємо сесії в Instruments / Android Profiler, збираємо baseline-метрики FPS, janky frames, frame time.
- Аналіз — виявляємо вузькі місця: main thread блокування, overdraw, зайві layout passes.
- Правки — ітераційно, з виміром після кожної зміни.
- Регресійний прогін — перевіряємо, що правка не зламала сусідні екрани.
- Моніторинг — інтегруємо Firebase Performance або власний FPS-монітор для відстеження в продакшені.
Оцінюємо обсяг після аудиту — іноді проблема вирішується за день, іноді вимагає переписування кастомного layout. Ми маємо 8+ років досвіду в мобільній розробці та виконали понад 50 проектів з оптимізації UI. Наші клієнти економлять до 40% бюджету на доробках у порівнянні з повним переписуванням коду.
Орієнтири за термінами
Точкова правка (один екран, зрозуміла причина) — 1–3 дні. Системний аудит та оптимізація кількох екранів — 1–3 тижні. Якщо проблема в архітектурних рішеннях (неправильне використання main thread по всьому додатку) — закладайте 3–6 тижнів з поетапною міграцією.
Отримайте консультацію по вашому проекту — оцінимо обсяг робіт та запропонуємо оптимальне рішення. Зв'яжіться з нами для старту аудиту.







