---
title: "Как найти просадку FPS в браузерной 2D-игре: бюджет кадра, Chrome DevTools Performance, долгие задачи и сборка мусора"
description: "Пошаговый поиск просадок кадра в браузерной 2D-игре: бюджет 16,67 мс и около 10 мс на свою работу по модели RAIL, замер performance.now() с перцентилями вместо среднего, запись в панели Performance Chrome DevTools с замедлением процессора, дорожка Frames, долгие задачи от 50 мс, Minor GC и график памяти. Код замера на JavaScript проверен в Node.js."
slug: prosadki-fps-v-brauzernoj-igre-chrome-devtools-performance
date: 2026-11-03
updated: 2026-11-03
scheduled_at: "2026-11-03T09:00:00+03:00"
rubric: logika-i-arhitektura
tags: ["proizvoditelnost", "chrome-devtools", "javascript", "profi", "otladka"]
reading_time: 9
draft: false
summary: "Просадку FPS в браузерной 2D-игре ищут в три шага. Сначала замеряют время кадра в коде через performance.now() и смотрят не на среднее, а на 99-й перцентиль и долю кадров сверх бюджета. Затем записывают проблемный участок в панели Performance Chrome DevTools с замедлением процессора в 4 раза и ищут красные кадры, долгие задачи (50 мс и дольше) и события сборки мусора. Наконец, по вкладке Bottom-up находят функцию с наибольшим собственным временем и исправляют её."
---

## Коротко

Просадку FPS в браузерной 2D-игре находят в три шага: замер в коде, запись в Chrome DevTools и поиск самой дорогой функции. Время кадра измеряют через `performance.now()` и сравнивают с бюджетом не по среднему, а по 99-му перцентилю. Затем проблемный участок записывают в панели Performance с замедлением процессора и ищут красные кадры, долгие задачи от `50 мс` и события сборки мусора, а по вкладке Bottom-up — функцию с наибольшим собственным временем.

## Содержание

1. Бюджет кадра: 16,67 мс, из которых ваших около 10
2. Замер в коде: performance.now() и перцентили вместо среднего
3. Запись в панели Performance: что смотреть
4. Долгие задачи: всё, что дольше 50 мс
5. Сборка мусора: пила на графике памяти
6. Практический сценарий: игра проседает при 200 врагах
7. Когда вам это не подойдёт

## Бюджет кадра: 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).

Главное правило — не смотреть на среднее. Длинные кадры редки, и среднее их растворяет, а игрок замечает именно их.

```js
// Сбор статистики времени кадра: среднее, перцентили, доля кадров сверх бюджета.
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). Этот фрагмент работает только в браузере:

```js
// Только для браузера: замер полного кадра и собственной работы.
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: он работает не во всех основных браузерах. Используйте его только как дополнительный сигнал и проверяйте наличие:

```js
// Только для браузера: логирование долгих задач там, где интерфейс поддерживается.
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` врагами игра на ноутбуке идёт ровно, а у части игроков дёргается. Цель — найти причину и подтвердить исправление цифрами.

1. **Замер в коде.** Добавьте `createFrameStats` и соберите 20 секунд игры на участке с врагами. Допустим, получилось: среднее `9 мс`, 99-й перцентиль `31 мс`, `4%` кадров сверх бюджета.
2. **Запись с замедлением.** Откройте Chrome DevTools, панель Performance, в настройках записи выберите замедление CPU в `4` раза, включите флажок Memory и запишите `5–10` секунд на том же участке.
3. **Кадры.** На дорожке Frames найдите красные (пропущенные) кадры и выделите один из них.
4. **Главный поток.** На дорожке Main под этим кадром найдите самую широкую задачу. Если в углу красный треугольник — это долгая задача.
5. **Bottom-up.** Отсортируйте по Self Time. Если первая строка — ваша функция проверки столкновений, проблема в алгоритме (каждый враг с каждым — это `200 × 199 ÷ 2 = 19 900` пар за кадр). Если в верхних строках Minor GC — ищите создание объектов в кадре по таблице выше.
6. **Исправление и повтор.** После правки повторите шаги 1–2 и сравните 99-й перцентиль и долю кадров сверх бюджета, а не впечатление «стало плавнее».

Таблица: Симптом → вероятная причина

| Что видно в записи | Вероятная причина | Куда смотреть дальше |
|---|---|---|
| Ровная высокая нагрузка в каждом кадре | Дорогой алгоритм в логике | Self Time функций игры |
| Редкие длинные кадры, рядом Minor GC | Временные объекты в кадре | График JS Heap, таблица источников мусора |
| Одна задача на `100+ мс` при входе на уровень | Загрузка или генерация в одном кадре | Перенести на экран загрузки или разбить на части |
| Нагрузка главного потока низкая, а кадры жёлтые или красные | Узкое место в отрисовке | Число вызовов отрисовки и размер текстур |

## Когда вам это не подойдёт

Этот порядок закрывает большинство просадок в 2D-играх на JavaScript, но есть случаи, где нужен другой инструмент:

- **Проблема только в Safari или на iPhone.** Панель Performance — инструмент Chrome; поведение другого браузера записывают его собственными инструментами разработчика, и цифры напрямую не сравнить.
- **Тормозит видеокарта, а не код.** Если главный поток почти свободен, а кадры всё равно пропускаются, разбирать нужно отрисовку: число вызовов и размер текстур, а не функции логики.
- **Просадка только на конкретном телефоне.** Замедление процессора на компьютере — приближение; окончательную проверку делают на самом устройстве.
- **Игра на WebAssembly.** Имена функций в записи могут быть непонятными без отладочной информации, и Bottom-up без неё мало поможет.

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок; он в разработке и публично пока не открыт, гарантий частоты кадров мы не даём, а заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=prosadki-fps-v-brauzernoj-igre-chrome-devtools-performance) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 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 Гц, пороги и примеры — наш расчёт.

**Читайте также:**
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
- [Поиск пути A* на тайловой сетке: бюджет кадра и много врагов](/blog/algoritm-a-star-poiska-puti-na-tajlovoj-setke)
- [Процедурная генерация подземелий: BSP-деревья и клеточные автоматы](/blog/procedurnaya-generaciya-podzemelij-bsp-i-kletochnye-avtomaty)
