Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Сховище браузера: cookie, LocalStorage, SessionStorage та IndexedDB». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Браузерне сховище** - це чотири API для зберігання даних на клієнті: cookies, LocalStorage, SessionStorage та IndexedDB. | Сховище | Розмір | Тривалість | На сервер? | |---|---|---|---| | Cookie | ~4KB | Expires/Max-Age | Так, кожен запит | | LocalStorage | ~5-10MB | До видалення | Ні | | SessionStorage | ~5-10MB | Закриття вкладки | Ні | | IndexedDB | 100MB+ | До видалення | Ні | **Правило вибору:** cookies для авторизації (з `HttpOnly`), LocalStorage для налаштувань, SessionStorage для стану вкладки, IndexedDB для офлайн і великих даних.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Браузерне сховище** (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 на 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:** ```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()` можуть конкурувати при навантаженні. Транзакція гарантує атомарність. ### Cookie для авторизації проти токена у LocalStorage ```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 після пентесту. Міграція займає один день. Спокій залишається набагато довше.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.