GameDev Guide
Глава 1615 мин чтенияПроверено: 5 сентября 2026 г.

Производительность и бюджет кадра

Разберём бюджет кадра в 16 мс, профилирование во вкладке Performance, типичные причины просадок в 2D-играх, оптимизацию ассетов и звука — и требование останавливать звук при потере фокуса.

Содержание главы

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

Проблема в том, что «тормозит» — не бинарное состояние. На вашем ноутбуке игра идёт идеально, а модератор открывает её на телефоне трёхлетней давности через мобильный интернет. Единственный способ не гадать — измерять. Эта глава про то, чем и что именно.

Бюджет кадра

При 60 кадрах в секунду на кадр приходится 16,7 мс. За это время браузер обязан успеть всё: ваш update(), ваш draw(), композитинг, обработку ввода, сборку мусора. Реально в распоряжении игрового кода — 10–12 мс, остальное съест сам браузер.

Из этого следуют два практических вывода. Первый: 8 мс на кадр на вашей машине — это не «в два раза быстрее нужного», это «на слабом телефоне будет 25 мс и просадка». Второй: важна не средняя частота кадров, а худшая. Стабильные 45 fps ощущаются лучше, чем 60 fps с провалом до 12 fps раз в две секунды — а именно такой провал и есть «фриз», который увидит модератор.

Простейший счётчик, который стоит держать в игре с самого начала:

// perf.js
let last = performance.now();
let worst = 0;
let frames = 0;
let acc = 0;

export function tickPerf(now) {
  const dt = now - last;
  last = now;
  frames += 1;
  acc += dt;
  if (dt > worst) worst = dt;

  if (acc >= 1000) {
    console.log(
      'fps', Math.round(frames * 1000 / acc),
      '| худший кадр', worst.toFixed(1), 'мс'
    );
    frames = 0; acc = 0; worst = 0;
  }
}

Вызывайте tickPerf(now) первой строкой игрового цикла и выключайте вывод в релизной сборке (лишние логи в консоли модерация тоже видит). «Худший кадр» за секунду — самая информативная цифра из всех: пока она держится ниже 30 мс, игра ощущается плавной.

Профилирование во вкладке Performance

Платформа отправляет во вкладку Performance две собственные метки, и это документированная возможность, а не хак:

МеткаЧто означает
Game ReadyМомент вызова ysdk.features.LoadingAPI.ready() — игра загрузила ресурсы и готова к взаимодействию
Time To Interactive (TTI)Момент, когда завершились все длительные задачи; учитывает загруженность браузера при инициализации

Порядок работы:

  1. Запустить игру любым удобным способом — локально через sdk-dev-proxy или в режиме черновика.
  2. Открыть DevTools (F12 или Ctrl + Shift + I, на macOS Cmd + Opt + I).
  3. Перейти на вкладку Performance.
  4. Нажать Record and reload и дождаться сборки профиля.
  5. В строке Timings появятся жёлтые метки Game Ready и Time To Interactive.

Дальше смотрите, что происходит до метки Game Ready: длинные жёлтые полосы — это ваш JS, фиолетовые — пересчёт стилей и разметки, зелёные — отрисовка. Задачи длиннее 50 мс браузер помечает как long tasks; на старте они неизбежны, в геймплее их быть не должно.

Важно

На debug-панели у Game Ready есть таймаут в 90 секунд. Если LoadingAPI.ready() не вызван за это время, индикатор становится красным и считается, что Game Ready в игре не используется — это отказ по пункту 1.19.2. Медленная загрузка перестаёт быть просто неудобством и становится нарушением.

Профилируйте с включённым троттлингом CPU (в Performance — выпадающий список CPU: 4× slowdown) и сети. Это ближайшее, что есть у вас к «среднему телефону», пока вы не открыли игру на реальном телефоне через режим черновика.

Пять причин просадок в 2D-играх

Список короткий, потому что в 2D на canvas почти всегда виноват один из пяти пунктов.

1. Перерисовка того, что не изменилось. Полная очистка канваса и отрисовка всей сцены каждый кадр — нормально для динамичной игры и расточительно для головоломки, где меняются две клетки. Статичный фон выносится в отдельный слой (второй <canvas> под игровым) и не перерисовывается вовсе.

2. Создание объектов в игровом цикле. Каждый {x, y}, каждый new Vector(), каждый array.map() внутри update() — это мусор, который сборщик потом соберёт одним рывком. Именно так выглядит «фриз раз в несколько секунд».

// Плохо: 500 новых объектов и новый массив каждый кадр.
function update() {
  particles = particles
    .map((p) => ({ x: p.x + p.vx, y: p.y + p.vy, life: p.life - 1 }))
    .filter((p) => p.life > 0);
}

// Хорошо: мутируем на месте, идём с конца, ничего не аллоцируем.
function update() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.life -= 1;
    if (p.life <= 0) {
      particles[i] = particles[particles.length - 1];
      particles.pop();
    }
  }
}

3. Отсутствие пула объектов. Пули, частицы, всплывающие цифры урона создаются и выбрасываются десятками в секунду. Пул решает это в двадцать строк:

// pool.js
export function createPool(factory, reset, size) {
  const free = [];
  for (let i = 0; i < size; i++) free.push(factory());

  return {
    take() {
      return free.length ? free.pop() : factory();
    },
    put(obj) {
      reset(obj);
      free.push(obj);
    },
  };
}

const bullets = createPool(
  () => ({ x: 0, y: 0, vx: 0, vy: 0, alive: false }),
  (b) => { b.alive = false; },
  200
);

4. Крупные PNG. Фон 4096 × 4096 в PNG — это 10 МБ трафика и, что важнее, около 64 МБ видеопамяти после распаковки, независимо от размера файла. На бюджетном телефоне это гарантированные подтормаживания или падение вкладки. Держите текстуры в размере, в котором они реально рисуются, и складывайте мелкую графику в атласы: один drawImage из атласа дешевле, чем десять из десяти файлов.

5. Тяжёлые эффекты canvas. shadowBlur, filter, globalCompositeOperation и рисование текста каждый кадр стоят дорого. Текст, который меняется редко (название уровня, имя игрока), рисуйте один раз в offscreen-canvas и копируйте готовую картинку.

Совет

Прежде чем оптимизировать, снимите профиль и найдите самую длинную полосу. В 2D-играх интуиция ошибается регулярно: «тормозит физика» на профиле оказывается перерисовкой фона, а «тормозит отрисовка» — сборкой мусора от строк, которые вы клеите в HUD.

Ассеты: что реально уменьшает время до Game Ready

Ограничение на архив — 100 МБ до сжатия, и упереться в него сложно. Гораздо важнее другое: показатель Конверсия в игровую сессию в Консоли считает долю сессий длиннее 60 секунд, и документация прямо советует для его улучшения сокращать размер ресурсов на стадии загрузки. Игрок, который ждёт 15 секунд на пустом экране, уходит до того, как увидит игру.

Что даёт эффект:

  • Разделить загрузку на обязательную и отложенную. До Game Ready грузите только то, что нужно для первого экрана и первого уровня. Музыку следующих уровней и графику магазина — после.
  • Сжать PNG. pngquant/oxipng дают минус 50–70 % без видимой разницы. Фотографические фоны — в JPEG.
  • Осторожно с WebP и AVIF. Требование 1.20.3 обязывает игру работать на iOS 9.0+, а поддержка этих форматов появилась в мобильных браузерах гораздо позже. Без запасного PNG/JPEG вы рискуете получить пустые текстуры у части аудитории.
  • Собрать спрайты в атласы. Меньше HTTP-запросов, меньше переключений текстуры при отрисовке.
  • Не тянуть весь движок ради двух функций. Библиотека на 300 КБ ради твинов — это лишняя секунда на медленной сети.

Звук — отдельная статья расходов. Длинные WAV в архиве встречаются до сих пор: конвертируйте музыку в MP3 или OGG с битрейтом 96–128 кбит/с, короткие эффекты — в один спрайт-файл со смещениями. Декодирование звука тоже стоит времени: если игра запускает decodeAudioData для сорока файлов на старте, Game Ready сдвинется на секунды.

Требование 1.3: звук обязан замолкать

Это не про производительность, но живёт в том же коде и стоит в списке частых причин отказа: «Звук продолжается при переключении вкладки».

Методика проверки описана буквально. Модерация смотрит, останавливается ли звук:

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

Не считается нарушением: задержка до 2 секунд, продолжение звука после клика по рекламному баннеру и перехода на вкладку с предложением, а также звук в меню выбора вкладок на iOS.

Рабочая связка — событие браузера плюс события платформы:

// audio-focus.js
function suspendAll() {
  if (audioCtx.state === 'running') audioCtx.suspend();
  bgm.pause();
}

function resumeAll() {
  if (audioCtx.state === 'suspended') audioCtx.resume();
  if (!isPausedByGame) bgm.play().catch(() => {});
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') suspendAll();
  else resumeAll();
});

window.addEventListener('blur', suspendAll);
window.addEventListener('focus', resumeAll);

// Реклама, окно покупок, сворачивание — приходят от платформы.
ysdk.on('game_api_pause', suspendAll);
ysdk.on('game_api_resume', resumeAll);

visibilitychange покрывает переключение вкладки, blur/focus — сворачивание окна и переход в другое приложение, game_api_pause — рекламу и системные окна платформы. Проверить всё это без реального устройства помогает debug-панель: кнопка с глазом снимает фокус с игры и возвращает его обратно.

Осторожно

Останавливать нужно и игровой цикл, а не только звук. Игра, продолжающая симуляцию в фоновой вкладке, к возвращению игрока успевает «проиграть» за него минуту, а браузер в это время душит requestAnimationFrame — после возврата вы получите гигантский deltaTime и телепортацию персонажа сквозь стены. Ограничивайте шаг симуляции: dt = Math.min(dt, 50).

Чек-лист перед отправкой

  • Худший кадр в геймплее — ниже 30 мс на среднем устройстве, не только на вашем.
  • Профиль снят с включённым CPU 4× slowdown, метка Game Ready укладывается в несколько секунд.
  • В update() не создаются объекты и массивы; частицы и снаряды идут через пул.
  • Ни одна текстура не крупнее, чем нужно для реальной отрисовки; графика собрана в атласы.
  • Звук останавливается по трём сценариям из методики проверки пункта 1.3 и по game_api_pause.
  • Игра проверена на реальном телефоне через режим черновика, а не только в эмуляции DevTools.
  • В консоли нет ошибок и предупреждений после десяти минут игры.

Что дальше

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

Источники

Официальная документация платформы — единственный источник истины по её требованиям.

Нужно подробнее?

Полное обучение разбирает те же темы глубже: уроки с примерами, готовые структуры проектов, развёрнутые чек-листы и файл инструкций для Claude Code — он входит в стоимость. Бесплатные главы при этом остаются открытыми и продолжают дополняться.

Что входит в полное обучение