localStorage и sessionStorage в JavaScript: что хранить в браузере и как не сломать приложение

Когда приложению нужно запомнить что-то прямо в браузере без сервера, разработчик обычно смотрит на localStorage и sessionStorage. Оба API простые, широко поддерживаются и хорошо подходят для небольших клиентских данных. Но именно из-за этой простоты их часто используют слишком широко: складывают туда всё подряд, читают без проверок и получают странные баги после первого же обновления схемы данных или открытия второй вкладки.
Практичнее относиться к Web Storage не как к «мини-базе данных», а как к синхронному хранилищу строк для небольшого объёма неопасных данных, которые легко восстановить.
Когда Web Storage нужен, а когда лучше выбрать что-то другое
Web Storage хранит пары ключ-значение в браузере. Важное отличие от cookies в том, что эти данные не отправляются автоматически с каждым HTTP-запросом. Поэтому для чисто клиентских задач он обычно удобнее: тема интерфейса, выбранный язык, сортировка таблицы, открытые панели, черновик формы.
Cookies полезнее там, где значение действительно нужно серверу. А localStorage и sessionStorage — там, где состояние в первую очередь нужно JavaScript-коду на странице.
Но есть и ограничение: операции Web Storage синхронные. Пока выполняется getItem, setItem или removeItem, основной поток JavaScript занят. Для одной-двух небольших записей это обычно незаметно. Для больших JSON-объектов, частого автосохранения и тяжёлого парсинга — уже источник подвисаний интерфейса. Если данных много или они часто меняются, лучше рассматривать асинхронные механизмы вроде IndexedDB.
localStorage и sessionStorage: разница, которая влияет на поведение интерфейса
| Свойство | localStorage | sessionStorage |
|---|---|---|
| Срок жизни | Сохраняется между сессиями браузера | Живёт, пока существует вкладка или окно |
| Область видимости | Общий для документов одного origin | Разделён по origin и по вкладке |
| Синхронизация | Изменения можно отследить в других вкладках того же origin через storage | Между обычными вкладками не синхронизируется |
| Типичные сценарии | Тема, язык, постоянные настройки интерфейса | Черновик формы, состояние пошагового мастера, временный выбор в пределах одной вкладки |
Оба хранилища разделяются по origin: схема, хост и порт образуют границу доступа. Поэтому http://example.com и https://example.com — это разные хранилища. Для документов, открытых по file:, поведение браузеров не стандартизовано полностью, так что на него нельзя опираться в рабочем приложении.
У sessionStorage есть практическая особенность: оно переживает перезагрузку страницы и восстановление вкладки, но исчезает после закрытия самой вкладки или окна. Если новая вкладка была открыта из текущей, она может получить начальную копию sessionStorage, однако дальше состояния расходятся и не обновляют друг друга.
В приватном режиме API обычно доступен, но долговечность localStorage там ограничена: данные удаляются после закрытия приватных вкладок.
Что на самом деле хранится внутри
И ключи, и значения в Web Storage — это строки. Даже число 42 там будет лежать как строка. Поэтому для простых значений полезно приводить тип явно:
localStorage.setItem('fontSize', String(16));
const fontSize = Number(localStorage.getItem('fontSize') ?? 14);
Для массивов и объектов применяют JSON:
const settings = {
theme: 'dark',
sidebarOpened: true,
tags: ['js', 'storage']
};
localStorage.setItem('ui:settings', JSON.stringify(settings));
const raw = localStorage.getItem('ui:settings');
const parsed = raw ? JSON.parse(raw) : null;
Обычно ошибка возникает не в момент записи, а позже: ключа нет, JSON повреждён, структура устарела, данные вручную изменили в DevTools. Поэтому в реальном приложении почти всегда нужен защитный слой вокруг чтения и записи.
Безопасное чтение и запись: отсутствие ключа, плохой JSON, запрет на доступ
Доступ к самому объекту хранилища тоже может завершиться исключением. Спецификация и документация описывают SecurityError, если источник страницы неподходящий или политика браузера запрещает сохранять данные. Поэтому лучше сначала получить хранилище, а затем работать с ним через обёртки.
function getStorage(type = 'local') {
try {
return type === 'local' ? window.localStorage : window.sessionStorage;
} catch (error) {
console.warn('Хранилище недоступно', error);
return null;
}
}
function loadJSON(storage, key, fallback) {
if (!storage) return fallback;
try {
const raw = storage.getItem(key);
if (raw === null) return fallback;
return JSON.parse(raw);
} catch (error) {
console.warn('Не удалось прочитать ключ', key, error);
return fallback;
}
}
function saveJSON(storage, key, value) {
if (!storage) return false;
try {
storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.warn('Не удалось сохранить ключ', key, error);
return false;
}
}
Эти проверки закрывают несколько типичных проблем:
- Ключ отсутствует.
getItemвернётnull, а не пустую строку. - JSON повреждён.
JSON.parseвыбросит исключение. - Доступ к хранилищу запрещён. Тогда ошибку можно пережить без падения интерфейса.
- Лимит превышен. Запись может завершиться ошибкой, в том числе
QuotaExceededError.
Полезная привычка — использовать методы getItem и setItem, а не обращаться к ключам как к свойствам объекта. У методов более предсказуемое поведение: отсутствующий ключ даёт null, а не undefined.
Что делать, если формат данных поменялся
Когда структура объекта растёт, без версии схемы легко сломать старые сохранения. Проще сразу хранить версию и при чтении мигрировать старые данные.
function getSettings(storage) {
const fallback = { version: 2, theme: 'light', density: 'comfortable' };
const data = loadJSON(storage, 'ui:settings', fallback);
if (!data || typeof data !== 'object') return fallback;
if (data.version === 1) {
return {
version: 2,
theme: data.theme ?? 'light',
density: 'comfortable'
};
}
if (data.version !== 2) return fallback;
return { ...fallback, ...data };
}
Так приложение не падает после релиза, в котором вы добавили новое поле и изменили старую форму данных.
Ограничения API, о которых забывают чаще всего
- Объём ограничен. MDN указывает предел до 5 MiB для localStorage и до 5 MiB для sessionStorage на origin. Не путайте это с зависящими от браузера квотами IndexedDB и других хранилищ. Обрабатывайте отказ записи через try/catch: доступ к хранилищу или место могут быть недоступны.
- Операции синхронные. Большие записи и частое чтение ухудшают отзывчивость интерфейса.
- Нет гарантированной межвкладочной блокировки. Две вкладки могут почти одновременно прочитать старое значение и перезаписать друг друга.
- Порядок обхода ключей нельзя считать стабильным. Если порядок важен, храните его явно.
- Доступ зависит от контекста. Например, у стороннего кода в
iframeдоступ может быть ограничен настройками приватности браузера.
Из-за отсутствия блокировок localStorage плохо подходит для схемы «прочитал счётчик → увеличил → записал» в качестве генератора уникальных идентификаторов. Если две вкладки сделают это одновременно, обе могут получить один и тот же результат.
Синхронизация между вкладками через событие storage
Если одна вкладка изменила localStorage, другие вкладки того же origin могут узнать об этом через событие storage. Это удобно для вещей, которые должны сразу отражаться во всём интерфейсе: тема, язык, выбранный режим отображения.
window.addEventListener('storage', (event) => {
if (event.key !== 'ui:settings' || event.newValue === null) return;
try {
const settings = JSON.parse(event.newValue);
applySettings(settings);
} catch (error) {
console.warn('Некорректные настройки из другой вкладки', error);
}
});
Здесь важны два правила:
- Событие не приходит в ту же вкладку, которая вызвала изменение. Текущую страницу нужно обновлять сразу после
setItem. - Для
sessionStorageэто не способ синхронизации между обычными вкладками. Там событие относится только к документам в том же верхнем контексте, например кiframeв текущей вкладке.
Какие данные лучше не класть в Web Storage
Из спецификации прямо следует, что постоянное клиентское хранилище нужно считать потенциально чувствительным. Поэтому его лучше не использовать для данных, утечка или подмена которых уже сама по себе проблема.
- Конфиденциальные пользовательские данные. Например, переписка, содержимое документов, медицинские сведения, данные календаря.
- Данные, связанные с управлением сессией. Спецификация отдельно рассматривает риск подмены идентификатора сессии как опасный сценарий.
- Долговечные уникальные идентификаторы для отслеживания. Спецификация описывает использование persistent storage для user tracking как риск приватности.
Хорошие кандидаты для хранения — тема интерфейса, язык, локальные фильтры, сортировка, ширина панелей, неважные черновики и другие данные, которые легко восстановить или сбросить без ущерба для пользователя.
Небольшой прикладной пример: настройки интерфейса и черновик формы
Практичное разделение обычно такое:
localStorage— для долговечных настроек интерфейса.sessionStorage— для временного черновика в пределах одной вкладки.
const local = getStorage('local');
const session = getStorage('session');
const SETTINGS_KEY = 'ui:settings';
const DRAFT_KEY = 'form:draft';
function saveSettings(nextSettings) {
const settings = { theme: 'dark', compact: false, ...nextSettings };
saveJSON(local, SETTINGS_KEY, settings);
applySettings(settings);
}
function restoreSettings() {
const settings = loadJSON(local, SETTINGS_KEY, {
theme: 'light',
compact: false
});
applySettings(settings);
}
function saveDraft(formData) {
saveJSON(session, DRAFT_KEY, formData);
}
function restoreDraft() {
return loadJSON(session, DRAFT_KEY, {
title: '',
comment: ''
});
}
В такой схеме пользователь после перезапуска браузера сохраняет тему оформления, а случайное обновление страницы не уничтожает набранный текст. При этом черновик не смешивается с черновиком в другой вкладке.
Учебный антипример: если сохранять черновик формы заказа в localStorage под одним ключом, а пользователь откроет два заказа в разных вкладках, вкладки начнут перетирать данные друг друга. Для независимых параллельных сценариев лучше подходит sessionStorage или ключи с явным идентификатором сущности, например draft:order:123.
Короткое правило выбора
Если данные должны пережить закрытие браузера и быть общими для вкладок одного сайта, выбирайте localStorage. Если данные нужны только в рамках одной вкладки до её закрытия, выбирайте sessionStorage. Если данных много, они часто меняются или важна надёжная работа при конкурентных изменениях, Web Storage становится тесным.
Самая полезная стратегия проста: хранить в браузере только небольшие и легко восстанавливаемые данные, читать их через защитные обёртки и заранее учитывать, что формат когда-нибудь изменится. Тогда localStorage и sessionStorage будут удобным инструментом интерфейса, а не источником трудноуловимых ошибок.




