Коротко
Просадку FPS в браузерной 2D-игре находят в три шага: замер в коде, запись в Chrome DevTools и поиск самой дорогой функции. Время кадра измеряют через performance.now() и сравнивают с бюджетом не по среднему, а по 99-му перцентилю. Затем проблемный участок записывают в панели Performance с замедлением процессора и ищут красные кадры, долгие задачи от 50 мс и события сборки мусора, а по вкладке Bottom-up — функцию с наибольшим собственным временем.
Содержание
- Бюджет кадра: 16,67 мс, из которых ваших около 10
- Замер в коде: performance.now() и перцентили вместо среднего
- Запись в панели Performance: что смотреть
- Долгие задачи: всё, что дольше 50 мс
- Сборка мусора: пила на графике памяти
- Практический сценарий: игра проседает при 200 врагах
- Когда вам это не подойдёт
Бюджет кадра: 16,67 мс, из которых ваших около 10
При 60 кадрах в секунду на кадр приходится 1000 ÷ 60 ≈ 16,67 мс, но вся эта величина игре не принадлежит. По модели RAIL браузеру нужно около 6 мс на отрисовку каждого кадра, поэтому на собственную работу кадра (логика, физика, подготовка спрайтов) рекомендуется закладывать около 10 мс (web.dev, Measure performance with the RAIL model, проверено: ноябрь 2026).
Частота кадров зависит от экрана: MDN указывает, что requestAnimationFrame обычно вызывается с частотой обновления дисплея, чаще всего 60 Гц, но распространены и 75, 120, 144 Гц (MDN, Window: requestAnimationFrame(), проверено: ноябрь 2026).
| Частота экрана | Длительность кадра | Ориентир на свою работу (наш расчёт: минус ~6 мс браузеру) |
|---|---|---|
60 Гц | 16,67 мс | около 10 мс |
75 Гц | 13,33 мс | около 7 мс |
120 Гц | 8,33 мс | около 2–3 мс, если игра ограничена 120 кадрами |
144 Гц | 6,94 мс | практически нет — игру разумнее привязать к 60 кадрам логики |
Вывод для 2D-игры: логику обычно считают с фиксированным шагом, а рисуют с частотой экрана; бюджет «около 10 мс» относится к кадру при 60 Гц.
Замер в коде: performance.now() и перцентили вместо среднего
Первым шагом замеряют время кадра в самой игре, потому что это дёшево и работает на любом устройстве. performance.now() возвращает время в миллисекундах дробным числом с точностью до микросекунд и, в отличие от Date.now(), использует монотонные часы, которые не прыгают при переводе системного времени (MDN, Performance: now(), проверено: ноябрь 2026).
Главное правило — не смотреть на среднее. Длинные кадры редки, и среднее их растворяет, а игрок замечает именно их.
// Сбор статистики времени кадра: среднее, перцентили, доля кадров сверх бюджета.
function createFrameStats(budgetMs) {
const samples = [];
return {
record: function (ms) { samples.push(ms); },
summary: function () {
const sorted = samples.slice().sort(function (a, b) { return a - b; });
const n = sorted.length;
const pick = function (p) { return sorted[Math.min(n - 1, Math.floor(p * n))]; };
let sum = 0, over = 0;
for (const ms of sorted) { sum += ms; if (ms > budgetMs) over++; }
return {
frames: n,
avg: +(sum / n).toFixed(2),
p95: pick(0.95),
p99: pick(0.99),
max: sorted[n - 1],
overBudgetPct: +((over / n) * 100).toFixed(1),
};
},
};
}
// Проверка в Node.js: 10 секунд при 60 к/с, каждые 50 кадров — длинный кадр 40 мс.
const stats = createFrameStats(1000 / 60);
for (let i = 0; i < 600; i++) stats.record(i % 50 === 49 ? 40 : 8);
console.log(stats.summary());
// { frames: 600, avg: 8.64, p95: 8, p99: 40, max: 40, overBudgetPct: 2 }
Среднее 8,64 мс выглядит прекрасно, а 99-й перцентиль 40 мс и 2% кадров сверх бюджета показывают, что игра дёргается каждые 50 кадров, то есть чуть меньше чем раз в секунду.
В браузере эти числа собирают в цикле requestAnimationFrame. Разница между соседними метками времени — полная длительность кадра, а замер вокруг update() — только ваша работа. Метки performance.mark() и performance.measure() к тому же видны в записи DevTools на дорожке Timings (Chrome DevTools, Performance features reference, проверено: ноябрь 2026). Этот фрагмент работает только в браузере:
// Только для браузера: замер полного кадра и собственной работы.
const frameStats = createFrameStats(1000 / 60);
const workStats = createFrameStats(10); // ориентир RAIL
let lastT = 0;
function loop(t) {
if (lastT) frameStats.record(t - lastT); // полный кадр
lastT = t;
const t0 = performance.now();
performance.mark("update-start");
update(); // логика игры
render(); // подготовка и отправка спрайтов
performance.measure("update+render", "update-start");
workStats.record(performance.now() - t0); // ваша работа
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// Через 10–20 секунд игры: console.table([frameStats.summary(), workStats.summary()])
Помните, что в фоновой вкладке большинство браузеров приостанавливает requestAnimationFrame (MDN), поэтому первый кадр после возврата во вкладку будет огромным; такие значения из статистики исключают.
Запись в панели Performance: что смотреть
Если замер показал длинные кадры, причину ищут записью в панели Performance Chrome DevTools. Официальное руководство Chrome советует перед записью включить замедление процессора: в настройках записи выбрать для CPU замедление в 4 раза, а для проверки на слабых мобильных устройствах — в 20 раз (руководство по панели Performance в документации Chrome DevTools, проверено: ноябрь 2026). Ноутбук разработчика заметно быстрее среднего телефона, и без замедления проблема может не воспроизвестись.
| Где | Что означает | Что искать |
|---|---|---|
| Дорожка FPS | Частота кадров во времени | Красная полоса над графиком: частота упала так, что это, вероятно, мешает игроку |
| Дорожка Frames | Каждый кадр отдельным блоком | Зелёный — вовремя, жёлтый с пунктиром — показан частично, красный — пропущен, белый — без изменений |
| Дорожка Main | Работа главного потока, ось Y — стек вызовов | Широкие блоки: чем шире, тем дольше; красный треугольник в углу задачи |
| Вкладка Bottom-up | Суммарное время по функциям | Столбец Self Time (собственное время) — функция, которая тратит время сама, а не через вызовы |
| Дорожка Timings | Ваши performance.mark() и measure() | Совпадает ли длинный кадр с вашим участком кода |
Типы кадров и цвета на дорожке Frames, столбцы Self Time и Total Time и дорожка Timings описаны в справочнике Performance features reference (Chrome DevTools, проверено: ноябрь 2026). Узкое место обычно там, где велико собственное время: большое полное время при малом собственном означает, что функция просто вызывает что-то дорогое.
Долгие задачи: всё, что дольше 50 мс
Долгая задача — это непрерывная работа главного потока длительностью 50 мс и больше. MDN перечисляет типичные причины: долгие обработчики событий, дорогие перерасчёты разметки и перерисовки (MDN, PerformanceLongTaskTiming, проверено: ноябрь 2026). В панели Performance такие задачи отмечены красным треугольником, а часть сверх 50 мс заштрихована красным (Performance features reference).
В игре долгая задача — это 3 пропущенных кадра подряд при 60 Гц, поэтому её видно сразу. Частые виновники в 2D-играх: загрузка и декодирование картинок посреди уровня, генерация уровня в одном кадре, сохранение большого объёма данных, синхронный разбор большого JSON.
Долгие задачи можно ловить и в коде через PerformanceObserver, но MDN помечает этот интерфейс как экспериментальный и не входящий в Baseline: он работает не во всех основных браузерах. Используйте его только как дополнительный сигнал и проверяйте наличие:
// Только для браузера: логирование долгих задач там, где интерфейс поддерживается.
if (typeof PerformanceObserver !== "undefined" &&
(PerformanceObserver.supportedEntryTypes || []).indexOf("longtask") !== -1) {
new PerformanceObserver(function (list) {
for (const e of list.getEntries()) {
console.warn("долгая задача:", Math.round(e.duration), "мс");
}
}).observe({ type: "longtask", buffered: true });
}
Сборка мусора: пила на графике памяти
Если в записи много коротких событий Minor GC, игра создаёт слишком много временных объектов. Справочник DevTools показывает пример, где под вызовами функции видны тысячи вызовов Minor GC, и во вкладке Bottom-up сборка мусора выходит на второе место по собственному времени; флажок Memory перед записью добавляет график памяти, включая кучу JavaScript (JS Heap) (Performance features reference, проверено: ноябрь 2026).
На графике памяти это выглядит как пила: куча растёт, сборщик мусора её очищает, и снова рост. Для игры это значит, что в каждом кадре создаётся что-то новое: массивы для результатов поиска столкновений, объекты {x, y} для векторов, строки для текста интерфейса, замыкания в обработчиках.
| В кадре создаётся | Замена |
|---|---|
const hits = [] для результатов столкновений | Один массив на всю игру, очистка через hits.length = 0 |
{ x: a.x + b.x, y: a.y + b.y } на каждый вектор | Запись в заранее созданный объект или в два числа |
| Новый снаряд или частица на каждый выстрел | Пул объектов: заранее созданные экземпляры, которые выключаются и переиспользуются |
Строка "Очки: " + score каждый кадр | Обновлять текст только когда счёт изменился |
array.map(...), array.filter(...) в цикле обновления | Обычный цикл for по существующему массиву |
Практический сценарий: игра проседает при 200 врагах
Задача: на уровне с 200 врагами игра на ноутбуке идёт ровно, а у части игроков дёргается. Цель — найти причину и подтвердить исправление цифрами.
- Замер в коде. Добавьте
createFrameStatsи соберите 20 секунд игры на участке с врагами. Допустим, получилось: среднее9 мс, 99-й перцентиль31 мс,4%кадров сверх бюджета. - Запись с замедлением. Откройте Chrome DevTools, панель Performance, в настройках записи выберите замедление CPU в
4раза, включите флажок Memory и запишите5–10секунд на том же участке. - Кадры. На дорожке Frames найдите красные (пропущенные) кадры и выделите один из них.
- Главный поток. На дорожке Main под этим кадром найдите самую широкую задачу. Если в углу красный треугольник — это долгая задача.
- Bottom-up. Отсортируйте по Self Time. Если первая строка — ваша функция проверки столкновений, проблема в алгоритме (каждый враг с каждым — это
200 × 199 ÷ 2 = 19 900пар за кадр). Если в верхних строках Minor GC — ищите создание объектов в кадре по таблице выше. - Исправление и повтор. После правки повторите шаги 1–2 и сравните 99-й перцентиль и долю кадров сверх бюджета, а не впечатление «стало плавнее».
| Что видно в записи | Вероятная причина | Куда смотреть дальше |
|---|---|---|
| Ровная высокая нагрузка в каждом кадре | Дорогой алгоритм в логике | Self Time функций игры |
| Редкие длинные кадры, рядом Minor GC | Временные объекты в кадре | График JS Heap, таблица источников мусора |
Одна задача на 100+ мс при входе на уровень | Загрузка или генерация в одном кадре | Перенести на экран загрузки или разбить на части |
| Нагрузка главного потока низкая, а кадры жёлтые или красные | Узкое место в отрисовке | Число вызовов отрисовки и размер текстур |
Когда вам это не подойдёт
Этот порядок закрывает большинство просадок в 2D-играх на JavaScript, но есть случаи, где нужен другой инструмент:
- Проблема только в Safari или на iPhone. Панель Performance — инструмент Chrome; поведение другого браузера записывают его собственными инструментами разработчика, и цифры напрямую не сравнить.
- Тормозит видеокарта, а не код. Если главный поток почти свободен, а кадры всё равно пропускаются, разбирать нужно отрисовку: число вызовов и размер текстур, а не функции логики.
- Просадка только на конкретном телефоне. Замедление процессора на компьютере — приближение; окончательную проверку делают на самом устройстве.
- Игра на WebAssembly. Имена функций в записи могут быть непонятными без отладочной информации, и Bottom-up без неё мало поможет.
Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, гарантий частоты кадров мы не даём, а заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники (проверено: ноябрь 2026): web.dev, Measure performance with the RAIL model (16 мс на кадр, около 6 мс браузеру, около 10 мс на свою работу); Chrome for Developers, руководство по анализу производительности в панели Performance, документация Chrome DevTools (замедление CPU в 4 и 20 раз, дорожка FPS, красный треугольник долгой задачи); Chrome for Developers, Performance features reference (типы кадров на дорожке Frames, штриховка сверх 50 мс, Minor GC в Bottom-up, Self Time и Total Time, флажок Memory, дорожка Timings); MDN, Performance: now(); MDN, Window: requestAnimationFrame(); MDN, PerformanceLongTaskTiming. Таблица бюджета для 75–144 Гц, пороги и примеры — наш расчёт.
Читайте также: