Главная
АИ #33 (319)
Статьи журнала АИ #33 (319)
Оптимизация производительности мобильной игры: диагностика узких мест и точечные...

10.51635/AI-33-319_z3wMY

Оптимизация производительности мобильной игры: диагностика узких мест и точечные улучшения вместо портирования

14 августа 2026

Цитирование

Кананович М.. Оптимизация производительности мобильной игры: диагностика узких мест и точечные улучшения вместо портирования // Актуальные исследования. 2026. №33 (319). URL: https://apni.ru/article/15913-portirovanie-refaktoring-ili-optimizaciya-kak-my-vytaskivali-socialnuyu-igru-s-polumillionnoj-auditoriej-iz-yamy-proizvoditelnosti

Аннотация статьи

Когда мобильная игра за полгода набирает сотни тысяч новых пользователей, производительность часто падает быстрее, чем команда успевает реагировать. Именно так случилось в одном проекте, где мы участвовали в роли внешних консультантов: месячная аудитория достигла 800 тысяч активных игроков, и интерфейс начал «тормозить» на каждом переходе, особенно на бюджетных устройствах. Внутренняя команда уже готовила двухлетний план полного портирования клиента на Unity и переписывания бэкенда, но мы настояли на предварительной диагностике. Около месяца профилирования на реальных смартфонах и анализа клиентской телеметрии показали неожиданное: основная проблема была не в рендеринге, а в синхронных последовательных сетевых запросах в UI-потоке и бесконтрольном создании объектов в циклах обновления. Заменив синхронные вызовы асинхронными с корректным управлением жизненным циклом, внедрив пулы для часто создаваемых UI-элементов и оптимизировав пару «горячих» алгоритмов, мы за шесть месяцев убрали девять из десяти заметных глазу задержек, при этом обошлись без смены платформы. Стоимость работ оказалась в десять раз ниже запланированного портирования, а удержание на второй день (D2) выросло на 7 процентных пунктов – что для данного сервиса означало дополнительные миллионы в годовом исчислении. Ключевой вывод: не стоит заказывать «революцию», пока не измерили, где именно теряется время. Часто точечные правки дают больше, чем полная перестройка, и решение должно приниматься на основе цифр, а не страхов перед легаси-кодом.

Текст статьи

Введение

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

В этой статье я хочу рассказать о реальном проекте, где мы столкнулись именно с такой ситуацией. Не буду называть компанию и конкретные цифры бюджета, но суть передам достаточно точно. Игра – социальное казино, условно говоря, набор слотов и мини-игр с элементами прогрессии. К моменту нашего приглашения у неё было около 800 тысяч активных пользователей в месяц, и это число быстро росло. А вместе с ним росли и жалобы: «зависает», «долго грузится», «вылетает».

Внутренняя команда, которая занималась проектом несколько лет, уже устала бороться с симптомами. Их предложение было радикальным: давайте портируем клиент на Unity, а бэкенд перепишем на более современном стеке (например, Go или Java), чтобы избавиться от «узких мест» раз и навсегда. Они оценили этот процесс в два года и несколько миллионов долларов. Издатель был готов одобрить бюджет, но кто-то из руководства предложил сначала позвать сторонних экспертов – нас.

Когда я в первый раз услышал про двухлетний план, у меня внутри всё сжалось. Я достаточно видел таких проектов, чтобы знать: два года – это почти вечность для мобильного рынка.

Что мы измеряли и как

Первый месяц ушёл исключительно на сбор данных. Мы развернули профилирование на всех уровнях. На клиенте – Android Studio Profiler, Xcode Instruments, плюс самописные трекеры времени для критических операций. На сервере – логирование каждого эндпоинта с миллисекундами, включая время обработки в PHP, время выполнения запросов к MySQL, а также замеры сетевой задержки на стороне клиента (мы добавили телеметрию, которая фиксировала полное время отклика от отправки запроса до получения ответа, чтобы видеть реальное RTT). И конечно, мы не ограничились флагманскими смартфонами – накупили кучу бюджетных устройств разных лет, чтобы увидеть реальную картину.

Знаете, что оказалось самым неожиданным? Рендеринг, который все считали главным виновником, не был критическим. Draw calls – в пределах нормы, текстуры сжаты в ASTC (для старых устройств использовались fallback-форматы ETC2), шейдеры не перегружены. Мы потратили несколько дней, перепроверяя показатели, но факт оставался фактом: GPU справлялся отлично, если не считать косвенных проблем, связанных с созданием большого количества текстурных объектов в памяти.

А вот что действительно убивало производительность, так это две вещи.

Первая – сеть. При каждом переходе на новый экран клиент отправлял от пяти до десяти последовательных синхронных HTTP-запросов к серверу, и все они выполнялись в главном потоке. Запросы были зависимы: результат одного требовался для формирования следующего, поэтому они не могли идти параллельно. На хорошем Wi-Fi это ещё сходило с рук, но при мобильном интернете или загруженном сервере каждый такой запрос мог зависнуть на 200–300 мс. В сумме (поскольку они шли цепочкой) это давало две-три секунды ожидания, в течение которых интерфейс просто не реагировал на касания. А игрок думал, что приложение вылетело.

Вторая – управление памятью. Разработчики, которые писали UI, не заморачивались с переиспользованием объектов. Каждое обновление экрана (а они случались часто) порождало сотни новых UI-элементов: текстовые метки, кнопки, контейнеры, а также текстурные объекты (например, спрайты для анимаций и иконок). Эти объекты создавались динамически и не переиспользовались, что приводило к частым аллокациям и, в конечном счёте, к паузам при сборке мусора. В движке использовалась собственная система управления временем жизни объектов с периодической пакетной очисткой (аналог сборщика мусора, написанный на C++ поверх NDK). При большом количестве временных объектов очистка вызывала остановки всех потоков на 50–100 мс. Эти паузы накладывались на сетевые задержки, и на слабых устройствах получалась просто катастрофа.

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

Вот такая была картина. Никаких фатальных архитектурных ошибок вроде невозможности масштабирования, просто накопившиеся со временем локальные недочёты, которые в сумме давали эффект тормозящей игры.

Почему мы не стали портировать

После того как диагноз был поставлен, мы сели и честно сравнили три варианта.

Портирование – оно, конечно, звучит красиво. «Мы всё перепишем на современном стеке, привлечём новых разработчиков, избавимся от легаси». Но цена этого – два года, в течение которых команда почти не будет выпускать новые фичи. А в социальных играх, где каждую неделю нужны ивенты и акции, остановка на два года – это практически самоубийство. Плюс риски: новая платформа – новые баги, которые могут быть даже хуже старых. К тому же портирование бэкенда на другой язык не решало проблемы синхронных клиентских запросов и управления памятью – это требовало бы переписывания и клиента, и сервера, что лишь удваивало объём работ.

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

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

Чтобы убедиться, что это сработает, мы провели быстрый эксперимент. Взяли один из самых проблемных сценариев – переход из лобби в игру-слот – и за две недели набросали прототип с фиксами: асинхронные запросы, кэширование статичных данных и пул для кнопок и меток. Замеры показали, что время отклика (p95) сокращается с 2,5 секунд до 0,4 секунды. Это был решающий аргумент. Мы пошли к издателю и сказали: дайте нам полгода и в десять раз меньше денег, и мы сделаем продукт, который будет летать. Они согласились.

Что мы сделали за полгода

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

  1. Сетевой слой (самый большой прирост). Перевели все вызовы на асинхронный режим с использованием колбэков, привязанных к жизненному циклу экрана (с weak-ссылками и токенами отмены). Внедрили локальный кэш для часто запрашиваемых данных: текущий баланс игрока, список бонусов, информацию об акциях. Для инвалидации кэша мы использовали версионность: сервер возвращает хэш или версию данных, и клиент обновляет кэш только при изменении версии; кроме того, критические операции (списание баланса) всегда выполняются по транзакционному API, возвращающему актуальное состояние. Также мы объединили мелкие запросы в один пакетный – число HTTP-вызовов на один переход сократилось с 5–10 до 1–2, а за счёт использования keep-alive и пула соединений мы снизили накладные расходы на TLS-рукопожатия. Этот этап дал основной прирост: среднее время отклика (p50) упало на 60%, а p95 – почти на 70%.
  2. Управление памятью и UI-объектами. Здесь мы перелопатили весь код, отвечающий за создание динамических элементов. Вместо того чтобы каждый раз создавать новый объект, мы стали брать его из заранее подготовленного пула. Особенно это касалось текстовых меток, кнопок, контейнеров и мелких спрайтов, которые меняются часто. Тяжёлые текстурные ассеты мы не пулили, а кэшировали по идентификатору ресурса с подсчётом ссылок. Параллельно нашли несколько утечек: слушатели событий не отписывались, и объекты висели в памяти, не давая очищать пулы. После исправления паузы при пакетной очистке сократились с 50–100 мс до менее чем 10 мс, а пиковое потребление RAM на бюджетных устройствах снизилось на 25%.
  3. Алгоритмические оптимизации. Самый больной участок – расчёт выигрышных комбинаций на игровом поле. Мы заменили линейный поиск по массиву линий на хеш-таблицу, где ключом была комбинация символов, а значением – результат. Кроме того, убрали вложенные циклы там, где можно было обойтись однопроходным обходом с накоплением. Время обработки одного спина на сложных конфигурациях сократилось с 50 до 5 мс. Это не только ускорило сам расчёт, но и снизило общую нагрузку на CPU, что позволило устройству реже включать троттлинг.
  4. Тестирование и развертывание. Мы не выкатили все изменения разом, а делали это постепенно через систему feature-флагов и staged rollout. Сначала на 10% пользователей, потом на 30, потом на 50. На каждом шаге собирали статистику по задержкам, вылетам и удержанию. Если видели негативную динамику – отключали флаг (что равносильно откату) и разбирались. Так мы поймали пару неожиданных эффектов: например, на некоторых старых смартфонах пул объектов давал ещё большую экономию памяти, чем мы предполагали, а на новых, наоборот, требовал донастройки размера пула. Полное включение на 100% аудитории произошло через пять с половиной месяцев после старта, а последние две недели ушли на финальную стабилизацию и мониторинг.

В итоге через шесть месяцев мы получили игру, в которой 90% видимых задержек (под «видимыми» мы понимали сценарии, где время отклика превышало 300 мс) просто исчезли. Частота кадров на целевых бюджетных устройствах стабилизировалась на уровне 30 fps (и перестала опускаться ниже), а количество вылетов снизилось на 40%. Но самое главное – удержание на второй день (D2) выросло на 7 процентных пунктов, например с 25% до 32% (в статье мы не раскрываем абсолютные цифры, но рост был статистически значимым в А/В-тесте с контрольной группой). В деньгах это означало порядка 7–8 миллионов долларов дополнительной выручки в год (расчёт основан на исторических данных о конверсии, ARPDAU и среднем сроке жизни игрока при данном уровне удержания).

Для наглядности приведу таблицу ключевых метрик «до» и «после» на целевом бюджетном устройстве (Android, 2 ГБ ОЗУ, мобильная сеть 4G):

Таблица

Метрика

До

После

Условия измерения

Время открытия экрана Lobby → Slot, p50

2,1 с

0,4 с

Холодный старт кэша

Время открытия экрана Lobby → Slot, p95

3,8 с

0,7 с

Холодный старт кэша

Паузы main-thread >50 мс на 100 сессий

42

3

Сбор статистики за неделю

Пиковое использование RAM

680 МБ

510 МБ

Та же модель устройства

Число HTTP-запросов на переход

5–10 (последовательно)

1–2 (пакетно)

С keep-alive

Время расчёта выигрышных комбинаций (p95)

50 мс

5 мс

Сложная конфигурация

Crash-free users (за сутки)

94,2%

97,8%

На всей аудитории

D2 retention (процентные пункты)

Базовое значение

+7 п.п.

Статистически значимо (p<0.01)

FPS на целевых устройствах, p10

18 fps

29 fps

Во время игры в слот

Когда оптимизация не сработает

Было бы неправильно создавать впечатление, что оптимизация – это всегда лучший путь. Я сам видел проекты, где без портирования не обойтись. Например, если вы используете движок, завязанный на устаревший графический API (скажем, OpenGL ES 2.0), который перестаёт поддерживаться на новых устройствах, и переход на Vulkan или Metal требует глубокой переработки рендеринга. Или если ваш код написан настолько плохо, что любые изменения вызывают баги в неожиданных местах, а документации нет – тогда переписывание может оказаться дешевле, чем попытки что-то исправить.

Также портирование неизбежно, когда бизнес меняет направление: например, вы решаете добавить полноценную 3D-графику, а ваш текущий движок рассчитан только на 2D. Или, когда требуется кардинально изменить архитектуру клиент-серверного взаимодействия (например, перейти с синхронного REST на событийно-ориентированную модель с WebSocket), но это не обязательно требует смены игрового движка – можно заменить сетевой слой и постепенно мигрировать API.

В нашем случае ничего такого не было. Движок – самописный на C++ с NDK – имел запас прочности, команда знала код, а новые фичи легко вписывались в существующую архитектуру. Поэтому выбор оптимизации был не просто оправдан, а экономически очевиден.

Что мы вынесли из этого опыта

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

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

Третий – внедряйте изменения постепенно, с возможностью отката. Feature-флаги и staged rollout – это не просто модные слова, а реальный способ снизить риски. Мы ни разу не выкатили изменения на всех пользователей без предварительной проверки, и это спасло нас от пары потенциально неприятных ситуаций.

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

Заключение

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

Если вы узнаёте свою ситуацию – не спешите заказывать портирование. Сделайте профилирование, найдите узкие места и попробуйте сначала просто их исправить. Возможно, вы обнаружите, что нужная вам «революция» обойдётся всего лишь несколькими месяцами спокойной оптимизации.

Список литературы

  1. Koulaxidis G., Xinogalos S. Improving Mobile Game Performance with Basic Optimization Techniques in Unity. Modelling. 2022. Vol. 3, No. 2. P. 201-223. DOI: 10.3390/modelling3020014.
  2. Halbhuber D., Schauhuber P., Schwind V., Henze N. The Effects of Latency and In-Game Perspective on Player Performance and Game Experience. Proceedings of the ACM on Human-Computer Interaction. 2023. Vol. 7, CHI PLAY. Article 424. P. 1308-1329. DOI: 10.1145/3611070.
  3. Qian F., Wang Z., Gerber A., Mao Z., Sen S., Spatscheck O. Profiling Resource Usage for Mobile Applications: A Cross-Layer Approach. Proceedings of MobiSys ’11. 2011. P. 321-334. DOI: 10.1145/1999995.2000026.
  4. Linares-Vásquez M., Vendome C., Luo Q., Poshyvanyk D. How Developers Detect and Fix Performance Bottlenecks in Android Apps. 2015 IEEE International Conference on Software Maintenance and Evolution. 2015. DOI: 10.1109/ICSM.2015.7332486.
  5. Hertz M., Berger E. D. Quantifying the Performance of Garbage Collection vs. Explicit Memory Management. Proceedings of OOPSLA ’05. 2005. P. 313-326. DOI: 10.1145/1094811.1094836.
  6. Kim M., Zimmermann T., Nagappan N. An Empirical Study of Refactoring Challenges and Benefits at Microsoft. IEEE Transactions on Software Engineering. 2014. Vol. 40, No. 7. P. 633-650. DOI: 10.1109/TSE.2014.2313039.

Поделиться

18
Обнаружили грубую ошибку (плагиат, фальсифицированные данные или иные нарушения научно-издательской этики)? Напишите письмо в редакцию журнала: info@apni.ru

Похожие статьи

Другие статьи из раздела «Информационные технологии»

Все статьи выпуска
Актуальные исследования

#34 (320)

Прием материалов

15 августа - 21 августа

осталось 7 дней

Размещение PDF-версии журнала

26 августа

Размещение электронной версии статьи

сразу после оплаты

Рассылка печатных экземпляров

9 сентября