Мобильная разработка

Оптимизация рендеринга

Исследование Google 2016 года: 53% пользователей покидают мобильный сайт, если он загружается больше 3 секунд. Но ещё критичнее - дёргающийся скролл. A/B тест Instagram показал: улучшение плавности ленты с 45 до 60 FPS увеличило время сессии на 12%. Пользователь не думает 'приложение лагает' - он просто закрывает его.

  • **TikTok** инвестировал значительные ресурсы в оптимизацию рендеринга ленты - предзагрузка видео, adaptive bitrate, предиктивное кэширование следующих 3 видео делают скролл ленты отзывчивым даже на слабых устройствах
  • **WhatsApp** при переходе на Jetpack Compose столкнулся с регрессией производительности списка чатов - решение включало пересмотр стабильности типов и устранение нежелательных recompositions через @Stable аннотации
  • **Uber Eats** использует RecyclerView с множеством типов ячеек (блюда, разделители, рекламные баннеры, ресторанные блоки) - оптимизация DiffUtil и правильные ViewType сократили время рендеринга при обновлении меню на 40%

FPS, jank и 16-миллисекундный бюджет

Экран телефона обновляется 60 раз в секунду - это 16.67 мс на каждый кадр. Если рендеринг кадра занимает больше - пользователь видит jank: подвисание анимации, дёргающийся скролл. На устройствах с 120 Гц бюджет ещё жёстче - 8.33 мс. Главная причина пропущенных кадров - работа на main thread: сетевые запросы, парсинг JSON, синхронные операции с диском. Все они блокируют UI thread и превышают бюджет.

Android Systrace и Perfetto показывают доли кадров с превышением бюджета. Xcode Instruments -> Core Animation фиксирует dropped frames на iOS. В Compose есть showSystemUiOverlay и Layout Inspector для диагностики recompositions. Цель: 99-й перцентиль frame time должен быть ниже 16 мс.

Экран с 60 Гц обновлением показывает jank. Какой максимальный frame time допустим, чтобы jank не возникал?

Layout passes и дорогостоящие измерения

Каждый кадр Android проходит три фазы: Measure -> Layout -> Draw. Measure рекурсивно обходит дерево view и вычисляет размеры. Если родитель измеряет дочерний элемент дважды (wrap_content внутри wrap_content с весами) - это exponential рост. RelativeLayout с перекрёстными зависимостями измеряет детей дважды за проход. ConstraintLayout решает это за один проход, но только при правильно настроенных ограничениях.

В Compose нет проблемы двойного измерения: каждый Layout{} может измерить каждого ребёнка только один раз. Нарушение правила - compile-time ошибка. IntrinsicSize в Compose - исключение: он доступен явно через Modifier.width(IntrinsicSize.Min) и задокументирован как дорогая операция.

Почему ConstraintLayout предпочтительнее вложенных LinearLayout при сложных UI?

Overdraw и GPU bound рендеринг

Overdraw - ситуация когда один пиксель закрашивается несколько раз за один кадр. Белый фон Activity, поверх него - фон Fragment, поверх него - фон CardView, поверх него - фон TextView: четыре слоя краски на одном пикселе, три из которых невидимы. GPU всё равно выполняет все четыре draw call. Android Debug -> Show GPU Overdraw рисует тепловую карту: синий (1x), зелёный (2x), розовый (3x), красный (4x). Допустимый максимум - 2x overdraw, красный должен отсутствовать.

Главные источники overdraw: redundant backgrounds на каждом уровне иерархии, полупрозрачные overlays без clipRect, тени и blur в реальном времени. Canvas.clipRect() позволяет GPU пропустить невидимые области. Compose автоматически не рисует composable за пределами viewport (LazyColumn) - manual clipping не нужен.

В Debug режиме Android Show GPU Overdraw показывает большие красные области. Что это означает?

Lazy Loading и виртуализация списков

RecyclerView на Android, LazyColumn в Compose, UITableView/UICollectionView на iOS - все они реализуют виртуализацию: в DOM/иерархии одновременно существует только ~20-30 видимых ячеек, независимо от размера списка в 100 тысяч элементов. Compose LazyColumn идёт дальше: не только виртуализирует видимые элементы, но и кэширует state сохранённых позиций через rememberLazyListState(). При неправильном использовании key в LazyColumn элементы не переиспользуются, а пересоздаются.

Flashlist от Shopify для React Native работает в 10 раз быстрее стандартного FlatList: он переиспользует нативные ячейки без пересоздания JS-контекста. Принцип тот же - виртуализация, но реализован на нативном уровне без JS bridge overhead. DiffUtil в RecyclerView вычисляет минимальный набор изменений через Myers diff algorithm.

Lazy loading в списке автоматически решает все проблемы производительности - просто заменить ListView на RecyclerView или FlatList на FlashList

Виртуализация решает проблему памяти и первоначального рендеринга, но не устраняет jank при скролле - тяжёлые операции в onBindViewHolder/itemContent всё равно блокируют main thread

Загрузка изображений, парсинг данных, синхронное чтение из базы в callback привязки ячейки - всё это происходит на main thread во время скролла. Решение: предзагрузка данных в фоне, placeholder images, async image loading через Coil/Kingfisher

Что произойдёт в Compose LazyColumn, если не указать key для items?

Ключевые идеи

  • **16 мс на кадр** - жёсткий бюджет. Main thread должен быть свободен: сеть, IO, тяжёлые вычисления - всё в фоновые потоки. Корень большинства jank-проблем именно здесь
  • **Overdraw и лишние layout passes** - GPU и CPU проблемы соответственно. Убирать redundant backgrounds, использовать ConstraintLayout вместо вложенных LinearLayout, clipRect для кастомных view
  • **Виртуализация + стабильные ключи** - LazyColumn/RecyclerView/UITableView держат в памяти только видимые ячейки. Без стабильных ключей структурные изменения списка вызывают полное пересоздание

Связанные темы

Рендеринг тесно связан с управлением памятью и архитектурными решениями:

  • Memory Management — Bitmap кэширование, view recycling и утечки памяти напрямую влияют на производительность рендеринга - GC паузы вызывают jank
  • Android Architecture Components — ViewModel и LiveData/StateFlow обеспечивают правильное обновление UI без лишних рендеров при поворотах экрана

Вопросы для размышления

  • Если LazyColumn показывает 60 FPS на устройстве с 120 Гц дисплеем - пользователь всё равно видит jank. Почему, и как это диагностировать?
  • Как бы спроектировали рендеринг чата с тысячами сообщений, где каждое может содержать текст, изображение, видео или аудио-сообщение с разными размерами?
  • В чём принципиальная разница между recomposition в Compose и reconciliation в React? Как это влияет на оптимизационные техники?

Связанные уроки

  • arch-08-memory-hierarchy
Оптимизация рендеринга

0

1

Войти