Коротко
Для настроек и небольшого прогресса браузерной игры хватает localStorage: он хранит только строки, около 5 МиБ на сайт и бросает QuotaExceededError при переполнении. Для больших сохранений, нескольких слотов и бинарных данных нужен IndexedDB. В обоих случаях в сохранении нужно поле версии с цепочкой миграций, сохранять надо при visibilitychange со статусом hidden, а не при закрытии страницы, и помнить, что без navigator.storage.persist() браузер вправе удалить данные сайта.
Содержание
- Что именно сохранять
- localStorage: просто, но с ограничениями
- IndexedDB: для больших и сложных сохранений
- Версия формата и миграции
- Когда сохранять
- Почему сохранения пропадают
- Чек-лист: надёжное сохранение за вечер
- Когда вам это не подойдёт
Что именно сохранять
Сохранять нужно минимальное состояние, из которого игру можно восстановить, а не весь мир целиком. Позиции частиц, кадры анимации и путь врага пересчитываются; номер уровня, контрольная точка, инвентарь, открытые двери и счёт — нет.
const save = {
version: 3, // версия формата, обязательна
savedAt: Date.now(),
level: "forest-2",
checkpoint: 4,
hp: 5,
inventory: ["key-red", "bomb", "bomb"],
openedDoors: [12, 17],
settings: { music: 0.6, sfx: 0.8 }
};
Настройки (громкость, управление) удобно хранить отдельно от прогресса: их читают до загрузки уровня, и их порча не должна стоить игроку прохождения.
localStorage: просто, но с ограничениями
localStorage — самый простой способ сохранить данные между сеансами, но у него четыре ограничения, которые надо знать заранее (MDN, проверено: ноябрь 2026):
- Только строки. Ключи и значения хранятся строками UTF-16, поэтому объект нужно превращать в JSON.
- Около 5 МиБ на сайт. По MDN браузеры дают до 5 МиБ для
localStorageи отдельно до 5 МиБ дляsessionStorageна каждый сайт (источник). При превышении бросаетсяQuotaExceededError, и MDN прямо советует оборачивать запись вtry...catch. - Разные адреса — разные хранилища. Данные для
http://иhttps://версии одного домена хранятся отдельно; для страниц, открытых какfile:, поведение не определено. - Приватный режим. Данные
localStorageприватного окна удаляются, когда закрыта последняя приватная вкладка.
function writeSave(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (e) {
// QuotaExceededError или запрет хранения в настройках браузера
console.warn("Сохранение не удалось:", e.name);
return false;
}
}
Ещё один практический момент: вызовы localStorage синхронные, то есть выполняются прямо в игровом цикле. Для сохранения размером в несколько килобайт это незаметно; для мегабайтного JSON запись может занять заметную часть кадра, и это повод перейти на IndexedDB.
IndexedDB: для больших и сложных сохранений
IndexedDB — асинхронное хранилище объектов в браузере, рассчитанное на большие объёмы данных. По MDN (Using IndexedDB, проверено: ноябрь 2026) работа строится так: открыть базу с номером версии, создать хранилища объектов в обработчике onupgradeneeded, а чтение и запись выполнять в транзакциях.
localStorage | IndexedDB | |
|---|---|---|
| Что хранит | Строки | Объекты, массивы, бинарные данные |
| Вызовы | Синхронные | Асинхронные, через запросы и транзакции |
| Объём | До 5 МиБ на сайт | Общая квота хранилища сайта, обычно намного больше |
| Схема и версии | Нет, всё на вас | Номер версии базы, onupgradeneeded |
| Порог входа | Две строки кода | Заметно больше кода; часто берут обёртку |
Две детали из MDN, которые важны именно для сохранений:
- Версия базы — целое число. Значения
2.1и2.4округляются до2, и обновление между ними не вызоветonupgradeneeded. - Одна логическая операция — одна транзакция. MDN приводит пример: если очистить хранилище в одной транзакции, а записать новые данные в другой, закрытие браузера между ними оставит пустую базу. Удаление старого слота и запись нового должны идти в одной транзакции.
Если не хочется писать код запросов вручную, MDN перечисляет готовые обёртки; например, idb-keyval — хранилище «ключ — значение» на промисах размером около 600 байт.
Версия формата и миграции
Поле version в каждом сохранении — единственный способ безопасно менять формат после релиза. Без него игра после обновления либо падает на старом сохранении, либо молча теряет прогресс. Рабочая схема — цепочка миграций, где каждая функция поднимает версию на 1:
const CURRENT_VERSION = 3;
const migrations = {
1: (s) => ({ ...s, version: 2, hp: s.hp ?? 5 }), // в v2 появилось здоровье
2: (s) => ({ ...s, version: 3, openedDoors: s.doors || [] }) // в v3 поле переименовано
};
function loadSave(raw) {
let s = JSON.parse(raw);
if (typeof s.version !== "number") s.version = 1;
if (s.version > CURRENT_VERSION) throw new Error("Сохранение из более новой версии игры");
while (s.version < CURRENT_VERSION) s = migrations[s.version](s);
return s;
}
Две страховки дополняют миграции. Первая — два слота: новое сохранение пишется в свободный слот, и только после успешной записи помечается как текущее; при сбое остаётся предыдущее. Вторая — проверка при загрузке: если JSON.parse или миграция упали, игра предлагает загрузить предыдущий слот, а не начинает с нуля.
Когда сохранять
Сохранять нужно на контрольных точках и при уходе со страницы, причём уход ловится событием visibilitychange. По MDN оно срабатывает со статусом hidden, когда игрок переключает вкладку, переходит на другую страницу, закрывает вкладку, сворачивает браузер или на телефоне переключается на другое приложение; это последнее событие, которое страница надёжно получает (проверено: ноябрь 2026).
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
pauseGame();
writeSave("save-auto", collectSaveData());
}
});
Полагаться на обработчики выгрузки страницы нельзя: MDN предупреждает, что транзакции IndexedDB, начатые в обработчике выгрузки при закрытии браузера, не завершатся. Синхронная запись в localStorage в обработчике visibilitychange — самый простой надёжный вариант для небольших сохранений.
Почему сохранения пропадают
Сохранения пропадают, потому что по умолчанию браузер хранит данные сайта в режиме best-effort и может их удалить. По MDN (Storage quotas and eviction criteria, проверено: ноябрь 2026):
- Нехватка места. Браузер удаляет данные сайтов, которые давно не посещались (политика LRU), причём удаляются сразу все данные сайта, а не отдельные записи. Сайты с постоянным хранением при этом пропускаются.
- Safari и защита от трекинга. Данные, созданные скриптом, удаляются, если с сайтом не было взаимодействия 7 дней использования браузера.
- Приватный режим. Данные обычно удаляются по окончании приватного сеанса.
| Браузер | Квота сайта |
|---|---|
| Firefox | Меньшее из 10% диска и 10 ГиБ (на группу сайтов); с постоянным хранением — до 50% диска |
| Chromium-браузеры | До 60% диска |
| Safari (macOS 14, iOS 17 и новее) | Около 60% диска для браузеров, около 15% для встроенных WebView |
Что с этим делать:
async function protectSaves() {
if (navigator.storage && navigator.storage.persist) {
const persisted = await navigator.storage.persist(); // запрос постоянного хранения
const { usage, quota } = await navigator.storage.estimate();
console.log("persist:", persisted, "занято:", usage, "из", quota);
}
}
Firefox при вызове persist() показывает игроку запрос, а Safari и Chromium-браузеры решают сами, опираясь на историю взаимодействия с сайтом. Поэтому вызывать его разумнее после того, как игрок явно вовлечён (например, после первой ручной записи), а не при первом открытии страницы. Последняя линия защиты — экспорт: игрок по команде из меню скачивает сохранение JSON-файлом и может импортировать его обратно.
Чек-лист: надёжное сохранение за вечер
Задача: добавить сохранение в браузерную 2D-игру так, чтобы прогресс переживал обновление игры, переполнение хранилища и закрытие вкладки. Проверяемый результат — пять ручных тестов ниже проходят без потери прогресса.
- Опишите объект сохранения с полем
version, без вычисляемых данных. - Разделите ключи:
settings,save-1,save-2,save-auto. - Оберните запись в
try...catchи покажите игроку понятное сообщение при ошибке. - Напишите цепочку миграций и тест на загрузку сохранения каждой прошлой версии.
- Сохраняйте на контрольных точках и при
visibilitychange→hidden. - Запросите
persist()после первой ручной записи. - Добавьте экспорт и импорт в файл.
- Проверьте вручную: закрыть вкладку посреди уровня; переключиться на другое приложение на телефоне; загрузить сохранение прошлой версии; испортить JSON в DevTools; заполнить хранилище до ошибки.
Когда вам это не подойдёт
Хранение в браузере подходит для одиночной игры на одном устройстве. Случаи, когда нужно другое:
- Прогресс на нескольких устройствах. Браузерное хранилище привязано к браузеру и сайту; для синхронизации нужен сервер и учётная запись.
- Соревновательная игра с таблицей рекордов. Игрок может отредактировать локальное сохранение в DevTools; результаты, которые важны другим, проверяет сервер.
- Игра публикуется на внешней площадке во фрейме. Доступ к хранилищу во встроенном фрейме зависит от политики браузера и площадки; проверяйте на каждой площадке отдельно.
- Сохранение сотен мегабайт. Ресурсы (графику, звук) кешируют отдельно, а в сохранение пишут только состояние игры.
Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники (проверено: ноябрь 2026): MDN — Storage quotas and eviction criteria (лимиты Web Storage, QuotaExceededError, best-effort и постоянное хранение, квоты Firefox, Chromium и Safari, LRU-вытеснение, правило 7 дней в Safari, приватный режим); MDN — Window: localStorage property (строки UTF-16, разделение http и https, file:, приватные вкладки); MDN — Using IndexedDB (версии базы, onupgradeneeded, транзакции, закрытие браузера, обёртки); MDN — Document: visibilitychange event. Объект сохранения, миграции и чек-лист — наш пример.
Читайте также: