Как Instagram Direct перестроил UI под ИИ-агентов и сократил расход токенов на 33%

TL;DR:

Команда Instagram Direct использовала переход на Jetpack Compose не просто как переписывание интерфейса, а как повод перестроить архитектуру под работу с ИИ-агентами. Для перенесённых частей Direct объём UI-кода сократился примерно на 50%, время работы агентов – на 35%, количество взаимодействий между разработчиком и агентом – на 32%, а расход токенов на сессию – на 33%. При этом миграцию проводили постепенно, с A/B-тестами и постоянным контролем производительности.

По ходу работы инженеры Meta и Google дорабатывали Compose: использовали Pausable composition вместе с LazyLayoutCacheWindow, добавили onVisibilityChanged, оптимизировали первый запуск через Baseline Profiles и постепенно заменяли RecyclerView на LazyColumn.

Главный вывод команды: для больших проектов недостаточно просто встроить Compose в старую архитектуру – больше пользы даёт перенос крупных частей интерфейса целиком и такие архитектурные ограничения, при которых и разработчику, и ИИ сложнее написать заведомо плохой код.

Instagram Direct – одна из ключевых частей Instagram, через которую ежедневно проходят миллиарды пользовательских сообщений. За годы разработки команда выжала из старой системы Android Views практически все возможные микрооптимизации. Но поддерживать и развивать настолько оптимизированный устаревший интерфейс со временем становится всё дороже: растёт технический долг, усложняется разработка. Особенно это заметно сейчас, когда команды всё активнее переходят на декларативный UI и используют ИИ-ассистентов для написания кода.

Переход Instagram Direct на Jetpack Compose оказался чем-то большим, чем обычное обновление UI-стека. Команда построила кодовую базу интерфейса, изначально рассчитанную на работу с ИИ: она стала на 50% меньше исходной реализации, при этом время работы ИИ-агента сократилось на 35%, количество взаимодействий между разработчиком и агентом – на 32%, а расход токенов – на 33%. Вместе с Google команда внедряла Jetpack Compose, не снижая требований к производительности. Оптимизации, сделанные в ходе этой работы, улучшили Compose не только для Instagram, но и для экосистемы Android-разработки в целом.

Модернизация кодовой базы в огромном масштабе

ИИ быстро стал повседневным инструментом инженеров, и даже в такой крупной кодовой базе, как Instagram, он уже даёт заметный прирост продуктивности. Но команда Instagram Direct поставила перед собой более амбициозную задачу. Вместо того чтобы просто подключить ИИ-инструменты к существующему коду, разработчики решили перестроить кодовую базу и архитектуру так, чтобы они изначально были удобны для работы ИИ. Это позволяет получить от него гораздо больше, чем при простой адаптации существующей системы.

Одним из ключевых элементов такой UI-архитектуры команда выбрала Jetpack Compose. Декларативный подход помогает писать более компактный и предсказуемый код, который ИИ-моделям проще анализировать: в нём меньше побочных эффектов и неявного состояния, а границы между компонентами выражены чётче.

Переход на Jetpack Compose требовал аккуратного планирования. Сотни миллионов людей ежедневно отправляют сообщения в Instagram, поэтому миграцию нужно было проводить постепенно и незаметно для пользователей, параллельно перестраивая базовую архитектуру. Масштаб задачи хорошо показывают цифры: отдельный UI-компонент может отображаться более чем в 160 различных комбинациях состояний, а один экран переписки поддерживает более 200 типов сообщений.

При миграции кодовой базы такого масштаба на Compose возникает соблазн пойти по простому пути и встроить Compose-компоненты в существующую иерархию View. Как промежуточный этап постепенной миграции это вполне рабочий вариант. Но в долгосрочной перспективе использование Compose внутри кодовой базы, построенной на View, создаёт проблемы. ИИ-инструменты часто выбирают путь наименьшего сопротивления. Если смешивать декларативный и императивный подходы к UI, ИИ с большой вероятностью начнёт соединять их неудачным образом, что приводит к трудноуловимым ошибкам, техническому долгу и ухудшению производительности.

Строим UI-архитектуру под ИИ

В приложении масштаба Instagram без определённого уровня архитектурных абстракций не обойтись: именно они позволяют поддерживать код по мере роста проекта. Рассмотрим распространённый подход, при котором каждый тип элемента RecyclerView наследуется от собственного базового класса RecyclerViewItem, предоставляющего привычные методы жизненного цикла, например onBind.

Пример 1

class ChatItem( val features: FeatureFlagProvider) : RecyclerViewItem { // Императивный контекст: // ИИ часто может пойти по пути наименьшего сопротивления и создать здесь // изменяемое состояние, которое живёт за пределами ChatUiState. Этот объект // сохраняется между повторными привязками и используется несколькими элементами, // что в итоге приводит к неожиданным и трудно воспроизводимым ошибкам. var isPinned: Boolean = false override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) { // Императивный контекст val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") // Декларативный контекст holder.composeView.setContent { // Смешение императивного и декларативного контекстов if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } }}

В этом фрагменте появляются сразу две проблемы. Во-первых, флаг isPinnedChatsEnabled читается в императивном коде, а затем захватывается внутри Compose-лямбды. Так возникает неочевидная связь между двумя разными подходами.

Во-вторых, isPinned хранится как изменяемое поле самого элемента, а не внутри ChatUiState. Поэтому состояние переживает повторные привязки RecyclerView и переиспользование элементов между строками, начинает «протекать» между ними и в итоге приводит к ошибкам, которые сложно воспроизвести.

Даже если немного привести код в порядок и вынести UI элемента в отдельную функцию @Composable, проблемы никуда не исчезают.

Пример 2

class ChatItem( val features: FeatureFlagProvider) : ComposeRecyclerViewItem { // Императивный контекст val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") var isPinned: Boolean = false // Декларативный контекст @Composable override fun Content(uiState: ChatUiState) { // Смешение императивного и декларативного контекстов if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... }}

Пример намеренно упрощён, но он показывает более общую проблему: чем меньше ограничений задано для ИИ, тем ниже со временем становится качество генерируемого им кода. Ограничения и дополнительные инструкции помогают, но сами по себе проблему не решают. Столкнувшись с препятствием, ИИ нередко пытается его обойти, лишь бы продолжить выполнение задачи.

Чтобы кодовая база была удобна для работы ИИ, команда сформулировала два практических правила:

  • Свести к минимуму зависимость от специфического контекста. Чем больше внутренних особенностей конкретной кодовой базы должен знать ИИ-агент, чтобы внести корректное изменение, тем ниже качество результата. Чем ближе код следует распространённым практикам, тем лучше с ним работает ИИ.

  • Кодовая база, рассчитанная на ИИ, должна сама обеспечивать соблюдение своих границ. Закрывать архитектурные пробелы с помощью дополнительных инструкций для ИИ плохо масштабируется: каждая такая инструкция занимает место в контексте и может снижать эффективность агента. Поэтому эту задачу должна брать на себя сама архитектура. ИИ-агенты естественным образом выбирают путь наименьшего сопротивления, а значит, архитектура должна делать именно этот путь правильным: приводить к качественному коду, а неудачные архитектурные решения, наоборот, делать сложными и неудобными для реализации.

Элемент списка по-прежнему можно представить отдельной абстракцией, но теперь весь Compose-код находится в конструкторе. Поэтому у него нет доступа к полям и состоянию класса, а все аргументы он получает только через конструктор. По сути, такой компонент эквивалентен обычной функции @Composable, но при этом вписывается в существующую архитектуру.

Пример 3

class ChatItem( val features: FeatureFlagProvider, val onPin: (Boolean) -> Unit,) : ComposeItem( // Compose UI content = { uiState: ChatUiState -> val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") if (isPinnedChatsEnabled) { Button(onClick = { onPin(!uiState.isPinned) }) { Text(if (uiState.isPinned) "Unpin" else "Pin") } } ... },)

Миграция кодовой базы такого масштаба – огромная задача. На протяжении долгого времени сотни UI-компонентов, из которых состоит основная часть интерфейса Direct, должны были существовать параллельно со своими старыми версиями, причём обе реализации приходилось одновременно поддерживать. ИИ помог ускорить написание большого объёма кода и сделал такую параллельную миграцию возможной. Благодаря этому команда Direct смогла провести переход в сжатые сроки, не мешая остальным разработчикам выпускать новые функции для миллионов пользователей.

Несколько инженеров одновременно запускали собственных ИИ-агентов, используя общую базу знаний с переиспользуемыми инструкциями и соглашениями, накопленными в ходе миграции. Так рабочие процессы и лучшие практики оставались едиными для всей команды, а каждому разработчику не приходилось заново находить одни и те же решения. Для каждого интерфейса миграция проходила в два этапа:

  • Написать весь Compose-код с помощью ИИ.

  • Доработать его: разобрать пограничные случаи, устранить проблемы с производительностью и довести интерфейс до состояния, в котором его можно запустить для реальных пользователей в рамках публичного тестирования.

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

Результаты подтвердили выбранный подход. На перенесённых интерфейсах Instagram Direct Jetpack Compose позволил команде сократить общий объём UI-кода на 50%. Чем меньше кода нужно генерировать ИИ, тем выше качество результата и тем меньше расход токенов на задачу.

Внутренний анализ Android-кодовой базы Instagram Direct сравнил сессии ИИ-агентов при работе с интерфейсами на Compose и при выполнении аналогичных задач с Android Views. Рост эффективности был заметен по двум показателям:

  • На один символ кода, попавшего в основную кодовую базу: при работе с Compose потребовалось на 32% меньше взаимодействий между инженером и агентом и на 35% меньше времени работы агента. Под временем работы здесь понимается интервал от момента, когда агент получает запрос инженера, до момента, когда возвращает ответ.

  • На одну сессию агента: при использовании Compose общий расход токенов снизился на 33% по сравнению с Views.

Авторы приводят отдельно эффективность результата и показатели типичной сессии, поскольку они отражают разные аспекты работы. Количество взаимодействий между инженером и агентом и время выполнения показывают затраты ресурсов на единицу кода, который в итоге попал в кодовую базу. Расход токенов, в свою очередь, отражает общую стоимость типичной сессии агента.

Данные также показали устойчивую разницу между двумя UI-фреймворками при работе со сложным или хрупким кодом. В Meta для этого используют показатель риска изменений, который учитывает общее качество кода и вероятность того, что изменение приведёт к инциденту в продакшене. Эффективность использования ресурсов ИИ-агентом оценивали по совокупности трёх показателей: расходу токенов, времени работы агента и количеству взаимодействий между инженером и агентом. Чем выше накопленный показатель риска у файла, тем менее эффективно агент использует ресурсы.

Когда накопленный показатель риска файла увеличивается вдвое, для UI на Android Views эффективность использования ресурсов агентом снижается на 30% в пересчёте на один символ кода, попавшего в основную кодовую базу. Для UI на Jetpack Compose снижение в тех же условиях составляет всего 9%.

Совместная работа Google и Meta позволила команде Instagram Direct посмотреть на внедрение Compose с другой стороны: не просто как на переписывание UI, а с точки зрения того, насколько кодовая база готова к работе с ИИ. Этот опыт показал, что Compose может служить хорошей основой для кодовых баз и архитектур, изначально рассчитанных на использование ИИ, особенно в приложениях масштаба Instagram.

Оптимизация производительности

Instagram Direct – одна из ключевых частей приложения, и пользователи ожидают, что сообщения всегда будут открываться и работать быстро. Переход на Jetpack Compose фактически означал масштабное переписывание интерфейса, поэтому главной задачей было сохранить прежний уровень качества без регрессий.

За годы разработки команда довела старую реализацию на Views до очень высокой производительности. При переходе на совершенно другой UI-фреймворк нужно было удержать ту же планку.

Instagram отслеживает сотни, если не тысячи, метрик производительности. При переходе на Compose особенно важными были три:

  • Время до готовности к взаимодействию – сколько проходит от открытия экрана до момента, когда пользователь уже может с ним работать.

  • Время полной загрузки – сколько проходит от открытия экрана до полной загрузки всего содержимого, например изображений.

  • Плавность прокрутки – насколько плавно прокручивается экран и возникают ли пропуски кадров.

Эти метрики собираются непосредственно в продакшене, поэтому команда могла проводить A/B-тесты: сравнивать перенесённый на Compose интерфейс со старой реализацией и оценивать влияние миграции на производительность.

При подобных миграциях обычно начинают с малого: переносят несколько UI-компонентов, собирают данные и смотрят, как они ведут себя. Это полезно, но первые результаты показывают лишь часть картины и могут создавать ложное впечатление, что Compose проигрывает по производительности. Причин две:

  • Нерепрезентативность – один перенесённый UI-компонент может дать полезные данные о производительности на конкретном экране. Но разные компоненты ведут себя по-разному, и причины этих различий не всегда применимы к остальному интерфейсу. Поэтому экстраполировать такой результат на всю миграцию нельзя.

  • Накладные расходы на взаимодействие двух систем – небольшой фрагмент Compose внутри большой кодовой базы на Views платит дополнительную и не всегда предсказуемую цену за взаимодействие между двумя UI-системами. Эти расходы искажают измерения, поэтому результаты небольших миграций не показывают, как будет работать полностью перенесённый интерфейс.

Поэтому небольшие миграции полезны, но не всегда отражают реальный эффект от Compose. Чем большую часть интерфейса удаётся перенести целиком, без постоянных переходов между Compose и Views, тем точнее становится картина производительности – и, как правило, тем лучше выглядят результаты.

Основные экраны Instagram Direct построены вокруг длинных списков с множеством типов элементов. Изначально для них использовался RecyclerView. Архитектура опирается на собственные абстракции, которые позволяют масштабировать код, но при этом остаётся привязана к жизненному циклу системы Views.

Основная часть работы заключалась в постепенном переносе нескольких сотен отдельных элементов списков на Compose внутри существующей архитектуры на RecyclerView. Их небольшими независимыми группами выкатывали в продакшен под A/B-тестами, причём для пользователей работа с сообщениями внешне не менялась.

Главный недостаток такой схемы – даже после переноса всех элементов списка на Compose архитектура по-прежнему сильно зависит от старой системы Views из-за самого RecyclerView. Поэтому следующим логичным шагом стала замена архитектуры на основе RecyclerView на нативный для Compose вариант – LazyColumn.

То есть Compose-компоненты нужно отвязать от контейнера, в котором они используются, сохранив совместимость и с RecyclerView, и с LazyColumn. Не менее важно уметь переключаться между ними во время выполнения с помощью флагов функций, чтобы проводить A/B-тесты.

Новые элементы на Compose изначально совместимы с LazyColumn и могут встраиваться в единое дерево композиции. При этом команда создала API совместимости, который позволяет использовать те же элементы и внутри RecyclerView. Благодаря этому LazyColumn можно было раскатывать в рамках A/B-теста параллельно с RecyclerView, переиспользуя одни и те же Compose-компоненты и постепенно доводя производительность, не мешая остальной команде разрабатывать и улучшать функциональность.

Масштаб и сложность Instagram, а также высокая чувствительность приложения даже к небольшим регрессиям стали серьёзным испытанием для Jetpack Compose. Решать возникающие проблемы пришлось итеративно и в тесном взаимодействии. Инженеры Google и Meta совместно анализировали метрики, находили узкие места и проектировали новые возможности Compose, чтобы выйти на показатели реализации на Views или превзойти их. Среди результатов этой работы особенно выделяются приостанавливаемая композиция вместе с LazyLayoutCacheWindow и отслеживание видимости элементов.

Приостанавливаемая композиция с LazyLayoutCacheWindow

Приостанавливаемая композиция, включённая по умолчанию в Compose 1.10, позволяет распределять композицию тяжёлых элементов ленивых списков между несколькими кадрами и тем самым уменьшать подтормаживания. В сочетании с LazyLayoutCacheWindow, появившимся в Compose 1.9, это заметно повышает плавность прокрутки.

В недавних внутренних тестах Meta сочетание приостанавливаемой композиции с LazyLayoutCacheWindow размером в одну область просмотра снизило количество крупных просадок кадров в минуту (LFDs/m) примерно на 13% по сравнению с базовой конфигурацией Compose. Один только Cache Window дал снижение примерно на 8% относительно той же базовой конфигурации. LFDs/m – внутренняя метрика Meta, с помощью которой команда отслеживает заметные рывки при прокрутке.

LazyLayoutCacheWindow заранее подготавливает и сохраняет элементы за пределами видимой области в заданном диапазоне пикселей. Это помогает при быстрой инерционной прокрутке. Чтобы использовать LazyLayoutCacheWindow в приложении, можно взять актуальную Compose 1.13.0-alpha03 и настроить окно кэша так:

val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp)// ORval cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f)LazyColumn(state = state, cacheWindow = cacheWindow) { ...}

Окно кэша можно настроить двумя способами. В обоих случаях задаётся одно и то же: какой объём содержимого за пределами экрана нужно оставлять скомпонованным, но единицы измерения различаются.

  • Dp – фиксированная абсолютная длина. ahead = 150.dp сохраняет скомпонованными 150 dp содержимого за видимой границей независимо от устройства.

  • Float – доля области просмотра. aheadFraction = 0.5f оставляет скомпонованным содержимое на половину экрана вперёд. Абсолютный размер окна при этом зависит от высоты экрана и адаптируется к разным форм-факторам: на планшете или разложенном складном устройстве он будет больше, на компактном смартфоне – меньше.

Команда Instagram отдельно подобрала значения долей окна кэша под структуру контента Direct и размеры его элементов. Универсальных значений здесь нет: оптимальные параметры зависят от конкретного интерфейса, поэтому подходящий баланс приходится искать экспериментально.

Логирование показов с помощью onVisibilityChanged

API onVisibilityChanged, добавленный в Compose 1.9.0, стал ещё одним важным результатом технического сотрудничества Google и Meta. Он даёт крупным интерфейсам на Jetpack Compose единый способ определять, действительно ли composable-компонент виден на экране, и заменяет собственные реализации, которые раньше приходилось писать вручную.

Только в Instagram Direct такие сигналы видимости используются в сотнях файлов. На них завязаны метрики качества продукта, которым важно знать, был ли конкретный элемент интерфейса действительно показан пользователю.

Производительность при запуске

Переход Instagram Direct на Jetpack Compose неожиданно улучшил производительность и в других частях приложения. У рантайма Jetpack Compose есть затраты на первоначальный прогрев, но оплачиваются они только один раз. Поскольку Direct – одна из самых посещаемых частей приложения и пользователи часто открывают сообщения в начале сессии, другие экраны Instagram на Compose тоже стали работать быстрее.

Производительность самого Compose-интерфейса Instagram Direct при первом запуске оптимизировали с помощью Baseline Profiles. Они позволяют заранее компилировать часто выполняемые участки кода ещё при установке приложения, чтобы Compose быстро отрисовывал интерфейс уже при первом запуске.

Что команда вынесла из миграции Instagram Direct на Jetpack Compose

  • Jetpack Compose даёт пользу сразу. Чтобы получить выигрыш от Compose, не обязательно использовать продвинутые сценарии работы с ИИ. Сокращение объёма кода примерно на 50% означает, что поддерживать приходится меньше кода, а значит, уменьшается и площадь для потенциальных ошибок.

  • Архитектура, изначально рассчитанная на работу с ИИ, дала заметный эффект: время работы ИИ-агентов сократилось на 35%, количество взаимодействий между инженером и агентом – на 32%, а расход токенов – на 33%.

  • Несмотря на наличие множества API совместимости между Views и Compose, лучше переносить крупные части интерфейса целиком, а не отдельные небольшие компоненты. Так UI остаётся внутри единого непрерывного дерева композиции и получает доступ ко всем оптимизациям производительности, которые Compose может дать в нативном для себя сценарии.

  • Используйте Pausable composition вместе с LazyLayoutCacheWindow. Такое сочетание даёт лучший результат, чем одно окно кэша. Если использовать только Cache Window, тяжёлый элемент всё равно может попытаться скомпоноваться за один проход и выйти за бюджет кадра.

  • Участвуйте в развитии самого Compose. Meta сотрудничала с командой Jetpack Compose, чтобы перенести свои идеи и обратную связь непосредственно в Compose. Поскольку это инструмент с открытым исходным кодом, исправления ошибок и централизованные оптимизации производительности в итоге полезны всем. Поэтому не стесняйтесь оставлять обратную связь.

Переход на Jetpack Compose заметно улучшил сценарии разработки с помощью ИИ и одновременно упростил повседневную работу с UI в Instagram. Декларативный подход уменьшает объём шаблонного кода, упрощает работу с состоянием и в целом повышает продуктивность разработчиков.

Команда Instagram планирует постепенно переносить на Compose и другие части приложения. Google и Meta также продолжат совместную работу над улучшениями, которые будут полезны как Instagram, так и другим пользователям Jetpack Compose.

Если вы ещё не пробовали Compose, то с появлением ИИ-инструментов переход на Jetpack Compose стал проще, чем раньше.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев