Коротко
Браузерная 2D-игра тормозит на отрисовке чаще из-за числа вызовов отрисовки, чем из-за числа спрайтов: каждая смена текстуры, шейдера или режима смешивания начинает новый пакет. Поэтому спрайты складывают в атласы и рисуют сгруппированно, по слоям и по атласам. WebGPU — преемник WebGL, работающий поверх Direct3D 12, Metal и Vulkan, но по данным caniuse на конец октября 2026 года его полностью поддерживают браузеры около 85,8% пользователей, поэтому игре нужен запасной путь через WebGL2.
Содержание
- Вызов отрисовки и пакет: что на самом деле стоит дорого
- Что разрывает пакет: правило подсчёта
- Сколько данных уходит в видеокарту за кадр
- WebGPU и WebGL2: чем отличаются и где работают в ноябре 2026
- Как устроен переход с WebGPU на WebGL2
- Практический сценарий: 600 монет на экране
- Когда вам это не подойдёт
Вызов отрисовки и пакет: что на самом деле стоит дорого
Дорого стоит не сам спрайт, а каждая отдельная команда видеокарте «нарисуй вот это»: вызов отрисовки. Видеокарта рисует тысячи треугольников за один вызов почти так же быстро, как два, а подготовка каждого вызова (проверка состояния, привязка текстуры и шейдера) ложится на процессор и браузер.
Документация MDN WebGL best practices формулирует правило прямо: объединяйте вызовы в меньшее число крупных; если нужно нарисовать 1000 спрайтов, попробуйте сделать это одним вызовом drawArrays() или drawElements() (MDN, раздел «Batch draw calls», проверено: ноябрь 2026). Там же названа ещё одна техника — атлас текстур: несколько картинок в одной текстуре, потому что для смены текстуры пакет приходится разрывать.
| Способ | Вызовов отрисовки | Что делает процессор |
|---|---|---|
| Каждый спрайт отдельно | 1000 | 1000 раз готовит состояние и отправляет команду |
| Все спрайты из одного атласа одним пакетом | 1 | Один раз заполняет буфер вершин и отправляет одну команду |
| 4 атласа, спрайты отсортированы по атласу | 4 | Четыре команды |
Что разрывает пакет: правило подсчёта
Новый пакет начинается каждый раз, когда в порядке рисования меняется хотя бы одно из состояний: текстура (атлас), шейдер (материал), режим смешивания или цель отрисовки. Отсюда простое правило, которое можно проверить на бумаге до запуска игры:
число пакетов = 1 + число смен состояния в порядке рисования
Пример: слой «Фон» (атлас A), слой «Мир» (атласы A и B вперемешку), слой «Интерфейс» (атлас C)
Вперемешку A, B, A, B ... для 200 объектов мира:
1 (фон) + 200 (мир, смена на каждом объекте) + 1 (интерфейс) ≈ 202 пакета
Внутри слоя «Мир» сначала все из A, потом все из B:
1 (фон A) + 0 (мир A продолжает пакет фона) + 1 (мир B) + 1 (интерфейс C) = 3 пакета
Порядок слоёв при этом менять нельзя: то, что должно быть поверх, рисуется позже. Сортировать по атласу можно только внутри одного слоя и только те объекты, которые не перекрываются полупрозрачными краями. Поэтому практический рецепт такой: один атлас на слой, где это возможно, и как можно меньше слоёв, в которых спрайты из разных атласов перемешаны.
Сколько данных уходит в видеокарту за кадр
Для типичного спрайтового пакета это десятки килобайт за кадр, и этот объём легко посчитать заранее. Прямоугольный спрайт — это 4 вершины и 2 треугольника (6 индексов). Если на вершину приходится позиция (2 числа по 4 байта), координаты в атласе (2 × 4 байта) и цвет (4 байта), получается 20 байт:
байт на спрайт = 4 вершины × 20 байт = 80 байт
1000 спрайтов = 80 000 байт ≈ 78 КБ на кадр
при 60 кадр/с = 4 800 000 байт ≈ 4,6 МБ в секунду
индексы (6 штук по 2 байта на спрайт) статичны: их загружают один раз
Это наш расчёт для примерного формата вершины; реальные движки упаковывают данные по-своему. Вывод от формата не зависит: при пакетной отправке узкое место смещается с числа команд на объём буфера, а он для 2D-сцены невелик.
WebGPU и WebGL2: чем отличаются и где работают в ноябре 2026
WebGPU — новый графический интерфейс браузера, который MDN называет преемником WebGL: он работает поверх современных интерфейсов ОС (Direct3D 12, Metal, Vulkan), даёт доступ к вычислениям общего назначения на видеокарте, а для OpenGL, а значит и для WebGL, новых обновлений не планируется (MDN, WebGPU API, проверено: ноябрь 2026). WebGPU доступен только на защищённых страницах (HTTPS).
Первым WebGPU по умолчанию включил Chrome 113: о выпуске команда Chrome объявила 6 апреля 2023 года, первая версия работала на ChromeOS, macOS и Windows (Chrome for Developers, Chrome ships WebGPU, проверено: ноябрь 2026). По данным MDN, с версии 144 Chrome поддерживает WebGPU и на Linux, но только с видеокартами Intel Gen12 и новее.
| Браузер | WebGPU | WebGL2 |
|---|---|---|
| Chrome и Edge на компьютере | Да, с версии 113 | Да (Chrome с 56, Edge с 79) |
| Chrome для Android | Да (по MDN — с версии 121) | Да |
| Safari на macOS | Частично с 26.0: по умолчанию только на macOS 26 Tahoe и новее | Да, с версии 15 |
| Safari на iPhone и iPad | Да, с версии 26.0 | Да, с версии 15 |
| Firefox на компьютере | Частично с 141: по умолчанию на Windows и на macOS 26 Tahoe с Apple Silicon | Да, с версии 51 |
| Firefox для Android | Нет (выключен по умолчанию) | Да |
| Общий охват пользователей | 85,8% полностью + 3,66% частично ≈ 89,5% | 97,13% |
Разница означает простую вещь: игра, которая умеет рисовать только через WebGPU, сегодня не запустится примерно у 10–14% пользователей (в зависимости от того, учитывать ли частичную поддержку), то есть примерно у каждого седьмого–десятого, а без WebGL2 остаются около 3%. Эти цифры меняются с каждым выпуском браузеров, поэтому их нужно перепроверять перед выбором технологии.
Как устроен переход с WebGPU на WebGL2
Переход устроен как проверка при запуске: если браузер не дал доступ к WebGPU, игра создаёт контекст WebGL2 и рисует через него. Проверка состоит из трёх шагов:
1. есть ли navigator.gpu? нет → WebGL2
2. вернул ли requestAdapter() адаптер? нет (null или ошибка) → WebGL2
3. удалось ли requestDevice()? нет → WebGL2
иначе → WebGPU
отдельно: страница открыта не по HTTPS → navigator.gpu нет → WebGL2
В коде это одна асинхронная функция. Объекты nav и canvas передаются параметрами, чтобы функцию можно было проверить без браузера:
// Выбор способа отрисовки при запуске игры.
// nav — объект navigator, canvas — элемент canvas.
async function pickRenderer(nav, canvas) {
if (nav && nav.gpu) {
try {
const adapter = await nav.gpu.requestAdapter();
if (adapter) {
const device = await adapter.requestDevice();
return { kind: "webgpu", device: device };
}
} catch (err) {
// ошибка на любом шаге — идём к WebGL2
}
}
const gl = canvas.getContext("webgl2");
if (gl) return { kind: "webgl2", gl: gl };
return { kind: "none" }; // показать игроку сообщение, а не пустой экран
}
// Проверка без браузера: поддельные navigator и canvas.
const fakeCanvas = { getContext: function (name) { return name === "webgl2" ? {} : null; } };
const noGpu = {};
const nullAdapter = { gpu: { requestAdapter: async function () { return null; } } };
const goodGpu = { gpu: { requestAdapter: async function () {
return { requestDevice: async function () { return { label: "dev" }; } };
} } };
(async function () {
console.log((await pickRenderer(noGpu, fakeCanvas)).kind); // webgl2
console.log((await pickRenderer(nullAdapter, fakeCanvas)).kind); // webgl2
console.log((await pickRenderer(goodGpu, fakeCanvas)).kind); // webgpu
})();
Важно, что после такого выбора вся остальная игра не должна знать, какой путь сработал: правила пакетной отправки из первых разделов одинаково работают и в WebGPU, и в WebGL2.
Практический сценарий: 600 монет на экране
Задача: на уровне 1280×720 одновременно видны 600 монет, 1 герой, 40 тайлов фона и интерфейс; нужно, чтобы кадр укладывался в 16,67 мс при 60 кадр/с (1000 мс ÷ 60).
- Посчитайте пакеты на бумаге. Если монеты, герой и тайлы лежат в одном атласе, а интерфейс — во втором, по формуле выше выходит
2пакета; если у монеты свой файл, а между монетами рисуются враги из другого файла, пакетов может быть сотни. - Разложите по слоям:
Фон,Мир(монеты и герой),Интерфейс. ВнутриМирарисуйте сначала все спрайты одного атласа, потом другого. - Посчитайте вызовы в коде. В WebGL2 достаточно обернуть
drawElementsсчётчиком на время отладки. - Замерьте время кадра через
performance.now()в начале и в конце кадра и сравните со16,67 мс. - Проверьте оба пути: на устройстве с WebGPU и на устройстве без него (например, Firefox для Android) игра должна работать одинаково.
Счётчик вызовов и замер кадра на время отладки:
// Обёртка, которая считает вызовы drawElements за кадр.
function countDrawCalls(gl) {
const realDraw = gl.drawElements.bind(gl);
const stats = { calls: 0 };
gl.drawElements = function (mode, count, type, offset) {
stats.calls += 1;
return realDraw(mode, count, type, offset);
};
return stats;
}
const BUDGET_MS = 1000 / 60; // 16,67 мс
function measureFrame(stats, drawScene) {
stats.calls = 0;
const t0 = performance.now();
drawScene();
const ms = performance.now() - t0;
return { calls: stats.calls, ms: ms, ok: ms <= BUDGET_MS };
}
// Проверка без браузера: поддельный gl и сцена из 600 монет в одном атласе.
const fakeGl = { drawElements: function () {} };
const stats = countDrawCalls(fakeGl);
const coins = [];
for (let i = 0; i < 600; i++) coins.push({ atlas: "A" });
function drawScene() {
let current = null;
for (const c of coins) {
if (c.atlas !== current) { if (current) fakeGl.drawElements(); current = c.atlas; }
}
fakeGl.drawElements(); // последний пакет
}
const r = measureFrame(stats, drawScene);
console.log("вызовов:", r.calls, "бюджет:", BUDGET_MS.toFixed(2), "мс"); // вызовов: 1 бюджет: 16.67 мс
Время, измеренное performance.now() вокруг вызовов WebGL, показывает работу процессора на подготовку кадра, а не время работы видеокарты; для первой проверки «не слишком ли много пакетов» этого достаточно.
Готовый чек-лист «быстрой отрисовки»
| № | Правило | Как проверить |
|---|---|---|
| 1 | Один атлас на слой, где это возможно | Посчитать смены атласа в порядке рисования по формуле 1 + число смен |
| 2 | Минимум слоёв со спрайтами из разных атласов вперемешку | Выписать порядок рисования слоя и отметить смены атласа |
| 3 | Не рисовать объекты за пределами экрана | Сравнить число вызовов на пустом и заполненном участке уровня |
| 4 | Бюджет кадра — 16,67 мс при 60 кадр/с | performance.now() в начале и в конце кадра |
| 5 | Игра проверена и на WebGPU, и на WebGL2 | Два устройства: одно с WebGPU, одно без |
Когда вам это не подойдёт
Правила пакетной отправки и перехода на WebGL2 закрывают отрисовку типичной 2D-сцены, но не всё:
- Узкое место не в отрисовке. Если замер показывает, что время уходит на логику (например, проверку каждого объекта с каждым), атласы не помогут — сначала разберитесь с проверкой столкновений.
- Нужны вычисления на видеокарте. Частицы или симуляции на вычислительных шейдерах есть только в WebGPU; на WebGL2 их придётся делать иначе или отказаться от них на части устройств.
- Сотни полупрозрачных перекрывающихся спрайтов из разных атласов. Сортировать их по атласу нельзя без нарушения порядка наложения, поэтому пакетов будет много — такой эффект лучше собрать в один атлас.
- Простая игра на Canvas2D. Если на экране десятки спрайтов, а не сотни, обычный
CanvasRenderingContext2Dсправится, и переход на WebGL2 или WebGPU не окупит сложность.
Мы разрабатываем J2DRU — браузерный 2D игровой движок, который в браузере без WebGPU работает через WebGL2; он в разработке и публично пока не открыт, гарантий частоты кадров мы не даём, а заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники (проверено: ноябрь 2026): MDN, WebGL best practices (раздел «Batch draw calls», атласы текстур); MDN, WebGPU API (преемник WebGL, Direct3D 12 / Metal / Vulkan, только HTTPS); MDN Browser Compat Data, api.GPU (Chrome 144 на Linux, Chrome для Android с 121, Firefox 141 и 145); Chrome for Developers, Chrome ships WebGPU, 06.04.2023; caniuse.com, таблицы «WebGPU» и «WebGL 2.0», выпуск базы 1.0.30001820 от 29.10.2026. Расчёт байт на спрайт и число пакетов в примерах — наши.
Читайте также: