User Agent sniffing — это техника поиска в строке User Agent названия браузера и его версии для определения того, с какого браузера пришёл пользователь. Это можно использовать во благо для оптимизаций и загрузки полифилов.
На практике это применялось для трекинга и фингерпринтинга. Поэтому браузеры уже давно пихают в User Agent всё подряд и врут. Поэтому User Agent sniffing — крайне ненадёжная и нестабильная штука. На днях я столкнулся с этим на практике.
При аудите производительности сайта во вкладке Performance я заметил много операций принудительной перекомпоновки (forced reflow). Это когда чтение и запись геометрии элементов в JS приводит к пересчёту стилей.
Виновником стал скрипт
autosizes. Это полифил атрибута sizes="auto" у <img> и <source>. Он запрашивает размеры через getBoundingClientRect(), что и приводит к принудительным перекомпоновкам. Сам скрипт подключается автоматически.Интересно то, что отладка велась в Chrome версии 151, а поддержка
sizes="auto" реализована ещё в версии 126. То есть полифил выполняет работу в браузере, где функция поддерживается. Обычно в таких случаях логика не отрабатывает.Этот полифил не исключение. На страницу встроен скрипт, в котором есть функция проверки перед загрузкой полифила. Также в самом полифиле функция дублируется. То есть проверка выполняется даже дважды. Вот код этой функции:
const polyfillAutoSizes = () => {
if (!window.PerformanceObserver?.supportedEntryTypes?.includes("paint")) {
return false
}
const userAgent = navigator.userAgent;
const platform = navigator.platform;
const maxTouchPoints = navigator.maxTouchPoints
|| 0;
const isIOS = /iPad|iPhone|iPod/.test(platform)
|| platform === "MacIntel"
&& maxTouchPoints > 1;
const isMacSafari = platform.indexOf("Mac") === 0
&& /Safari/.test(userAgent)
&& !/Chrome|Chromium|CriOS|FxiOS|Edg|OPR|Android/.test(userAgent);
const isAppleSafari = isIOS || isMacSafari;
const browserMajorVersion = pattern => {
const match = userAgent.match(pattern);
return match
? parseInt(match[1], 10)
: null
}
const chromeVersion = browserMajorVersion(
/Chrome\/(\d+)/
);
if (chromeVersion !== null) {
return chromeVersion < 126
}
const safariVersion = isAppleSafari
? browserMajorVersion(
/Version\/(\d+).*Safari\//
)
: null;
if (safariVersion !== null) {
return safariVersion < 27
}
const firefoxVersion = browserMajorVersion(
/Firefox\/(\d+)/
);
if (firefoxVersion !== null) {
return firefoxVersion < 150
}
return true
}
;
if (!polyfillAutoSizes()) {
return
}В нём проверяется версия Chrome, Safari и Firefox на основе строки User Agent. Скрипт должен подключаться, если версия Chrome меньше 126. Но он подключается и работает в Chrome 151. Где-то функция дала сбой и вернула
true.Выяснилось, что в моей строке User Agent не было ни Chrome, ни Firefox. Логика прошла все проверки, нигде не случился early return и в итоге дошло до
return true. Вот такая у меня строка User Agent, где ни Chrome, ни его версии:Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1
В теории User Agent выглядит как источник данных о браузере и версии. Может показаться вполне разумным брать оттуда информацию для активации полифила. Ведь известны браузеры и версии, где функция поддерживается.
На деле же строка User Agent максимально непредсказуемая, браузеры постоянно её меняют и могут врать, что и произошло в моём случае. В строке User Agent в Chrome не было ни намёка на Chrome. Браузеры делают это специально.
При разработке полифилов надёжнее будет полагаться на обнаружение функций (feature detection) — проверку фактического наличия и работоспособности функции в браузере. Это и должно служить основанием для подключения полифила.
Авторы полифила это уже поняли, поэтому открыт PR c заменой User Agent sniffing на обнаружение функций. Создаётся реальное изображение, ему задаются CSS-свойства, устанавливается атрибут и производится проверка вычисляемого значения.
const polyfillAutoSizes = () => {
// Avoid polyfilling if the browser is too old and doesn't support performance observer and paint timing
if (
!('PerformanceObserver' in window)
|| !PerformanceObserver
.supportedEntryTypes
.includes('paint')
) {
return false;
}
// Check UA styles for expected styles to support sizes auto.
const img = document.createElement('img');
img.style.setProperty(
'display',
'none',
'important'
);
img.style.setProperty(
'contain',
'revert',
'important'
);
img.sizes = 'auto';
document.documentElement.append(img);
const {contain} = getComputedStyle(img);
img.remove();
return contain !== 'size';
};
if (!polyfillAutoSizes()) {
return;
}Этот способ намного надёжнее, хотя не всегда возможен. Есть функции, поддержку которых нельзя или сложно проверить таким образом. Тем не менее, во многих случаях это гораздо лучше проверки на основе User Agent.
#js #performance