Десятилетиями строка User-Agent (UA) оставалась главным инструментом для определения браузера, операционной системы и типа устройства пользователя. Однако с развитием технологий отслеживания этот заголовок превратился в опасную уязвимость для конфиденциальности.
В рамках инициативы Privacy Sandbox браузеры на движке Blink (Google Chrome, Яндекс Браузер, Microsoft Edge) завершили масштабную реформу: традиционная строка User-Agent была заморожена, а на её смену пришел стандарт User-Agent Client Hints (UA-CH).
Давайте разберем Client Hints, сопоставим его с классическим User-Agent и посмотрим в чем между ними разница.
- Эволюция идентификации браузеров: от User-Agent к UA-CH
- Почему классический User-Agent устарел и попал под заморозку
- Анатомия проблемы: Пассивный фингерпринтинг и Privacy Sandbox
- Архитектура Client Hints: Низкая и высокая энтропия
- Низкоэнтропийные заголовки
- Высокоэнтропийные заголовки
- Механизм согласования: Accept-CH, Critical-CH и Permissions Policy
- Сравнение User-Agent и Client Hints
- Структура данных и RFC 8941
- GREASE-механизм: Защита от жесткого хардкодинга брендов
- Практика внедрения: JavaScript API и Backend (Nginx, Node.js)
- Работа с navigator.userAgentData в JS
- Серверная конфигурация Nginx, Cloudflare и Node.js
- Настройка Nginx
- Обработка в Node.js (Express)
- Решение проблемы лишнего RTT с помощью Critical-CH
- Поддержка браузерами, Safari/Firefox и стратегии Fallback
- Позиция Apple (Safari WebKit) и Mozilla (Firefox Gecko)
- Построение гибридного кода определения устройств
- Влияние на SEO, Яндекс Индексацию, Детекцию ботов и SSR
- Поисковые боты: Googlebot и Yandexbot
- SSR в Next.js / Nuxt.js: Избегаем Hydration Mismatch
- Альтернативное мнение
- Нетривиальный факт
Эволюция идентификации браузеров: от User-Agent к UA-CH
Исторически строка User-Agent создавалась для простой идентификации клиента, но со временем превратилась в хаотичный набор текста. Из-за конкуренции браузеры начали маскироваться друг под друга: Chrome содержит упоминания Safari и Mozilla, а Edge указывает в строке Chrome.
/* Традиционная строка User-Agent */ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Почему классический User-Agent устарел и попал под заморозку
Главная проблема классической строки — разрастание данных и пассивный фингерпринтинг. Традиционный заголовок передавал исчерпывающие сведения об устройстве автоматически при абсолютно каждом HTTP-запросе:
-
Точную версию операционной системы и патч-уровень;
-
Архитектуру процессора (
x86_64,arm64); -
Модель мобильного устройства (
SM-G998B); -
Точную версию браузера вплоть до билда.
Рекламные трекеры и аналитические системы научились комбинировать эти данные с IP-адресом и разрешением экрана, создавая стойкий цифровой отпечаток без согласия пользователя.
Анатомия проблемы: Пассивный фингерпринтинг и Privacy Sandbox
Пассивный фингерпринтинг опасен тем, что его невозможно обнаружить на стороне клиента — браузер не может заблокировать доступ к данным, так как они отправляются на сетевом уровне до выполнения JavaScript.
Для решения этой проблемы комитеты W3C и IETF утвердили спецификацию RFC 8942 (HTTP Client Hints). Новый подход лишает сторонние скрипты возможности незаметно собирать информацию: браузер по умолчанию отправляет только базовый минимумом данных, а детальные сведения отдаются исключительно по явному запросу сервера.
«При поддержке крупного e-commerce проекта с 3 миллионами визитов в сутки мы столкнулись с тем, что старый модуль парсинга User-Agent на регулярных выражениях съедал до 4% ресурсов CPU на каждом инстансе Nginx. После перехода на заголовок
Sec-CH-UA-Mobileнагрузка на прокси-слой заметно снизилась, так как вместо сложных регулярных выражений сервер начал читать простое булево значение».
Архитектура Client Hints: Низкая и высокая энтропия
Ключевая концепция Client Hints — разделение параметров на две категории по уровню энтропии (степени уникальности передаваемой информации).
Низкоэнтропийные заголовки
Эти заголовки считаются безопасными, так как передают обобщенную информацию, недостаточно детализированную для фингерпринтинга. Они отправляются браузером автоматически в каждом HTTP-запросе:
-
Sec-CH-UA: Список брендов браузера и их мажорные версии ("Chromium";v="123"). -
Sec-CH-UA-Mobile: Булевый флаг (?1— мобильное устройство,?0— десктоп). -
Sec-CH-UA-Platform: Название операционной системы ("Windows","Android").
/* Пример Low-Entropy заголовков по умолчанию */ Sec-CH-UA: "Chromium";v="123", "Not:A-Brand";v="8", "Google Chrome";v="123" Sec-CH-UA-Mobile: ?0 Sec-CH-UA-Platform: "Windows"
Высокоэнтропийные заголовки
Высокоэнтропийные данные позволяют выделить конкретного пользователя из массы. Они не передаются по умолчанию. К ним относятся:
-
Sec-CH-UA-Model: Точная модель устройства ("Pixel 8 Pro"). -
Sec-CH-UA-Arch: Архитектура процессора ("x86","arm"). -
Sec-CH-UA-Bitness: Разрядность ОС ("64"). -
Sec-CH-UA-Platform-Version: Точная версия ОС ("15.0.0"). -
Sec-CH-UA-Full-Version-List: Полный список версий браузера со всеми билдами.
Механизм согласования: Accept-CH, Critical-CH и Permissions Policy
Чтобы получить высокоэнтропийные данные, сервер должен проинформировать браузер в первичном ответе с помощью заголовка Accept-CH.
/* Ответ сервера: запрашиваем модель и архитектуру */ HTTP/1.1 200 OK Accept-CH: Sec-CH-UA-Model, Sec-CH-UA-Arch Vary: Accept-CH
После этого при всех последующих запросах к данному домену браузер будет прикреплять запрошенные заголовки:
/* Последующий запрос от клиента */ GET /shop HTTP/1.1 Host: example.com Sec-CH-UA: "Chromium";v="123", "Google Chrome";v="123" Sec-CH-UA-Mobile: ?1 Sec-CH-UA-Platform: "Android" Sec-CH-UA-Model: "Pixel 8 Pro" Sec-CH-UA-Arch: "arm"
Если клиенту внутри iframe со стороннего домена (например, виджету) требуется получить Client Hints, основной документ обязан дать разрешение через Permissions Policy:
Permissions-Policy: ch-ua-model=(self "https://widget.example.com")
Сравнение User-Agent и Client Hints
Сравним два подхода по ключевым архитектурным параметрам:
| Параметр | Классический User-Agent | User-Agent Client Hints (UA-CH) |
| Формат данных | Монолитная некоординированная строка | Структурированные заголовки (RFC 8941) |
| Способ передачи | Пассивная (все данные сразу) | Избирательная (Low vs High Entropy) |
| Сложность парсинга | Высокая (требует громоздких Regex) | Низкая (типизированные пары ключ-значение) |
| Нагрузка на сеть | Передает лишний объем данных постоянно | Минимальный базовый заголовок |
| Безопасность | Высокий риск фингерпринтинга | Ограничение пассивного трекинга |
| Поддержка стандартов | Устаревший фактический стандарт | Официальные спецификации W3C и IETF |
Структура данных и RFC 8941
Заголовки Client Hints соответствуют стандарту RFC 8941. Это обеспечивает строгое форматирование:
-
Списки:
"Chromium";v="123", "Google Chrome";v="123" -
Булевы значения:
?1(true) или?0(false) -
Строки:
"Windows"
Благодаря этому бэкенд-парсеру не нужно писать регулярные выражения — достаточно расщепить строку по разделителям или использовать готовый парсер структурированных заголовков.
GREASE-механизм: Защита от жесткого хардкодинга брендов
Браузеры на базе Chromium внедряют в заголовок Sec-CH-UA так называемый GREASE (Generate Random Extensions And Sustain Extensibility). Это намереное подмешивание несуществующих брендов ("Not:A-Brand";v="8" или "Chromium";v="123").
Sec-CH-UA: "Not_A Brand";v="99", "Chromium";v="123", "Google Chrome";v="123"
Важно: GREASE предотвращает ситуацию, когда разработчики пишут проверки вида
if (ua === 'Chrome'). Код парсинга обязан корректно обходить незнакомые бренды и искать только целевое значение.
Практика внедрения: JavaScript API и Backend (Nginx, Node.js)
Работа с navigator.userAgentData в JS
На стороне клиента доступен объект navigator.userAgentData. Базовые низкоэнтропийные данные считываются синхронно:
// Проверка поддержки API
if (navigator.userAgentData) {
// Синхронный доступ к Low-Entropy данным
console.log(navigator.userAgentData.brands);
// [ {brand: "Chromium", version: "123"}, ... ]
console.log(navigator.userAgentData.mobile); // true или false
console.log(navigator.userAgentData.platform); // "Android", "Windows" и т.д.
}
Для получения высокоэнтропийных параметров вызывается асинхронный метод getHighEntropyValues():
if (navigator.userAgentData) {
navigator.userAgentData.getHighEntropyValues([
"architecture",
"model",
"platformVersion",
"bitness"
]).then(ua => {
if (ua.model) {
console.log("Модель устройства:", ua.model); // "Pixel 8"
}
console.log("Архитектура ЦП:", ua.architecture); // "arm"
}).catch(err => {
console.error("Ошибка получения High-Entropy CH:", err);
});
}
Серверная конфигурация Nginx, Cloudflare и Node.js
Настройка Nginx
Чтобы веб-сервер Nginx запрашивал модель устройства и версию платформы у клиентов, добавьте директиву add_header в блок server или location:
server {
listen 443 ssl http2;
server_name example.com;
# Информируем браузер о необходимости присылать High-Entropy данные
add_header Accept-CH "Sec-CH-UA-Model, Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch";
# КРИТИЧНО: предотвращаем некорректное кеширование на промежуточных прокси/CDN
add_header Vary "Accept-CH";
location / {
proxy_pass http://backend_cluster;
proxy_set_header Sec-CH-UA $http_sec_ch_ua;
proxy_set_header Sec-CH-UA-Mobile $http_sec_ch_ua_mobile;
proxy_set_header Sec-CH-UA-Platform $http_sec_ch_ua_platform;
proxy_set_header Sec-CH-UA-Model $http_sec_ch_ua_model;
}
}
Обработка в Node.js (Express)
const express = require('express');
const app = express();
app.use((req, res, next) => {
// Устанавливаем заголовок Accept-CH для всех ответов
res.setHeader('Accept-CH', 'Sec-CH-UA-Model, Sec-CH-UA-Platform-Version');
res.setHeader('Vary', 'Accept-CH');
// Считываем низкоэнтропийные заголовки
const isMobile = req.headers['sec-ch-ua-mobile'] === '?1';
const platform = req.headers['sec-ch-ua-platform'] || 'Unknown';
// Высокоэнтропийный заголовок (если пришел от клиента)
const deviceModel = req.headers['sec-ch-ua-model'] || 'Not Provided';
req.userDevice = { isMobile, platform, deviceModel };
next();
});
app.get('/', (req, res) => {
res.send(`Устройство: ${req.userDevice.deviceModel}, Мобильный: ${req.userDevice.isMobile}`);
});
app.listen(3000);
Решение проблемы лишнего RTT с помощью Critical-CH
По умолчанию при самом первом визите пользователя сервер возвращает заголовок Accept-CH, но клиент успевает отправить первый запрос без High-Entropy данных. Если серверу критически важно получить модель устройства до генерации первого HTML (например, для мобильного SSR-рендеринга), применяется заголовок Critical-CH.
HTTP/1.1 200 OK Accept-CH: Sec-CH-UA-Model Critical-CH: Sec-CH-UA-Model Vary: Accept-CH
Если браузер видит Critical-CH и понимает, что не передал требуемый заголовок в первом запросе, он забраковывает полученный ответ и незамедлительно делает повторный запрос, уже с прикрепленным Sec-CH-UA-Model.
⚠️ Предупреждение: Использование
Critical-CHприводит к повторному круговому ходу сети (RTT). Настраивайте его только для тех параметров, без которых корректный рендеринг страницы физически невозможен.
Поддержка браузерами, Safari/Firefox и стратегии Fallback
На момент 2026 года ситуация с поддержкой Client Hints в вендорной среде разделена:
Позиция Apple (Safari WebKit) и Mozilla (Firefox Gecko)
-
Apple (Safari): Инженеры WebKit официально выступают против UA-CH. По их мнению, заголовок
Accept-CHоткрывает возможности для активного фингерпринтинга. Safari использует альтернативный путь: максимальную заморозку и усечение традиционной строки User-Agent. -
Mozilla (Firefox): Разработчики Gecko занимают нейтрально-отрицательную позицию. Firefox поддерживает замороженный формат User-Agent и не включает API
navigator.userAgentDataпо умолчанию.
Построение гибридного кода определения устройств
Для устойчивой работы приложений в продакшене необходимо строить каскадный фоллбэк.
/**
* Универсальная функция определения типа устройства с каскадным фоллбэком
*/
function detectDeviceContext() {
// 1. Проверяем UA-CH (для Chrome, Яндекс Браузера, Edge)
if (navigator.userAgentData) {
return {
isMobile: navigator.userAgentData.mobile,
platform: navigator.userAgentData.platform,
source: 'UA-CH'
};
}
// 2. Фоллбэк на Feature Detection (CSS Media Queries & Touch API)
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isMobileScreen = window.matchMedia('(max-width: 768px)').matches;
if (hasTouch && isMobileScreen) {
return { isMobile: true, platform: 'Unknown Mobile', source: 'Feature Detection' };
}
// 3. Крайний фоллбэк на замороженный User-Agent (Safari, Firefox)
const legacyUA = navigator.userAgent || '';
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(legacyUA);
return {
isMobile: isMobileUA,
platform: parseLegacyPlatform(legacyUA),
source: 'Legacy UA Parsing'
};
}
function parseLegacyPlatform(ua) {
if (ua.includes('iPhone') || ua.includes('iPad')) return 'iOS';
if (ua.includes('Android')) return 'Android';
if (ua.includes('Windows')) return 'Windows';
if (ua.includes('Macintosh')) return 'macOS';
return 'Desktop';
}
Влияние на SEO, Яндекс Индексацию, Детекцию ботов и SSR
Поисковые боты: Googlebot и Yandexbot
Поисковые роботы обязаны точно идентифицировать себя перед веб-серверами, чтобы получать корректные версии страниц.
-
Googlebot: Полностью поддерживает UA-CH. При сканировании краулер Google отправляет как замороженную строку User-Agent, так и соответствующие заголовки
Sec-CH-UA("Googlebot";v="2.1"). -
Яндекс.Бот (Yandexbot): Индексирующий робот Яндекса ориентируется в первую очередь на классическую строку User-Agent, однако сервисы Яндекса полностью готовят инфраструктуру к обработке Client Hints.
⚠️ Важно: Сервер должен отдавать абсолютно идентичный HTML-код как при проверке через традиционный UA, так и при разборе
Sec-CH-UA. Рассогласование отдачи текста ботам может быть расценено алгоритмами Яндекса и Google как клоакинг.
SSR в Next.js / Nuxt.js: Избегаем Hydration Mismatch
Одна из самых частых ошибок фронтенд-разработчиков при переходе на Client Hints — возникновение ошибки Hydration Mismatch при рендеринге на стороне сервера.
Error: Text content does not match server-rendered HTML. Server: "Desktop Layout" Client: "Mobile Layout"
Sec-CH-UA-Mobile), а клиентский React после вызова navigator.userAgentData.mobile попытался перерисовать интерфейс под мобильную версию.Правильное решение на Node.js / Next.js:
// app/page.tsx (Next.js App Router)
import { headers } from 'next/headers';
export default async function HomePage() {
const headersList = await headers();
// Извлекаем низкоэнтропийный заголовок на сервере
const secChUaMobile = headersList.get('sec-ch-ua-mobile');
// Если заголовок отсутствует (например, Safari), используем фоллбэк по User-Agent
const userAgent = headersList.get('user-agent') || '';
const isMobile = secChUaMobile
? secChUaMobile === '?1'
: /Mobile|Android|iPhone/i.test(userAgent);
return (
<main>
{isMobile ? <MobileVersion /> : <DesktopVersion />}
</main>
);
}
Альтернативное мнение
Есть и те, кто критикует стандарт Client Hints за усложнение стека. По их мнению замена одной строки User-Agent на комбинацию из 8 разных HTTP-заголовков увеличивает объём сетевых оверхедов и создает путаницу из-за частичного отсутствия поддержки в Safari и Firefox. Так, решение задач адаптивности полностью должно переноситься на CSS Media Queries и Feature Detection, исключая серверное определение параметров устройств.
Нетривиальный факт
Интересный нюанс спецификации RFC 8941 заключается в том, что все булевы значения в Client Hints передаются со знаком вопроса: ?1 означает true, а ?0 означает false. Это сделано для того, чтобы HTTP-парсеры могли отличать численные типы данных от булевых флагов на этапе лексического анализа без привлечения схемы валидации.










