localStorage против sessionStorage против IndexedDB против Cookies — Хранение в браузере без 3-часового сожаления

Обновлено

Практическое руководство по выбору правильного хранилища браузера — с сравнительной таблицей, подробным объяснением спора о JWT и конкретными способами, как каждый из вариантов испортит ваш сон.

localStorage vs sessionStorage vs IndexedDB vs Cookies — Browser Storage Without the 3am Regret 1
Реклама · УДАЛИТЬ?

It’s 3am 😬. A user filed a bug: their shopping cart is empty. You open DevTools, click the Application tab, and stare at the sidebar. Где вы его поставили? Правильный ответ зависит от нескольких вопросов, которые большинство разработчиков рассматривают только после того, как жалоба подана.

Это не статья по определениям. Вы можете найти такие везде. Это часть, где мы говорим о том, что ломается, что следует использовать на практике, и почему спор о хранении JWT в localStorage в основном пропускает суть.

Краткое объяснение (т.е. таблица, которую вы сохраните)

localStoragesessionStorageIndexedDBФайлы cookie
ПersistenceПостоянноеТолько сессия вкладкиПостоянноеНастроимый (сессия или дата истечения)
Максимальный размер5–10 МБ5–10 МБГигабайты (квота браузера)~4 КБ на куку
Доступ к серверуНетНетНетДа — отправляется с каждым запросом
Асинхронный APIНет (блокирует основной поток)Нет (блокирует основной поток)Да (на основе Promise/события)Нет
Читаемо на JavaScriptДаДаДаТолько без флага HttpOnly
Доступ из Web WorkerНетНетДаНет
Общее для вкладокДаНет — каждая вкладка изолированаДаДа
Удаление в Safari ITPПосле 7 дней без взаимодействияПри закрытии вкладкиПосле 7 дней без взаимодействияЗависит от атрибута Expires

localStorage

Постоянное, синхронное, ограниченное по источнику. Работающий инструмент, который используется всеми и в половине случаев не подходит.

Что это на самом деле

localStorage хранит пары ключ-значение в виде строк. Это всё. Ограничение на хранение составляет 5 МБ в большинстве браузеров, 10 МБ в некоторых (Chrome даёт больше). Оно ограничено по источнику — протокол + домен + порт — поэтому http://example.com и https://example.com имеют отдельное хранилище. Оно сохраняется после закрытия вкладки, перезапуска браузера, за исключением случаев, когда пользователь очищает данные браузера или вы явно вызываете localStorage.clear().

// Read/write is synchronous — it happens right now, on the main thread
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'

// Storing objects? You're serializing manually.
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alex' }));
const user = JSON.parse(localStorage.getItem('user'));

Где оно ломается

  • Превышение квоты — выбрасывает синхронную DOMException: QuotaExceededError. Если вы не оборачиваете записи в try/catch, вы узнаете об этом через жалобу пользователя.
  • Приватный/инкогнито — браузеры предоставляют свежее, изолированное localStorage с более строгой (или нулевой) квотой. Firefox исторически выбрасывал ошибки квоты сразу. Никогда не полагайтесь на то, что localStorage доступен, без проверки его наличия.
  • Safari ITP — если пользователь не посещал ваш сайт в течение 7 дней, Safari может очистить localStorage. Это документированное поведение. Оно вас удивит в самый неподходящий момент.
  • XSS — любое содержимое в localStorage доступно любому JavaScript, работающему на вашем источнике. Если атакующий может вставить скрипт, он получает всё.

Используйте для

Предпочтения интерфейса (режим тёмной темы, состояние панели, язык), кэширование несущественных данных (время последнего получения, статическая конфигурация), любые данные, которые должны сохраняться после перезагрузки страницы, но не содержат токенов аутентификации или персональных данных. Если вы думаете о хранении JWT или API-ключа здесь — продолжайте читать.

sessionStorage

Всё, что делает localStorage, но с более коротким сроком годности и одним важным поведением изоляции, которое постоянно поражает людей.

Ловушка изоляции вкладок

sessionStorage — по вкладке, а не по браузеру. Открытие той же страницы в новой вкладке даёт вам полностью изолированный sessionStorage. Это нет очевидно для пользователей, и оно не будет очевидным для вас, пока кто-то не подаст жалобу о том, что данные формы исчезли, когда пользователь «неожиданно» открыл вторую вкладку.

Одна исключение: если пользователь открывает новую вкладку через window.open() или клик по ссылке, новая вкладка получает копию sessionStorage родительской вкладки в момент открытия. После этого вкладки изолированы. Это типичный крайний случай, который создаёт отличный вопрос на Stack Overflow в 3 часа ночи. Данные формы с несколькими шагами, данные одноуровневого ввода, любые данные, которые должны существовать на одной сессии и исчезать при закрытии вкладки. Это действительно полезно для процессов оформления заказа — вы не хотите, чтобы частичные данные оформления заказа оставались в предыдущей сессии. Не используйте его, если вам нужно хранить данные, которые должны быть доступны в разных вкладках или на отдельных страницах.

// Perfect for checkout flows — step data lives until the tab closes
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('cartSnapshot', JSON.stringify(cart));

// Cleared automatically when the tab closes — no cleanup code needed

Используйте для

Современный вариант. Асинхронный, транзакционный и способный хранить настоящие JavaScript-объекты — не просто сериализованные строки. Также имеет самую болезненную нативную API среди трёх, поэтому почти никто не использует его напрямую.

IndexedDB

IndexedDB — это полный хранилище ключ-значение с поддержкой индексов и запросов на основе курсора. Ограничения на хранение являются гибкими — браузеры позволяют использовать процент дискового пространства, обычно в гигабайтах на практике. Вы можете хранить структурированные объекты, Blobs, ArrayBuffers и Files без необходимости ручной сериализации. Оно доступно в Web Workers. Это то, что используют PWAs и приложения с возможностью работы без подключения к сети для хранения данных, которые невозможно было бы хранить в памяти.

Что это на самом деле

— то же самое, что и localStorage: после 7 дней без взаимодействия, Safari может удалить данные IndexedDB. Это уничтожило несколько PWAs до того, как Apple исправила поведение в новых версиях iOS. Если ваша целевая аудитория включает пользователей Safari на iOS, учитывайте это.

// Native IDB API — nobody writes this directly in production
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  db.createObjectStore('users', { keyPath: 'id' });
};
request.onsuccess = (event) => {
  const db = event.target.result;
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');
  store.put({ id: 1, name: 'Alex', avatar: someBlob });
};

// What you actually use — Dexie.js or the idb wrapper
import Dexie from 'dexie';
const db = new Dexie('myApp');
db.version(1).stores({ users: '++id, name, email' });
await db.users.add({ name: 'Alex', email: 'alex@example.com' });

Где оно ломается

Используйте для

Самый старый из четырёх. Единственный, который видит сервер. Единственный с ограничением в 4 КБ, которое обязательно ударит вас, если вы попытаетесь хранить JWT в нём.

Файлы cookie

Что делает куки отличными

Куки автоматически отправляются с каждым соответствующим HTTP-запросом. Это как преимущество, так и проблема. Это означает, что ваша сессионная кука достигает сервера без участия JavaScript — и это означает, что каждый запрос к

несёт перегрузку куки, независимо от того, хотите ли вы это. api.example.com Атрибуты, которые действительно важны:

— JavaScript не может прочитать эту куку. Атаки XSS не могут выгрузить её. Это базовый уровень для сессионных кук.

  • Любая кука, которая аутентифицирует пользователя, должна иметь — отправляется только по HTTPS. Без этого на производственном сайте вы отправляете аутентификационные куки по HTTP. Не делайте этого.
  • Безопасный SameSite=Strict
  • — кука отправляется только при запросе, инициированном вашей собственной доменом. Эффективная защита от CSRF, но она нарушает потокы OAuth. SameSite=Lax — разумный компромисс для большинства приложений. Expires / Max-Age
  • — без этих атрибутов это сессионная кука, которая удаляется при закрытии браузера. Установите явную дату истечения для поведения «запомнить меня». Если вы отладываете сложную строку кук, то
// Setting a cookie from JavaScript (no HttpOnly, obviously)
document.cookie = "theme=dark; Path=/; Max-Age=31536000; Secure; SameSite=Lax";

// Server-side (Node.js + Express) — where the real power is
res.cookie('sessionId', token, {
  httpOnly: true,   // JS cannot read this
  secure: true,     // HTTPS only
  sameSite: 'lax',  // balanced CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});

IO Tools’ Cookie Parser разделит её на читаемые пары ключ-значение — полезно, когда вы смотрите на заголовок длиной 400 символов и пытаетесь определить, какой атрибут неверен. Ограничение в 4 КБ Set-Cookie — это общая длина, включая имя, значение и все атрибуты. Типичный JWT составляет 400–800 байт. Тяжёлый с множеством утверждений — 1–2 КБ. У вас очень мало места.

Где оно ломается

  • SameSite=Strict нарушает OAuth — когда ваш IDP перенаправляет вас обратно в приложение после входа, это кросс-доменный запрос. SameSite=Strict означает, что сессионная кука не будет отправляться. Используйте Lax для любых потоков, проходящих через OAuth.
  • Порядок кук и совпадение путей — когда несколько кук имеют пересекающиеся пути, браузер отправляет их в неопределённом порядке. Не полагайтесь на приоритет.
  • Устаревание кук третьих сторон — Chrome удаляет куки третьих сторон. Зависимости кук между сайтами — это среднесрочная угроза.
  • Спор о хранении JWT в localStorage (настоящая уязвимость) Этот вопрос появляется в каждом обсуждении аутентификации, и большинство людей, обсуждающих его, говорят мимо друг друга.

Забота о хранении JWT в localStorage: если ваш сайт имеет уязвимость XSS, атакующий скрипт может напрямую прочитать токен и выгрузить его. Теперь атакующий имеет действительный токен, который он может использовать в любом месте, на любом устройстве, до истечения срока действия.

Аргумент в пользу кук с HttpOnly: JavaScript не может прочитать куку, поэтому XSS не может выгрузить её. Сессия всё ещё может использоваться в запросах (атакующий может делать запросы из браузера жертвы через XSS), но он не может украсть токен для использования в других местах. Это ограничивает радиус воздействия.

Честный взгляд:

корень проблемы — XSS, а не место хранения

. Если у вас есть XSS, кука с HttpOnly является значительно лучше — атакующий не может взять токен в другое место. Но устранение XSS — более значимая цель, достижимая с помощью строгого Content Security Policy, отсутствия неподконтрольных скриптов третьих сторон и правильной обработки вывода. Если вы создаёте SPA с строгим CSP и без неподконтрольных скриптов третьих сторон, localStorage, вероятно, подходит для JWT. Если вы запускаете сайт с Google Tag Manager, пикселями рекламы и десятками зависимостей из npm, куки с HttpOnly безопаснее, потому что ваша уязвимость XSS больше, чем вы думаете.При отладке проблем с JWT, полезен

JWT Decoder на IO Tools

— вставьте токен и посмотрите на содержимое и срок действия без написания кода. Полезно для подтверждения того, что токен всё ещё действителен, когда вы отслеживаете цепочку 401. Диаграмма принятия решений Перед тем как открыть вкладку Stack Overflow в 3 часа ночи, пройдите этот путь: проверщик истечения JWT Нужно ли серверу читать его?

→ Кука. Конец обсуждения.

Больше 5 МБ или нужно возможность запроса?

  1. → IndexedDB. Должно исчезать при закрытии вкладки?
  2. → sessionStorage. Всё остальное
  3. → localStorage, с соответствующим осторожным подходом к тому, что вы храните. Одно то, что все делают неправильно
  4. Хранилища API не надёжны во всех случаях для всех пользователей. Они могут быть очищены браузером, операционной системой, настройками конфиденциальности или пользователем. Любая архитектура, которая рассматривает клиентские хранилища как источник истины, а не как кэш, в конечном итоге потерпит неудачу у кого-то. Паттерн, который работает: рассматривайте хранилища браузера как оптимизацию производительности по сравнению с вашим настоящим источником истины (сервером). Активно кэшируйте, но проектируйте приложение так, чтобы оно восстанавливалось при пустом кэше. Сессии отладки в 3 часа ночи почти никогда не связаны с тем, какой API хранения вы использовали — они связаны с тем, что вы предполагали, что данные будут доступны, когда они не были.

Одно из вещей, которое все делают неправильно

API хранения данных не надежны для всех пользователей в любое время. Они могут быть очищены браузером, операционной системой, настройками конфиденциальности или пользователем. Любая архитектура, которая рассматривает клиентское хранилище как источник истины, а не как кэш, в конечном итоге не сработает для кого-то.

Паттерн, который работает: рассматривать хранилище браузера как оптимизацию производительности по сравнению с настоящим источником истины (сервером). Агрессивно кэшируйте данные, но проектируйте приложение так, чтобы оно восстанавливалось гладко при пустом кэше. Сессии отладки в 3 утра почти никогда не касаются того, какой API хранения вы использовали — они касаются предположения, что данные будут доступны, когда они на самом деле отсутствуют.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?