Сховище браузера: 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.
Швидкий приклад
// 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 на origin | Origin | До видалення | Ні | localStorage |
| SessionStorage | ~5-10MB на origin | Origin + вкладка | Закриття вкладки | Ні | sessionStorage |
| IndexedDB | 100MB+ (квота диска) | 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:
// Неправильно: будь-який скрипт на сторінці може це прочитати
localStorage.setItem("authToken", "eyJhbGci...");
// Правильно: встановлюється через заголовок у відповіді сервера
// Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=StrictLocalStorage доступний будь-якому JS на сторінці. Одна XSS-вразливість - і токен потрапляє до зловмисника.
Вважати, що LocalStorage оновлюється одразу в інших вкладках:
// Вкладка A записує:
localStorage.setItem("count", "1");
// Вкладка B відразу читає стару версію
// Потрібна подія storage:
window.addEventListener("storage", e => {
console.log("Оновлено в іншій вкладці:", e.newValue);
});Подія storage спрацьовує в інших вкладках, але не в тій, що записала. SessionStorage цю подію не генерує взагалі.
Не збільшувати версію IndexedDB при зміні схеми:
// Неправильно: версія залишається 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
// Зберігає вибір теми між перезавантаженнями без запитів на сервер
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 з транзакціями (приклад кошика)
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() можуть конкурувати при навантаженні. Транзакція гарантує атомарність.
Cookie для авторизації проти токена у LocalStorage
// Небезпечно: токен доступний будь-якому 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 після пентесту. Міграція займає один день. Спокій залишається набагато довше.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.