
До недавнего времени программный доступ к куки в браузере осуществлялся через API document.cookie
— простой строковый геттер/сеттер. Для получения одного файла куки приходилось разбирать всю строку вручную и преобразовывать ее в удобный формат. А чтобы записать куки, нужно было сначала сформировать структурированные данные, затем сериализовать их в строку и только после этого присвоить значение document.cookie
. Разработчики часто используют популярные библиотеки, например js-cookie, которые делают работу с куки гораздо удобнее.
Однако сторонние библиотеки негативно влияют на производительность. Разбор строки и преобразование данных требуют ресурсов, и, насколько я понимаю, чем больше куки-строка, тем выше эти затраты. Кроме того, доступ к document.cookie
синхронный. Если он попадает в "горячий" цикл (hot loop), это может негативно сказаться на отзывчивости основного потока (main thread).
Наконец, с document.cookie
невозможно использовать куки-строку как единственный источник актуального состояния, потому что она не поддерживает событие change
. Это становится особенно важно в реальных приложениях, где через куки может осуществляться синхронизация различных системных и пользовательских настроек на уровне всего приложения.
Но все это в прошлом. Сейчас в браузерах появился новый API — Cookie Store, который позволяет работать с куки в структурированном виде и с высокой производительностью. Говорю "новый", хотя на самом деле он уже четыре года доступен в браузерах на базе Chromium. Однако теперь, с выходом Safari 18.4 в конце марта 2025 года и добавлением поддержки этого API, у нас появилось как минимум два браузера, активно развивающих эту технологию. Теперь Cookie Store
можно смело применять как прогрессивное улучшение.
Похоже, поддержку добавили и Firefox 137, но в моих тестах (на версии 137.0.1 для aarch64) cookieStore
пока не заработал. Возможно, полноценная поддержка появится в Firefox 138.
Вот таблица поддержки на caniuse:

Я вижу три ключевых преимущества этого API:
- Производительность — асинхронный доступ, меньше операций сериализации и десериализации, более компактный размер сборки
- Управление состоянием напрямую через куки-хранилище — это позволяет избежать проблем с синхронизацией
- Стандартизация — упрощает создание общих SDK для работы с куки с минимальными различиями между библиотеками.
Рассмотрим каждое из этих преимуществ подробнее.
❯ Руководство по использованию
Полную документацию можно найти на MDN, но вот простой пример того, как работать с cookieStore:
async function setDarkModePreference() {
try {
await cookieStore.set('theme', 'dark');
} catch (err) {
// Одно из преимуществ Cookie Store API - мгновенная обработка ошибок,
// без необходимости сначала записывать, а потом читать данные для проверки
}
}
async function resetThemePreference() {
return cookieStore.delete('theme');
}
Обратите внимание: все методы интерфейса CookieStore
являются асинхронными.
❯ 1. Производительность
Работать с интерфейсом document.cookie
, основанным на строках, неудобно — приходится постоянно преобразовывать данные из строки в структурированный формат и обратно. Разбор куки-строки — задача не из простых, но можно довериться тому, что популярные библиотеки экосистемы строго следуют спецификации и хорошо протестированы.
Некоторые библиотеки реализуют оптимизации, например, прекращают разбор строки, как только находят нужную часть куки. Лично мне нравится библиотека js-cookie
— к тому же ее исходный код приятно читать.
Тем не менее, даже с оптимизациями document.cookie
остается синхронным. А это значит, что работа с ним может негативно повлиять на отзывчивость главного потока.
Кроме того, стоит учитывать влияние на размер итоговой сборки. Хотя сама js-cookie
довольно легкая, всего 780 байт в сжатом виде (min+gzip).
В целом, нативный асинхронный способ работы с куки — это шаг вперед в плане производительности, даже по сравнению с хорошо оптимизированными библиотеками.
❯ 2. Управление состоянием через куки
Хотя асинхронный доступ и уменьшение размера сборки мне уже нравятся, еще более важным для меня является событие change
, которое предоставляет Cookie Store
:
cookieStore.addEventListener('change', (event) => {
console.log(event);
});
Вот ссылка на CookieChangeEvent
.
Представьте, что у нас есть веб-приложение, которое позволяет пользователям сохранять предпочтения по теме сайта: светлая (light
), темная (dark
) и авто (auto
) (когда тема зависит от настроек системы). Когда пользователь выбирает свое предпочтение, мы хотим сохранить это в куки, чтобы при последующих визитах тема загружалась по умолчанию. В то же время разные части приложения могут обращаться к текущей теме через JS. Как гарантировать, что информация будет поступать из одного источника?
Поскольку разные части приложения заинтересованы в информации о теме, это состояние должно храниться в одном месте. Идеально, если бы этим управлял браузер, так как именно он сохраняет куки. Однако API document.cookie
не поддерживает событие change
, и поэтому любая часть интерфейса, не меняющая куки, не сможет на это отреагировать.
Оптимальной альтернативой будет самостоятельное управление копией состояния и внесение изменений через единственный сеттер. В терминах React это может быть Context, который хранит все значения куки или конкретное значение. Вот пример реализации куки для темы:
import Cookie from 'js-cookie';
import type { PropsWithChildren } from 'react';
import {
useCallback,
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
const THEME_COOKIE_NAME = 'theme';
type Theme = 'light' | 'dark' | 'auto';
// Как кортеж setState
type ContextValue = [Theme, (theme: Theme) => void];
const ThemeContext = createContext<ContextValue | undefined>(undefined);
export function useThemeState() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useThemeState must be used in a ThemeManager tree');
}
return ctx;
}
export function ThemeManager({ children }: PropsWithChildren) {
const [theme, setStoredTheme] = useState<Theme>();
useEffect(() => {
// Обратите внимание: это неполный пример;
// на практике может быть использован одноразовый useSyncExternalStore,
// и необходимо выполнять валидацию
setStoredTheme(Cookie.get(THEME_COOKIE_NAME));
}, [setStoredTheme]);
const setTheme = useCallback(
(theme: Theme) => {
Cookie.set(THEME_COOKIE_NAME, theme);
setStoredTheme(theme);
},
[setStoredTheme]
);
const memoValue = useMemo(() => [theme, setTheme], [theme, setTheme]);
return (
<ThemeContext.Provider value={memoValue}>{children}</ThemeContext.Provider>
);
}
Без учета некоторых опущенных для простоты деталей реализации, это работает. Я не считаю дублирование состояния большой проблемой (в конце концов, состояние React все равно должно где-то храниться), но иногда меня беспокоит возможность рассинхронизации состояния.
Теперь пример можно усовершенствовать за счет использования события change
. Поскольку существует полный набор API для чтения, записи и получения уведомлений об изменениях, мы можем воспользоваться встроенными механизмами синхронизации в React:
import { useCallback, useEffect, useState } from 'react';
const THEME_COOKIE_NAME = 'theme';
type Theme = 'light' | 'dark' | 'auto';
export function useThemeState() {
// Снимок (snapshot) для SSR/гидратации оставлен как упражнение для читателя
const [theme, setStoredTheme] = useState<Theme | undefined>(undefined);
const onChange = useCallback(
(ev: CookieChangeEvent) => {
const deleted = ev.deleted.find((c) => c.name === THEME_COOKIE_NAME);
if (deleted) {
setStoredTheme(undefined);
return;
}
const changed = ev.changed.find((c) => c.name === THEME_COOKIE_NAME);
if (changed) {
setStoredTheme(changed.value);
return;
}
},
[setStoredTheme]
);
useEffect(() => {
cookieStore.addEventListener('change', onChange);
return () => {
cookieStore.removeEventListener('change', onChange);
};
}, [onChange]);
const setTheme = useCallback((theme: Theme) => {
cookieStore.set(THEME_COOKIE_NAME, theme);
}, []);
return [theme, setTheme];
}
Это позволяет избавиться от контекста и использовать cookieStore
напрямую. Изначально я хотел использовать useSyncExternalStore, так как это позволило бы избавиться от лишнего useState
. Однако useSyncExternalStore
не извлекает новое значение из события change
, а получает его через отдельный вызов cookieStore.get
. Но поскольку этот вызов асинхронный, он не соответствует требованиям React синхронности к функции снимка.
Один безумный вариант — использовать синхронныйdocument.cookie
для снимка вuseSyncExternalStore
, но мне не очень нравится смешивать два API, учитывая вопросы совместимости и производительности.
❯ Применение CookieStore
Наверняка вы задаетесь вопросом, как использовать этот API, если он пока поддерживается не во всех браузерах.
Как показано в примере с темой, скорее всего, ваше приложение уже имеет централизованное место для хранения состояния куки, и изменения обычно происходят из-за действий пользователя. В таких случаях можно использовать API CookieStore
, если оно доступно, а в остальных — вернуться к привычной реализации на JS (например, js-cookie
) с дополнительным управлением состоянием.
// Можно предположить, что поддержка API CookieStore
// является свойством окружения, и нам не нужно реагировать на это динамически
const supportsCookieStore = 'cookieStore' in window;
const ThemeManagerUserland = ({ children }) => {
// js-cookie
return <ThemeProvider value={memoValue}>{children}</ThemeProvider>;
};
const ThemeManagerNative = ({ children }) => {
// cookieStore
return <ThemeProvider value={memoValue}>{children}</ThemeProvider>;
};
export const ThemeManager = supportsCookieStore
? ThemeManagerNative
: ThemeManagerUserland;
Можно создать два разных хука или поместить логику внутрь, вместо того, чтобы разделять провайдеры. Все зависит от ваших личных предпочтений и особенностей конкретного приложения.
Для разового изменения куки можно использовать стандартную реализацию, но имейте в виду, что функции нужно будет сделать асинхронными, так как интерфейс CookieStore
полностью асинхронный.
import { setCookie as setCookieFallback } from 'my-favorite-cookie-library';
export async function setCookie(cookie: string, value: string) {
if ('cookieStore' in window) {
return cookieStore.set(cookie, value);
}
setCookieFallback(cookie, value);
return;
}
Можно настроить этот интерфейс по своему усмотрению; я использовал (string, string)
для простоты. API Cookie Store
также предоставляет более структурированный способ установки куки, чем просто ключ и значение, с помощью дополнительного аргумента:
cookieStore.set({
name: 'theme',
value: 'dark',
path: '/',
partitioned: false,
sameSite: 'strict',
});
На данный момент для cookieStore
нет типов TypeScript. TypeScript добавляет API в общие типы DOM, только когда оно поддерживается всеми основными браузерами, что, на мой взгляд, вполне разумно. С учетом того, что Chrome, Safari и Firefox теперь поддерживают этот API, можно ожидать добавления типов в ближайшее время.
❯ 3. Стандартизация
Когда речь идет о Web API, стандарт — это всегда хорошо! Например, на одном домене может быть несколько сайтов, работающих с одинаковыми куки. В таком случае можно создать SDK для централизованного управления этой логикой, например, для постоянной валидации значений куки.
При использовании сторонней библиотеки для этой задачи необходимо решать, как будет происходить кодирование значений (или полагаться на библиотеку, которая сделает это за вас). Это, в свою очередь, может повлиять на совместимость с другими библиотеками. Например, js-cookie
уточняет следующее:
Этот проект соответствует RFC 6265. Все специальные символы, которые не разрешены в имени или значении куки, кодируются в их шестнадцатеричный (hex) UTF-8 эквивалент с использованием процентного кодирования.
Единственный символ, который разрешен в имени или значении куки и при этом кодируется, — это символ процента (%
), который экранируется, чтобы его можно было интерпретировать как обычный символ, а не как индикатор кодирования.
Обратите внимание, что стратегия кодирования/декодирования по умолчанию предназначена для обеспечения совместимоститолько между куки, которые читаются/записываются с использованием js-cookie
. Чтобы изменить эту стратегию, нужно будет использовать преобразователь.
Применяя стандартный API, можно рассчитывать на единое, согласованное поведение вне зависимости от его реализации. Вызовы точно будут опираться на это поведение. Признаюсь, я еще не тестировал, как именно cookieStore
выполняет кодирование значений.
В зависимости от системы управления состоянием и масштаба SDK, можно предоставить специальные хуки для уведомления приложения об изменениях куки. Вместо этого можно использовать стандартное событие CookieChangeEvent
, что упростит интеграцию с SDK.
Хорошо думать о будущем, но пока стандартизация не получит широкого применения, такие SDK, скорее всего, будут скрывать работу с document.cookie
и Cookie Store API
внутри, не полагаясь на них снаружи.
❯ Заключение
Итак, мы рассмотрели основные проблемы document.cookie
и познакомились с Cookie Store API
как современной альтернативой. Также мы рассмотрели, как манипуляции с куки могут быть интегрированы в управление состоянием в приложениях, и как стандартный cookieStore
может повлиять на развитие этого подхода в будущем.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩