Skip to main content

Сховище браузера: cookie, LocalStorage, SessionStorage та IndexedDB

Браузерне сховище (browser storage) - це чотири API браузера (cookies, LocalStorage, SessionStorage, IndexedDB), які дозволяють зберігати дані на стороні клієнта. Кожен має свої ліміти розміру, правила тривалості та одну ключову відмінність: чи летять дані на сервер автоматично, чи залишаються локально.

Теорія

TL;DR

  • Cookies - як листівки: маленькі, проштамповані та відправляються з кожним HTTP-запитом, хочеш ти цього чи ні
  • LocalStorage і SessionStorage - як блокноти в ящику столу: Local живе до видалення, Session зникає разом із вкладкою
  • IndexedDB - картотека: асинхронна, з індексами, для великих структурованих даних
  • Правило вибору: серверу потрібні дані? Cookie. Стан тільки для однієї вкладки? SessionStorage. Постійний key-value? LocalStorage. Офлайн-застосунок з реальними даними? IndexedDB.

Швидкий приклад

js
// Cookies автоматично летять з кожним HTTP-запитом document.cookie = "userId=123; path=/; max-age=3600"; // LocalStorage: живе між вкладками і перезапусками, ~5-10MB localStorage.setItem("theme", "dark"); console.log(localStorage.getItem("theme")); // "dark" // SessionStorage: тільки поточна вкладка, очищається при закритті sessionStorage.setItem("cart", JSON.stringify({ id: 1 })); // Відкрий Network у DevTools: тільки cookie видно в заголовках запиту // Cookie: userId=123

Тільки cookies з'являються в заголовках запиту. Три інших по мережі не передаються.

Головна різниця

Cookies автоматично чіпляються до кожного HTTP-запиту. Це і їхня сила, і їхня ціна: серверна авторизація без додаткового коду, але обмеження ~4KB на cookie і постійний трафік. LocalStorage, SessionStorage та IndexedDB мережу не торкаються. Вони живуть в ізольованому сховищі на диску, прив'язаному до origin, що робить читання швидким і тримає дані подалі від сервера, якщо ти їх явно не відправляєш.

Коли що використовувати

  • Токени авторизації та сесії: cookies з прапорами HttpOnly і Secure. JS не може прочитати HttpOnly-cookie, що закриває XSS-атаки на токени.
  • Налаштування користувача (тема, мова): LocalStorage. Зберігається між сесіями, синхронізується між вкладками через подію storage.
  • Чернетка форми в одній вкладці: SessionStorage. Очищається при закритті, ніякого ручного прибирання.
  • Дані офлайн-застосунку (повідомлення, задачі, каталог): IndexedDB. Підтримує транзакції, індекси, курсори та бінарні дані.
  • Кешування зображень або файлів на клієнті: теж IndexedDB. Єдиний варіант для бінарних даних у великих обсягах.

Таблиця порівняння

СховищеЛіміт розміруОбласть видимостіТривалістьВідправляється на сервер?API
Cookie~4KB на cookieДомен + шляхExpires / Max-AgeТак, кожен запитdocument.cookie
LocalStorage~5-10MB на originOriginДо видаленняНіlocalStorage
SessionStorage~5-10MB на originOrigin + вкладкаЗакриття вкладкиНіsessionStorage
IndexedDB100MB+ (квота диска)OriginДо видаленняНіAsync DB API

Як браузер це організовує всередині

LocalStorage і SessionStorage зберігаються як SQLite-файли, ключовані по origin. Rendering engine (Blink у Chrome, Gecko у Firefox) стежить за квотою ~10MB. Cookies живуть у cookie jar для кожного домену: мережевий стек браузера читає їх перед кожним fetch або XHR і серіалізує в заголовок Cookie:. IndexedDB у Chromium використовує LevelDB і має динамічну квоту, прив'язану до вільного місця на диску - в деяких браузерах до 50% від загального обсягу.

Типові помилки

Зберігати JWT у LocalStorage:

js
// Неправильно: будь-який скрипт на сторінці може це прочитати localStorage.setItem("authToken", "eyJhbGci..."); // Правильно: встановлюється через заголовок у відповіді сервера // Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict

LocalStorage доступний будь-якому JS на сторінці. Одна XSS-вразливість - і токен потрапляє до зловмисника.

Вважати, що LocalStorage оновлюється одразу в інших вкладках:

js
// Вкладка A записує: localStorage.setItem("count", "1"); // Вкладка B відразу читає стару версію // Потрібна подія storage: window.addEventListener("storage", e => { console.log("Оновлено в іншій вкладці:", e.newValue); });

Подія storage спрацьовує в інших вкладках, але не в тій, що записала. SessionStorage цю подію не генерує взагалі.

Не збільшувати версію IndexedDB при зміні схеми:

js
// Неправильно: версія залишається 1 після додавання нового object store indexedDB.open("myDB", 1); // onupgradeneeded більше не спрацює // Правильно: indexedDB.open("myDB", 2); // запустить onupgradeneeded для міграції

Колбек onupgradeneeded викликається тільки коли версія зростає.

Використовувати SessionStorage для багатовкладкових сценаріїв: Кожна вкладка отримує свій ізольований SessionStorage. Якщо користувач відкриє другу вкладку під час оформлення замовлення, кошик з першої вкладки там не з'явиться. Для стану між вкладками використовуй LocalStorage або BroadcastChannel API.

Писати багато cookies з клієнта: Парсинг document.cookie синхронний. На сторінках з 50+ cookies маніпуляції з рядком накопичуються. Краще встановлювати cookies через Set-Cookie на сервері, а на клієнті тільки читати.

Де це використовується

  • next-themes (React/Next.js): зберігає активну тему в LocalStorage, зчитує при першому рендері щоб уникнути спалаху неправильної теми
  • Redux Persist: серіалізує стан Redux у LocalStorage або IndexedDB для офлайн-застосунків
  • Auth0 / Okta: HttpOnly-cookies для JWT, LocalStorage тільки для не чутливих метаданих
  • PouchDB: обгортає IndexedDB щоб дати CouchDB-подібний API з синхронізацією на сервер при появі мережі
  • Shopify Storefront: SessionStorage для гостьового кошика в одній вкладці, IndexedDB для офлайн-каталогу

Питання на співбесіді

Q: У чому різниця між зберіганням JWT у LocalStorage і cookie?
A: Cookie з прапорами HttpOnly і Secure взагалі недоступний для JavaScript, тому XSS не може його прочитати. LocalStorage відкритий для будь-якого JS. Короткий термін дії токена з refresh-токеном зменшує ризик, але HttpOnly cookie - надійніша відправна точка за замовчуванням.

Q: Як працює квота в IndexedDB порівняно з LocalStorage?
A: LocalStorage має жорсткий ліміт ~5-10MB на origin. Квота IndexedDB динамічна: браузери зазвичай дозволяють до 50% вільного місця на диску, але сховище може бути примусово очищене. navigator.storage.persist() запрошує постійне зберігання, navigator.storage.estimate() показує поточне використання.

Q: Коли спрацьовує подія storage і для яких сховищ?
A: Вона спрацьовує в інших вкладках та вікнах одного origin при зміні LocalStorage. У вкладці, яка записала, вона не спрацьовує. Для SessionStorage ця подія не генерується взагалі, бо SessionStorage ізольований між вкладками за своїм призначенням.

Q: Як перенести дані з LocalStorage в IndexedDB без простою?
A: Зчитай ключі LocalStorage під час ініціалізації застосунку, запиши їх у транзакцію IndexedDB у фоні, потім видали LocalStorage-записи. Зберігай версійний прапор на кшталт migratedV2: true, щоб міграція виконалась один раз. Користувачі в середині сесії нічого не помітять.

Q (senior): Як поводиться кожне сховище в режимі інкогніто і що повертає navigator.storage.estimate()?
A: Всі сховища працюють під час сесії, але знищуються при закритті вікна. Cookies без Expires або Max-Age стають сесійними та зникають. У деяких браузерах квота IndexedDB в режимі інкогніто менша за звичайну. navigator.storage.estimate() повертає значення, але вони відображають ефемерне сховище в пам'яті, а не реальний диск. navigator.storage.persist() в режимі інкогніто завжди повертає false.

Приклади

Збереження теми в React через LocalStorage

js
// Зберігає вибір теми між перезавантаженнями без запитів на сервер const useTheme = () => { const [theme, setTheme] = useState( () => localStorage.getItem("theme") || "light" // ліниве зчитування, щоб уникнути SSR-проблем ); useEffect(() => { localStorage.setItem("theme", theme); document.body.className = theme; }, [theme]); return { theme, toggleTheme: () => setTheme(t => t === "light" ? "dark" : "light"), }; }; // Після перезавантаження: localStorage.getItem("theme") → "dark", мережевого запиту немає

Ліниве ініціалізування в useState запускається один раз. Без нього LocalStorage читатиметься на кожному рендері, що ламає серверний рендеринг.

IndexedDB з транзакціями (приклад кошика)

js
const request = indexedDB.open("cartDB", 1); request.onupgradeneeded = e => { const db = e.target.result; db.createObjectStore("items", { keyPath: "id", autoIncrement: true }); }; request.onsuccess = e => { const db = e.target.result; const tx = db.transaction("items", "readwrite"); const store = tx.objectStore("items"); // Обидва записи в одній транзакції: або обидва збережуться, або обидва відкочуються store.add({ item: "apple", qty: 2 }); store.add({ item: "banana", qty: 1 }); tx.oncomplete = () => console.log("Кошик збережено"); tx.onerror = () => console.error("Помилка запису, можливо диск повний"); };

Без транзакції два послідовні store.add() можуть конкурувати при навантаженні. Транзакція гарантує атомарність.

js
// Небезпечно: токен доступний будь-якому JS на сторінці localStorage.setItem("token", "eyJhbGci..."); // Безпечно: встановлюється сервером, JS його не бачить // Сервер надсилає: Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict // Клієнт просто звертається до захищених ресурсів const res = await fetch("/api/me"); // Заголовок Cookie додається автоматично const user = await res.json(); console.log(user.name); // токен жодного разу не торкався клієнтського JS

Я бачив команди, які переходили з LocalStorage-токенів на HttpOnly-cookies після пентесту. Міграція займає один день. Спокій залишається набагато довше.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?