Мобильная разработка
Оптимизация рендеринга
Исследование 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? Как это влияет на оптимизационные техники?