---
title: "Сохранения в браузерной игре: localStorage или IndexedDB, версии формата и защита от потери прогресса"
description: "Где хранить прогресс браузерной 2D-игры: ограничения localStorage (только строки, около 5 МиБ, QuotaExceededError), когда переходить на IndexedDB, как версионировать формат сохранения и писать миграции, когда сохранять (visibilitychange), почему браузер может удалить данные и что дают navigator.storage.persist() и estimate()."
slug: sohraneniya-v-brauzernoj-igre-localstorage-indexeddb
date: 2026-11-02
updated: 2026-11-02
scheduled_at: "2026-11-02T20:00:00+03:00"
rubric: logika-i-arhitektura
tags: ["sohraneniya", "localstorage", "indexeddb", "brauzernye-igry", "profi"]
reading_time: 9
draft: false
summary: "Для настроек и небольшого прогресса хватает localStorage: он хранит только строки, даёт около 5 МиБ на сайт и бросает QuotaExceededError при переполнении. Для больших сохранений, нескольких слотов и бинарных данных нужен IndexedDB. В любом случае в сохранении должно быть поле версии и цепочка миграций, сохранять надо при событии visibilitychange со статусом hidden, а не при закрытии страницы, и помнить, что браузер может удалить данные сайта, если не запрошено постоянное хранение."
---

## Коротко

Для настроек и небольшого прогресса браузерной игры хватает `localStorage`: он хранит только строки, около 5 МиБ на сайт и бросает `QuotaExceededError` при переполнении. Для больших сохранений, нескольких слотов и бинарных данных нужен IndexedDB. В обоих случаях в сохранении нужно поле версии с цепочкой миграций, сохранять надо при `visibilitychange` со статусом `hidden`, а не при закрытии страницы, и помнить, что без `navigator.storage.persist()` браузер вправе удалить данные сайта.

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

1. Что именно сохранять
2. localStorage: просто, но с ограничениями
3. IndexedDB: для больших и сложных сохранений
4. Версия формата и миграции
5. Когда сохранять
6. Почему сохранения пропадают
7. Чек-лист: надёжное сохранение за вечер
8. Когда вам это не подойдёт

## Что именно сохранять

Сохранять нужно минимальное состояние, из которого игру можно восстановить, а не весь мир целиком. Позиции частиц, кадры анимации и путь врага пересчитываются; номер уровня, контрольная точка, инвентарь, открытые двери и счёт — нет.

```js
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` приватного окна удаляются, когда закрыта последняя приватная вкладка.

```js
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 для сохранений игры

| | `localStorage` | IndexedDB |
|---|---|---|
| Что хранит | Строки | Объекты, массивы, бинарные данные |
| Вызовы | Синхронные | Асинхронные, через запросы и транзакции |
| Объём | До 5 МиБ на сайт | Общая квота хранилища сайта, обычно намного больше |
| Схема и версии | Нет, всё на вас | Номер версии базы, `onupgradeneeded` |
| Порог входа | Две строки кода | Заметно больше кода; часто берут обёртку |

Две детали из MDN, которые важны именно для сохранений:

- **Версия базы — целое число.** Значения `2.1` и `2.4` округляются до `2`, и обновление между ними не вызовет `onupgradeneeded`.
- **Одна логическая операция — одна транзакция.** MDN приводит пример: если очистить хранилище в одной транзакции, а записать новые данные в другой, закрытие браузера между ними оставит пустую базу. Удаление старого слота и запись нового должны идти в одной транзакции.

Если не хочется писать код запросов вручную, MDN перечисляет готовые обёртки; например, idb-keyval — хранилище «ключ — значение» на промисах размером около 600 байт.

## Версия формата и миграции

Поле `version` в каждом сохранении — единственный способ безопасно менять формат после релиза. Без него игра после обновления либо падает на старом сохранении, либо молча теряет прогресс. Рабочая схема — цепочка миграций, где каждая функция поднимает версию на 1:

```js
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).

```js
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 дней использования браузера.
- **Приватный режим.** Данные обычно удаляются по окончании приватного сеанса.

Таблица: Квоты хранилища по MDN

| Браузер | Квота сайта |
|---|---|
| Firefox | Меньшее из 10% диска и 10 ГиБ (на группу сайтов); с постоянным хранением — до 50% диска |
| Chromium-браузеры | До 60% диска |
| Safari (macOS 14, iOS 17 и новее) | Около 60% диска для браузеров, около 15% для встроенных WebView |

Что с этим делать:

```js
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-игру так, чтобы прогресс переживал обновление игры, переполнение хранилища и закрытие вкладки. Проверяемый результат — пять ручных тестов ниже проходят без потери прогресса.

1. **Опишите объект сохранения** с полем `version`, без вычисляемых данных.
2. **Разделите ключи:** `settings`, `save-1`, `save-2`, `save-auto`.
3. **Оберните запись в `try...catch`** и покажите игроку понятное сообщение при ошибке.
4. **Напишите цепочку миграций** и тест на загрузку сохранения каждой прошлой версии.
5. **Сохраняйте на контрольных точках и при `visibilitychange` → `hidden`.**
6. **Запросите `persist()`** после первой ручной записи.
7. **Добавьте экспорт и импорт** в файл.
8. **Проверьте вручную:** закрыть вкладку посреди уровня; переключиться на другое приложение на телефоне; загрузить сохранение прошлой версии; испортить JSON в DevTools; заполнить хранилище до ошибки.

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

Хранение в браузере подходит для одиночной игры на одном устройстве. Случаи, когда нужно другое:

- **Прогресс на нескольких устройствах.** Браузерное хранилище привязано к браузеру и сайту; для синхронизации нужен сервер и учётная запись.
- **Соревновательная игра с таблицей рекордов.** Игрок может отредактировать локальное сохранение в DevTools; результаты, которые важны другим, проверяет сервер.
- **Игра публикуется на внешней площадке во фрейме.** Доступ к хранилищу во встроенном фрейме зависит от политики браузера и площадки; проверяйте на каждой площадке отдельно.
- **Сохранение сотен мегабайт.** Ресурсы (графику, звук) кешируют отдельно, а в сохранение пишут только состояние игры.

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

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

**Читайте также:**
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
- [Конечный автомат персонажа в платформере](/blog/konechnyj-avtomat-personazha-platformera)
- [Процедурная генерация подземелий](/blog/procedurnaya-generaciya-podzemelij-bsp-i-kletochnye-avtomaty)
