Наша миссия в Duolingo — развивать лучшее образование в мире и сделать его общедоступным. Однако наши метрики показали, что миллионы учащихся, особенно использующих устройства начального уровня на развивающихся рынках, закрывали приложение из-за раздражающего времени загрузки: в начале 2024 года целых 39% пользователей устройств Android начального уровня ждали запуска приложения более 5 секунд. Это напрямую подрывало нашу миссию по обеспечению всеобщей доступности. Чтобы решить эту проблему, мы сформировали специальную команду для оптимизации производительности нашего приложения для Android.
Определение ключевых метрик для A/B-тестирования
В Duolingo у нас развита культура экспериментов и принятия решений на основе метрик. В любой задаче первым делом мы определяем правильные метрики для тестирования.
Распространённой метрикой производительности является задержка, например, время запуска приложения. Однако задержка не всегда транслируется в вовлеченность пользователей, что является нашей конечной целью. Поэтому мы решили больше сосредоточиться на метриках конверсии: проценте пользователей, завершающих критически важный этап, например, открывающих приложение и успешно переходящих на главный экран. Это позволяет более точно оценить влияние на поведение пользователей, напрямую влияя на количество активных пользователей в день (DAU).
Мы отдали приоритет конверсии в трёх критически важных этапах пути пользователя: открытию приложения, началу и завершению урока, причём открытие приложения имело приоритет, учитывая его влияние на вершину воронки продаж. Мы также установили конверсию открытия приложения в качестве общекорпоративной контрольной метрики, чтобы предотвратить снижение производительности в результате других экспериментов с продуктами.
Использование и создание правильных инструментов
Наш типичный подход к диагностике проблем с производительностью приложения состоит из четырёх этапов:
- Выявление замедлений на пути пользователя, например, в запуске приложения
- Аннотация соответствующих участков кода маркерами трассировки
- Создание системной трассировки пути пользователя
- Визуализация и анализ трассировки в Perfetto
Хотя существует множество руководств по трассировке, я хочу выделить созданный нами революционный инструмент, который преобразил наш рабочий процесс: автоматизированную трассировку методов.
Раньше для трассировки метода нам приходилось вручную добавлять точки трассировки.
Trace.beginSection("draw path node")
drawPathNode()
Trace.endSection("draw path node")
Это было утомительно и требовало постоянных пересборок приложения для добавления новых точек трассировки, причем каждая из сборок занимала до 20 минут. Наше автоматизированное решение использует манипуляцию байт-кодом (через ASM-преобразование) и фильтрацию регулярных выражений для динамической трассировки целых категорий методов (например, всех методов в ViewModel, Activity, Fragment, Repository) без изменения кода. Вы можете обратиться к этому репозиторию, чтобы ознакомиться с нашей реализацией. Это не только сэкономило время разработки, но и сделало трассировку гораздо более информативной. Сравните эти трассировки до и после:
До
После
При анализе трассировок мы в первую очередь фокусируемся на основном потоке, поскольку он напрямую отражает восприятие пользователя. Два паттерна постоянно указывают на возможности оптимизации:
- Пробелы бездействия: Участки, в которых основной поток бездействует, что часто указывает на то, что он заблокирован в ожидании завершения медленной фоновой работы (например, ввода-вывода) перед обновлением пользовательского интерфейса.
- Расширенные блоки: Длительно выполняемые фрагменты работы в основном потоке. Они являются основными причинами зависаний и даже ANR-ошибок (Application Not Responding).
Теперь, когда у нас есть необходимые инструменты, давайте рассмотрим некоторые из ключевых стратегий, которые мы использовали.
Удаление или отсрочка некритических задач
В начале нашего исследования мы провели аудит трассировки запуска приложения и обнаружили несколько задач, которые не были обязательными для первого взаимодействия пользователя. Это были лёгкие решения: удаляя или откладывая их, мы добились отличных результатов с минимальными усилиями.
Пример: отложенная загрузка рекламы
Раньше при запуске приложения мы сразу же загружали библиотеки рекламы. Трассировка выявила нечто шокирующее: одна из библиотек инициализировала WebView в главном потоке, потребляя более секунды процессорного времени!
Реклама Duolingo отображается только после завершения пользователем урока, поэтому инициализация библиотеки рекламы не привязана ко времени. Отложив её до полной загрузки приложения (когда процессор гораздо менее загружен), мы смогли сократить время запуска приложения примерно на 1.5 секунды и избавить 20,000 учащихся в день от необходимости выходить из приложения до его запуска.
Этот успех вдохновил нас на разработку HomeLoadedStartupTask — утилиты для простого запуска всей нужной для работы логики после загрузки домашней страницы. Мы сделали это рекомендуемой практикой для всей некритической логики запуска.
Уберите лишнее: запрашивайте только те данные, которые вам нужны сейчас
Ещё одна фундаментальная стратегия оптимизации связана с размером данных: вместо того, чтобы запрашивать весь набор данных заранее, запрашивайте только ту его часть, которая нужна приложению в данный момент. Минимизация объёма данных снижает нагрузку на все компоненты, сокращая время работы процессора, использование памяти и пропускную способность сети.
Пример: разбиение курса Duolingo
Мы столкнулись с этой проблемой в Duolingo в связи с постоянно растущими моделями курсов. По мере добавления контента и типов уроков размер флагманских курсов, таких как «английский-испанский», теперь составляет несколько мегабайт. Они медленно загружаются, а десериализация объекта курса может занимать несколько секунд.
Однако учащиеся взаимодействуют лишь с небольшой частью курса в любой момент времени. Поэтому мы предприняли усилия по разделению курса на более мелкие фрагменты и обновлению приложения для загрузки только необходимой части. Хотя это потребовало существенного рефакторинга как на бэкенде, так и на клиенте, результат оказался огромным: мы увидели общее повышение производительности и заметное увеличение количества активных пользователей в день (DAU). Что ещё важнее, такая фича разблокировала новые возможности, которые ранее были недоступны из-за снижения производительности.
Оптимизация сетевых запросов
В предыдущем разделе мы кратко упомянули сетевые запросы. Сетевые запросы могут быть незаметны для пользователей, но они незаметно снижают производительность, особенно для пользователей с плохим подключением к интернету. Наша команда обнаружила несколько оптимизаций, которые обеспечили значительный выигрыш в количестве активных пользователей в день.
Ускорение запросов
Мы сотрудничали с бэкенд-командами для уменьшения задержек. Это включало улучшение региональной маршрутизации для некоторых конечных точек (приближение данных к пользователям по всему миру) и оптимизацию параллелизма бэкенда для более эффективной обработки запросов.
Минимизация блокирующих запросов
Задержка пользовательского интерфейса в ожидании ответа сети значительно снижает производительность. Мы провели аудит сетевого подключения, чтобы уменьшить количество блокирующих вызовов, отдавая приоритет кэшированным данным в качестве резервных и выполняя предварительную выборку ключевых ресурсов (например, для следующего урока).
Пример: Ранее наше приложение отправляло блокирующий запрос для проверки доступности сайта при запуске. В таких регионах, как Индия, эта простая проверка занимала больше секунды. Сделав его неблокирующим, мы сократили время запуска на 15%.
Стратегическое управление неблокирующими запросами
Хотя на первый взгляд это выглядит безобидно, неблокирующие запросы могут незаметно снижать производительность, потребляя вычислительную мощность и создавая сетевые конфликты.
Пример: Во время начала урока наше приложение отправляет неблокирующие запросы для обновления таблиц лидеров и прогресса выполнения заданий. Задерживая эти некритичные запросы на 5 секунд для снижения конкуренции, мы значительно улучшили задержку начала сеанса и количество активных пользователей в день.
Добавление офлайн-поддержки
Хотя уроки уже доступны офлайн, мы добавили офлайн-поддержку для некоторых игровых функций, включая ежедневные задания и задания друзей, сохраняя прогресс на диске и синхронизируя его при восстановлении соединения.
Уделяйте внимание воспринимаемой задержке, а не только фактической
Вы когда-нибудь стояли у лифта, многократно нажимая кнопку «закрыть дверь»? На самом деле, эта кнопка часто ни к чему не подключена, но делает ожидание более приятным. Аналогично, когда наши приложения должны выполнять неизбежные блокирующие операции, смещение акцента на восприятие времени пользователем, а не на фактическое время обработки, может обеспечить исключительное ROI.
Пример: улучшение процесса завершения урока
По завершении урока мы выполняем блокирующий вызов для отправки данных сеанса и загрузки всех экранов завершения (например, рекламы, вознаграждений и т.д.). Изначально мы просто показывали полноэкранный индикатор загрузки во время ожидания. Это раздражало.
Затем наступил момент озарения: первое, что мы всегда показываем после индикатора загрузки, — это экран «Урок пройден». Что, если мы покажем его сразу, пока выполняется основная работа за кулисами?
Итак, мы реализовали именно это. Теперь, когда учащийся нажимает «Продолжить», он сразу видит фейерверк, праздничную анимацию и сообщение «Урок пройден» — и всё это время наше приложение незаметно отправляет данные и подготавливает следующие экраны.
Результаты оказались впечатляющими: более чем на 60% сократилась воспринимаемая задержка завершения уроков, значительно увеличилось количество активных пользователей (DAU) и общее количество завершенных сеансов.
До и после:
Наши достижения
В 2024 году команда провела более 200 A/B-тестов производительности на Android и добилась впечатляющих результатов:
- Конверсия открытия приложений на устройствах начального уровня выросла с 91% до 94.7%
- Задержки больше 5 секунд на устройствах начального уровня сократились с 39% до всего 8%
- Сотни тысяч пользователей в приросте ежедневных активных пользователей (DAU) были напрямую связаны с этими улучшениями производительности
Эта история успеха стала результатом симбиоза экспертных знаний: менеджеры по продукту и специалисты по данным поставили четкие, измеримые цели; инженеры взяли на себя ответственность за разработку идей и техническую реализацию; менеджеры технических программ стали связующим звеном, обеспечив согласованность действий команд и заинтересованных сторон. Объединив различные точки зрения вокруг общей цели – достижения высочайшего уровня производительности, мы преобразили опыт наших самых уязвимых пользователей.
Присоединяйтесь к обсуждению
Наш путь к оптимизации производительности Android – это непрерывный процесс. Мы поделились некоторыми стратегиями, которые оказали наибольшее влияние на Duolingo, но мы понимаем, что всегда есть чему поучиться и что открыть. Если вы сталкиваетесь с похожими проблемами производительности или у вас есть инновационные стратегии и идеи, которые мы не рассмотрели, мы будем рады узнать ваше мнение. Давайте учиться друг у друга и вместе поднимать планку качества Android-приложений!
Если вы инженер, стремящийся решать проблемы, чтобы сделать образование более доступным для миллионов (особенно на устройствах начального уровня), то у нас есть вакансии!

