I Hate Waiting

Ускоряет загрузку страниц: на видеохостингах — приоритет главному видео, на остальных — приоритет видимому контенту.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         I Hate Waiting
// @name:en      I Hate Waiting
// @namespace    https://tampermonkey.net/
// @version      2.2.5.5
// @license      MIT
// @description  Ускоряет загрузку страниц: на видеохостингах — приоритет главному видео, на остальных — приоритет видимому контенту.
// @description:en Speeds up page loading: on video-hosting sites, priority is given to the main video; on other sites, priority is given to visible content.
// @author       Qwen + Claude + Grok + DeepSeek + twicks other programmers
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @compatible   firefox 132+ Violentmonkey
// @compatible   firefox 132+ Tampermonkey
// @compatible   firefox 132+ GreaseMonkey
// @compatible   chrome  101+ Violentmonkey
// @compatible   chrome  101+ Tampermonkey
// @compatible   chrome  101+ ScriptCat
// @compatible   safari 18.0+ Stay
// @compatible   edge 101+ Tampermonkey
// @compatible   opera 87+ Tampermonkey
// @compatible   android Via Browser
// ==/UserScript==

(function () {
    'use strict';

    /* ── ЗАЩИТА ОТ ФРЕЙМОВ ──────────────────────────────── */
    // Скрипт работает только на основной странице.
    // Плюсы: нет дублирования логов, не трогаем DOM внутри iframe-плееров,
    // меньше MutationObserver-ов в памяти. Видео ищем через DOM основной
    // страницы — boostMainVideo не теряет доступ к iframe-плеерам.
    if (window.top !== window) return;

	// ═══════════════════════════════════════════════════════════════
	// 🔧 ВРЕМЕННЫЙ ПЕРЕКЛЮЧАТЕЛЬ РЕЖИМОВ ДЛЯ ТЕСТОВ (раскомментировать нужную строку)
	// ═══════════════════════════════════════════════════════════════
	// Для OFF:
	// localStorage.setItem('ihw:off:' + location.hostname, '1');
	// Для ON (явный):
	// localStorage.setItem('ihw:on:' + location.hostname, '1');
	// Для ON[E] (Extreme):
	// localStorage.setItem('ihw:extreme:' + location.hostname, '1');
	// Для AUTO (очистить всё, вернуть стандартное поведение):
	// ['off','on','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname));
	// ═══════════════════════════════════════════════════════════════
	
    /* ── ОТЛАДКА ────────────────────────────────────────── */
    // true  — все сообщения видны в консоли F12 (режим разработки)
    // false — лог отключён полностью (режим релиза, нет затрат на вывод)
    const DEBUG = false;
    const log = (...args) => { if (DEBUG) console.log(...args); };

    /* ── ОПРЕДЕЛЕНИЕ УСТРОЙСТВА ─────────────────────────── */
    // Mode: "Mobile" или "Desktop"
    const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
        || (navigator.maxTouchPoints > 1 && window.innerWidth < 1024);
    const MODE = isMobile ? 'Mobile' : 'Desktop';

    /* ── НАСТРОЙКИ ──────────────────────────────────────── */
    // true  — при скрытии вкладки ставить видео на паузу, при возврате — возобновлять.
    // Не влияет на картинку-в-картинке (PiP): если видео в PiP — оно продолжает играть.
    // Полезно на мобиле (АКБ) и Desktop (CPU/GPU в фоне). Отключить если сайт сам
    // управляет паузой (на YouTube Desktop) или поведение кажется лишним.
	// работает только на Youtube (там где плеер в главном окне, а не в защищенном iframe)
    const PAUSE_ON_HIDDEN = true;

	// ── SEEK HACK ─────────────────────────────────────────────────
	// Эксперимент: микро-сдвиг currentTime (+0.01) триггерит догрузку
	// следующего HLS/DASH сегмента до начала воспроизведения.
	// Работает только на сайтах, где главное видео представлено нативным элементом <video> и скрипт определяет его по стратегии 1.
	// НЕ применять на YouTube, Twitch и др. (нестабильно).
	// По умолчанию ВЫКЛЮЧЕНО. Включить можно двумя способами:
	//   1. Установить константу ниже в true
	//   2. Включить через localStorage: localStorage.setItem('ihw:seekhack', '1')
	const SEEK_HACK_ENABLED = false;          
	const SEEK_HACK = SEEK_HACK_ENABLED || localStorage.getItem('ihw:seekhack') === '1';

	// Список доменов, на которых seek-hack НЕ применяется (нестабильно)
	const SEEK_HACK_EXCLUDE = [
//		'youtube.com',
//		'youtu.be',
//		'twitch.tv'
	];

	// ── EXTREME MODE ──────────────────────────────────────────
    // Агрессивный режим экономии трафика и ресурсов. Управляется каруселью кнопки.
    // Для ручного тестирования: вручную задать '1' в localStorage ('ihw:extreme:hostname').
    // EXTREME_MODE: читаем ДО объявления SITE_KEY — используем inline ключ
    // В режиме ON[A] EXTREME_MODE определяется через _resolveGlobalMode() ниже.
    // Прямое значение используется только при явном локальном ON[E].
    const _localExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';

    /* ── АВТООПРЕДЕЛЕНИЕ ГЛОБАЛЬНОГО РЕЖИМА (_resolveGlobalMode) ─────────────
     * Функция выносится отдельно для будущего расширения условий (качество сети и т.д.)
     * Текущая логика:
     *   Mobile → ON[E] (Extreme): экономим трафик и АКБ на мобиле всегда
     *   Desktop → ON: обычное ускорение без визуального ущерба
     *
     * Будущие условия (не реализованы):
     *   navigator.connection.effectiveType === '2g' || 'slow-2g' → ON[E]
     *   DOM > 5000 узлов → ON[E]
     *   TTFB > 1500 мс → ON[E]
     ─────────────────────────────────────────────────────────────────────── */
    function _resolveGlobalMode() {
         // В будущем: добавить проверки сети, DOM-размера и TTFB
        return isMobile ? 'EXT' : 'ON'; // Mobile→Extreme, Desktop→Normal
    }

    // Определяем глобальный режим по-умолчанию (используется при ON[A])
    const _globalAutoMode = _resolveGlobalMode(); // 'ON' или 'EXT'

    // EXTREME_MODE = true если:
    //   - явный локальный ON[E] (пользователь выбрал Extreme для этого сайта)
    //   - ИЛИ режим ON[A] + _globalAutoMode === 'EXT' (авто: мобиль)
    const _isAutoMode = localStorage.getItem('ihw:auto:' + location.hostname) === '1'
        && !localStorage.getItem('ihw:off:' + location.hostname);
    const EXTREME_MODE = _localExt || (_isAutoMode && _globalAutoMode === 'EXT');

    // Флаг защиты от двойного запуска onLoadHandler —
    // при readyState=interactive вызываем сразу, но событие load всё равно придёт
    let _initDone = false;

    // _t0 — момент запуска скрипта (мс от навигации). Вне DEBUG-блока: нужен всегда
    // для показа «load − скрипт_запустился» на кнопке независимо от DEBUG-флага.
    const _t0 = performance.now();

	function _getModeLabel() {
		const isOff = localStorage.getItem(SITE_KEY) === '1';
		const isExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';
		const isAuto = localStorage.getItem('ihw:auto:' + location.hostname) === '1';
		if (isOff) return '[OFF]';
		if (isExt) return '[ON[E]]';
		if (isAuto) {
			const actual = EXTREME_MODE ? 'ON[E]' : 'ON';
			return `[ON[A]=${actual}]`;
		}
		// Явный ON (установленный через кнопку)
		return '[ON]';
	}

	/* ── ВЫВОД РАСШИРЕННЫХ МЕТРИК (после load) ──────────────────────────── */
    /* ── ЗАМЕР ВРЕМЕНИ ЗАГРУЗКИ ────────────────────────────────── */
    // _showDeltaOnBtn — показывает «▲ X мс» на кнопке ON/OFF после загрузки страницы:
    //   X = load − скрипт_запустился (= loadEventEnd − _t0)
    //   Это время от старта нашего скрипта до полной загрузки страницы.
    //   При ON браузер работает с нашими оптимизациями, при OFF — без них.
    //   Разница X(OFF) − X(ON) = реальный выигрыш от скрипта.
    //   Кнопка показывает значение 3 секунды, затем возвращает ON/OFF.
    // _printTiming — вывод всех метрик в консоль (только при DEBUG=true):
    //   • DOMContentLoaded — страница видна (HTML разобран, синхронные скрипты выполнены)
    //   • load            — всё загружено (картинки, шрифты, iframe)
    //   • load − DCL      — время догрузки ресурсов после первого рендера
    //   • load − старт    — КЛЮЧЕВАЯ метрика для сравнения ON vs OFF
    //   • скрипт запустился — для справки, когда Tampermonkey внедрил скрипт
    // Всё запускается строго после load-события — до него loadEventEnd = 0.
    // Работает при ON и при OFF одинаково (вызов ниже, до early-return SITE_KEY).

	function _logExtendedMetrics(modeLabel) {
		const nav = performance.getEntriesByType('navigation')[0];
		// Если load ещё не произошёл, пробуем снова через 100 мс
		if (!nav || nav.loadEventEnd <= 0) {
			setTimeout(() => _logExtendedMetrics(modeLabel), 100);
			return;
		}
		const re_d = nav.responseEnd;
		const ttfb = Math.round(nav.responseStart - nav.requestStart);
		const dcl  = nav.domContentLoadedEventEnd.toFixed(0);
		const load = nav.loadEventEnd.toFixed(0);
		const dDCL = Math.round(nav.loadEventEnd - nav.domContentLoadedEventEnd);
		const ldRaw= Math.round(nav.loadEventStart - re_d);
		const dom  = document.getElementsByTagName('*').length;
		const kb   = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;
		const loadMs = Math.round(nav.loadEventEnd - _t0);
		const fcp  = performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint');
		const fcpMs= fcp ? Math.round(fcp.startTime - re_d) : null;
		console.group(`[IHW] ${modeLabel} ${location.hostname}`);
		console.log(`  скрипт запустился    : ${_t0.toFixed(0)} мс`);
		console.log(`  TTFB                 : ${ttfb} мс  ← время ответа сервера`);
		console.log(`  loadStart−responseEnd: ${ldRaw} мс  ← not-lazy ресурсы без TTFB`);
		console.log(`  DOMContentLoaded     : ${dcl} мс`);
		console.log(`  load                 : ${load} мс`);
		console.log(`  load − DCL (▼)       : ${dDCL} мс  ← откл. ресурсы (>0 = скрипт помог)`);
		console.log(`  load − старт (▲)     : ${loadMs} мс  ← СРАВНИВАЙ ON vs OFF`);
		console.log(`  DOM узлов            : ${dom}${dom > 3000 ? '  ← тяжёлая страница' : ''}`);
		console.log(`  Размер страницы      : ${kb ? kb + ' кб' : 'кэш'}`);
		console.log(`  Заблокировано        : ${_blockedCount}`);
		if (_canplayMs) console.log(`  Canplay плеера       : ${_canplayMs} мс  ← нативный <video>`);
		console.groupEnd();
	}


    let _btn = null;       // ссылка на кнопку — для кратковременного показа времени загрузки
    let _blockedCount = 0; // счётчик заблокированных трекеров/prefetch (для диагностики)
    let _canplayMs = 0;    // время до готовности плеера (canplay) — только Strategy 1 (нативный <video>)

    /* ── ЧИСТЫЕ МЕТРИКИ (без влияния сети и тяжёлого медиа) ─────────────
     * Идея DeepSeek: вычитаем responseEnd (получен последний байт HTML) из
     * времён DOM-событий. Получаем время, потраченное ТОЛЬКО на обработку —
     * без TTFB, без загрузки картинок/видео/шрифтов.
     *
     * domInteractive − responseEnd  = парсинг HTML + синхронные скрипты
     * loadEventStart  − responseEnd  = всё вышеперечисленное + отложенные ресурсы
     *                                  (но НЕ картинки если loading=lazy)
     *
     * Работает при ON и при OFF → честное сравнение влияния скрипта.
     * Шрифты (font-display:swap) — не искажают domInteractive (async, после парсинга).
     * Изображения с loading=lazy — не ждутся при domInteractive.
     * ВЫВОД только при DEBUG=true — нулевая нагрузка в релизе.
     ──────────────────────────────────────────────────────────────────── */
    if (DEBUG) {
        const _logClean = () => {
            const nav = performance.getEntriesByType('navigation')[0];
            if (!nav) return;
            const re = nav.responseEnd; // конец получения HTML

			const modeLabel = _getModeLabel();
			// Для краткости можно оставить только метку, без лишних пробелов
			console.group(`[IHW] ${modeLabel} Чистые метрики (без сети) — ${location.hostname}`);

            const diRaw = Math.round(nav.domInteractive - re);
            const dclRaw = Math.round(nav.domContentLoadedEventStart - re);
            console.log(`  domInteractive − responseEnd : ${diRaw} мс  ← HTML разобран, DOM построен (текст виден)`);
            console.log(`  domContentLoaded − responseEnd: ${dclRaw} мс  ← + выполнены sync скрипты`);
            // loadEventStart недоступен при DOMContentLoaded (равен 0 до load события).
            // Полное время загрузки смотри на кнопке (▲) после load.
            console.log('  (Сравни ON vs OFF — разница = чистый эффект скрипта)');
            // FCP если доступен
            const fcp = performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint');
            if (fcp) console.log(`  FCP − responseEnd             : ${Math.round(fcp.startTime - re)} мс  ← первый контент на экране`);
            console.groupEnd();
        };
        // Запускаем после DOMContentLoaded — раньше некоторые значения могут быть 0
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => setTimeout(_logClean, 0), { once: true });
        } else {
            setTimeout(_logClean, 0);
        }
    }

    /* ── ИСКЛЮЧЕНИЯ САЙТОВ ──────────────────────────────── */
    const SITE_KEY = 'ihw:off:' + location.hostname;
    if (localStorage.getItem(SITE_KEY) === '1') {
        _renderBtn(); // OFF state reflected via _getMode()
        // OFF режим: полные метрики выводим в консоль после load (Qwen: OFF должен давать те же метрики)
        if (DEBUG) {
            _logExtendedMetrics('[OFF]');
        }
        return;
    }

    /* ── ОПРЕДЕЛЕНИЕ БРАУЗЕРА ───────────────────────────────────
     * Feature-detection, не UA-sniffing: надёжно при смене UA.
     * isFirefox: InstallTrigger присутствует только в Gecko.
     * isChromium: window.chrome без Gecko → Chromium-based.
     * CSS.supports guard eliminé — браузеры без поддержки content-visibility игнорируют стиль (Chrome 85+ / Firefox 125+).
     ─────────────────────────────────────────────────────────── */
    const isFirefox  = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
    const isChromium = !isFirefox && !!window.chrome;
    log(`[IHW] Browser: ${isFirefox ? 'Firefox' : isChromium ? 'Chromium' : 'Other'}`);

    /* ── ОПРЕДЕЛЕНИЕ ТИПА СТРАНИЦЫ ──────────────────────── */
    // Page: "Video Content" — популярные видеохостинги
    // Page: "Mixed Content" — все остальные сайты
    const VIDEO_HOSTS = [
        'youtube.com', 'youtu.be',
        'vimeo.com', 'rutube.ru', 'twitch.tv', 'dailymotion.com',
        'ok.ru', 'vk.com', 'vkvideo.ru', 'video.mail.ru', 'my.mail.ru',
        'bilibili.com', 'bilibili.tv', 'tiktok.com', 'odysee.com',
        'smotret.tv', 'platform.rambler.ru', 'kinopoisk.ru',
        'zetflix.bet', 'zetflix.to', 'zetflix.online', 'zetflix.app' // кино-стриминг
    ];

    // Поддомены или hostname, которые НЕ должны считаться видеохостингами
    // (даже если домен попал в VIDEO_HOSTS или путь совпал с VIDEO_PATH_SEGMENTS)
    const VIDEO_HOST_EXCEPTIONS = [
        'alice.yandex.ru',   // AI-чат (полный hostname)
    ];

    /* ── VIDEO_PATH_SEGMENTS ─────────────────────────────────────────────────
     * Сегменты пути URL, по которым обычный сайт считается Video Content.
     * Проверяются как ЦЕЛЫЕ сегменты пути (не подстроки), поэтому:
     *   /video           ✓  /live              ✓  /videos/12345    ✓
     *   /developer       ✗  /surveillance      ✗  /livewire        ✗
     * Добавляйте/удаляйте по необходимости.
     ─────────────────────────────────────────────────────────────────────── */
    const VIDEO_PATH_SEGMENTS = new Set([
        'video',    // example.com/video или example.com/video/...
        'videos',   // example.com/videos/12345-abc
        'live',     // example.com/live или example.com/live/stream
        'clip',     // example.com/clip/...
        'stream',   // example.com/stream
        'watch',    // example.com/watch?v=... (не-YouTube)
        'player',   // example.com/player/...
    ]);
	
    /* ── КАРТА CDN ДЛЯ PRECONNECT ───────────────────────────────────────────
     * Статический preconnect по хостингу — греет соединения до нахождения видео.
     * Особенно полезно для iframe-плееров (Vimeo, Twitch, VK, Dailymotion),
     * где PerformanceObserver не видит сетевые запросы внутри cross-origin iframe.
     * Данные: Qwen+Grok+DeepSeek анализ реальных CDN 2026.
     ─────────────────────────────────────────────────────────────────────── */
    const VIDEO_CDN_MAP = {
        'youtube.com':     ['googlevideo.com', 'ytimg.com', 'ggpht.com'],
        'youtu.be':        ['googlevideo.com', 'ytimg.com'],
        'rutube.ru':       ['cdnvideohub.com', 'yandex.net', 'rtbcdn.ru'],
        'vimeo.com':       ['akamaized.net', 'vimeocdn.com', 'player.vimeo.com'],
        'ok.ru':           ['mycdn.me', 'ok.ru', 'okcdn.ru'],
        'vk.com':          ['userapi.com', 'vkuser.net'],
        'vkvideo.ru':      ['userapi.com', 'vkuser.net'],
        'dailymotion.com': ['dmcdn.net', 'cloudfront.net'],
        'twitch.tv':       ['ttvnw.net', 'jtvnw.net', 'player.twitch.tv'],
        'bilibili.com':    ['bilivideo.com', 'akamaized.net'],
        'tiktok.com':      ['tiktokcdn.com', 'bytecdn.com'],
        'odysee.com':      ['odycdn.com', 'cloudflare-stream.com'],
        'dzen.ru':         ['dzen.ru', 'yandex.net'],
    };

    // Глобальный Set дедупликации — один preconnect на хост за сессию
    const _preconnected = new Set();

    // Добавить <link rel="preconnect"> с дедупликацией (Qwen+DeepSeek)
    function _doPreconnect(host) {
        if (!host || _preconnected.has(host) || host === location.hostname) return;
        if (isTracker('https://' + host)) return; // не греем трекеры
        _preconnected.add(host);
        const _lnk = document.createElement('link');
        _lnk.rel = 'preconnect';
        _lnk.href = 'https://' + host;
        _lnk.crossOrigin = 'anonymous';
        document.head.appendChild(_lnk);
        log('[IHW Video] Preconnect →', host);
    }

    // Проверить — является ли хост валидным видео-CDN (не трекер, не same-origin) (Qwen)
    function _isVideoCDN(host) {
        if (!host || host === location.hostname) return false;
        if (isTracker('https://' + host)) return false;
        // Проверка по карте
        const h = location.hostname.replace(/^www\./, '');
        const cdns = VIDEO_CDN_MAP[h];
        if (cdns && cdns.some(c => host.includes(c))) return true;
        // Универсальные паттерны видео-CDN
        return /googlevideo|cdnvideohub|akamaized|ttvnw|bilivideo|odycdn|vimeocdn|dmcdn|userapi|vkuser/i.test(host);
    }

	/* ── ДИНАМИЧЕСКИЙ PRECONNECT ДЛЯ НАТИВНОГО ВИДЕО ─────────────────
	 * Вызывается из стратегии 1 после нахождения главного видео.
	 * Пытается извлечь CDN из currentSrc (прямые mp4) или через PerformanceObserver (MSE/HLS).
	 * Использует уже существующие _doPreconnect, _isVideoCDN, isTracker.
	 * Только Desktop.
	 */
		function initDynamicPreconnect(video) {
			if (isMobile || !video) return;

			log('[IHW Video] Dynamic preconnect observer started for', video.src ? 'direct URL' : 'MSE/blob');

			const tryDirect = () => {
				try {
					const src = video.currentSrc || video.src;
					if (src && !src.startsWith('blob:') && !src.startsWith('data:')) {
						const host = new URL(src, location.origin).hostname;
						if (host !== location.hostname) {
							_doPreconnect(host);
							return true;
						}
					}
				} catch(e) {}
				return false;
			};

			if (!tryDirect() && window.PerformanceObserver) {
				try {
					const observer = new PerformanceObserver((list, obs) => {
						for (const entry of list.getEntries()) {
							const name = entry.name;

							// Расширенный фильтр специально для YouTube + универсальные сегменты
							const isVideoResource = /\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(name) ||
                                                     /\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4|videoplayback|initplayback/i.test(name) || 
                                                     /googlevideo|cdnvideohub|vimeocdn|dmcdn/i.test(name);

							if (!isVideoResource) continue;

							try {
								const segHost = new URL(name).hostname;
								if (segHost === 'i.vimeocdn.com') continue; // известный ложный срабатывающий хост

								if (_isVideoCDN(segHost) || !isTracker('https://' + segHost)) {
									_doPreconnect(segHost);           // логируем даже поддомены
									// Не disconnect — пусть ловит все поддомены YouTube
								}
							} catch(e) {}
						}
					});

					observer.observe({ type: 'resource', buffered: true });
					log('[IHW Video] PerformanceObserver для CDN запущен (YouTube-style)');

					// Таймаут 20 сек — YouTube иногда долго буферизирует первый сегмент
					setTimeout(() => { try { observer.disconnect(); } catch(e) {} }, 20000);
				} catch(e) {
					log('[IHW] PerformanceObserver error:', e);
				}
			}

    if (video.readyState >= 1) tryDirect();
    else video.addEventListener('loadedmetadata', tryDirect, { once: true });
}	
    /* ── initVideoPreconnect(): единая функция preconnect для ВСЕХ стратегий ──
     * Вызывается один раз при обнаружении Video Content страницы.
     * Действие 1: статический preconnect по карте CDN — работает для iframe-плееров
     *   (Vimeo, Twitch, VK, Dailymotion — там PerformanceObserver не видит iframe-сегменты).
     * Действие 2: PerformanceObserver — ловит реальные CDN-хосты из первых сегментов
     *   MSE/DASH/HLS. Работает для YouTube, Rutube, OK, Bilibili (нативный <video>).
     *   Расширенный паттерн Qwen: .ts .m4s .mp4 .webm .m3u8 .mpd + ?range= /seg- /chunk-
     * Только Desktop — мобиль не тратит трафик (isMobile guard).
     ─────────────────────────────────────────────────────────────────────── */
    function initVideoPreconnect() {
        if (isMobile) return;

        // ── Шаг 1: статический preconnect по карте для текущего хостинга ──
        const _h = location.hostname.replace(/^www\./, '');
        const _cdns = VIDEO_CDN_MAP[_h] || [];
        _cdns.forEach(cdn => _doPreconnect(cdn));
        if (_cdns.length) log('[IHW Video] Статический preconnect:', _cdns.join(', '));

        // ── Шаг 2: PerformanceObserver для динамических CDN (MSE/DASH сегменты) ──
        // Ловит первый реальный видео-сегмент и греет его CDN-хост.
        // Расширенный паттерн (Qwen+DeepSeek): .ts .m4s .mp4 .webm .mpd + query-паттерны
        if (!window.PerformanceObserver) return;
        try {
            const _po = new PerformanceObserver((list, obs) => {
                for (const entry of list.getEntries()) {
                    const n = entry.name;
                    // Паттерн видео-сегментов: расширения + query-строки HLS/DASH
                    if (!/\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(n)
                        && !/\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4/i.test(n)) continue;
                    try {
                        const segHost = new URL(n).hostname;
                        // Пропускаем i.vimeocdn.com (постеры, не видео-CDN)
                        if (segHost === 'i.vimeocdn.com') continue;
                        if (_isVideoCDN(segHost) || (!isTracker('https://' + segHost) && segHost !== location.hostname)) {
                            _doPreconnect(segHost);
                            obs.disconnect();
                            break;
                        }
                    } catch(e) {}
                }
            });
            _po.observe({ type: 'resource', buffered: true });
            // Самоочистка через 15с (DeepSeek: сегменты обычно идут в первые 3–8с)
            setTimeout(() => { try { _po.disconnect(); } catch(e) {} }, 15000);
        } catch(e) { log('[IHW Video] PerformanceObserver unavail:', e); }
    }

//    const isVideoHost = VIDEO_HOSTS.some(h => location.hostname.endsWith(h));
    const isVideoHost = (() => {
        const host = location.hostname;
        // 1. Исключения по hostname — всегда Mixed Content
        if (VIDEO_HOST_EXCEPTIONS.includes(host)) return false;
        // 2. Домен в списке видеохостингов
        if (VIDEO_HOSTS.some(h => host.endsWith(h))) return true;
        // 3. Путь URL содержит видео-сегмент как ЦЕЛЫЙ сегмент пути
        //    /live ✓  /live/stream ✓  /livewire ✗  /surveillance ✗
        const segments = location.pathname.split('/');
        return segments.some(seg => VIDEO_PATH_SEGMENTS.has(seg.toLowerCase().split('?')[0]));
    })();
    const PAGE = isVideoHost ? 'Video Content' : 'Mixed Content';

    log(`[IHW] Mode:${MODE} | Page:${PAGE}`);

    /* ── ТРЕКЕРЫ И ИСКЛЮЧЕНИЯ ───────────────────────────── */

    // Домены трекеров — блокируем sendBeacon, удаляем из DOM, исключаем из dns-prefetch
    const TRACKERS = [
         // ── Глобальные (Google/Meta/Microsoft) ──────────────────────
        'google-analytics.com', 'googletagmanager.com', 'doubleclick.net',
        'googlesyndication.com', 'googleadservices.com', 'google-analytics.com',
        'googletagservices.com', 'google.com/ads', // 'gstatic.com', (не грузится promt Gemini)
        'facebook.com', 'connect.facebook.net', 'fbcdn.net', 'fb.com',
        'scorecardresearch.com', 'quantserve.com', 'outbrain.com', 'taboola.com',
        'moatads.com', 'adnxs.com', 'openx.net', 'adtelligent.com',
        'advertising.com', 'adsrvr.org', 'rubiconproject.com', 'pubmatic.com',
        'casalemedia.com', 'smartadserver.com', 'appnexus.com', 'criteo.com',
        'bidswitch.net', 'rlcdn.com', 'bluekai.com', 'demdex.net',
        'amazon-adsystem.com', 'adcolony.com', 'media.net', '33across.com',
        // ── Аналитика и метрики ──────────────────────────────────────
        'clarity.ms', 'hotjar.com', 'mixpanel.com', 'segment.io', 'segment.com',
        'heap.io', 'heapanalytics.com', 'amplitude.com', 'fullstory.com',
        'logrocket.com', 'mouseflow.com', 'inspectlet.com', 'clicktale.net',
        'contentsquare.net', 'optimizely.com', 'crazy-egg.com',
        // ── Российские/СНГ-трекеры ───────────────────────────────────
        'mc.yandex.ru', 'mc.yandex.net', 'top-fwz1.mail.ru',
        'redirect.appmetrica.yandex.com', 'counter.yadro.ru',
        'cnt.tbz.liveinternet.ru', 'open.rambler.ru',
        'ads.vk.com', 'vk.com/rtr', 'target.my.com', 'mail.ru/counter',
        'rb.mail.ru', 'tbgcounter.com',
        // ── Сессионные записи и CX-аналитика ────────────────────────
        'static.hotjar.com', 'vc.hotjar.io',
        'stats.g.doubleclick.net',
        'bat.bing.com', 'ad.doubleclick.net', 'ad.atdmt.com',
        // ── Рекламные сети ───────────────────────────────────────────
        'adsymptotic.com', 'creativecdn.com', 'go.sonobi.com',
        'snigelweb.com', 'sharethrough.com', 'triplelift.com',
        'yieldmo.com', 'yieldlab.net', 'smartclip.net',
        'spoutable.com', 'undertone.com', 'indexexchange.com',
        'sovrn.com', 'lijit.com', 'contextweb.com', 'pulsepoint.com',
        // ── Push-уведомления и подписки ──────────────────────────────
        'onesignal.com', 'pusher.com', 'pushcrew.com',
        'aimtell.com', 'subscribers.com', 'pushassist.com',
        // ── Прочие трекеры ───────────────────────────────────────────
        'chartbeat.com', 'chartbeat.net', 'krxd.net',
        'clickagy.com', 'agkn.com', 'exelator.com', 'eyeota.net',
        'spotxchange.com', 'cxense.com', 'adobedtm.com', 'omtrdc.net',
        '2mdn.net', 'hlserve.com', 'cdn.syndication.twimg.com',
    ];

    // Домены-исключения — НЕ удаляем их элементы из DOM даже если совпадают с трекером.
    // Добавлять сюда сервисы, которые ломаются при агрессивной фильтрации DOM
    // (капчи, антибот-проверки, платёжные виджеты и т.п.)
    const TRACKER_EXCEPTIONS = [
        'cloudflare.com',            // Cloudflare Challenge / Turnstile капча
        'challenges.cloudflare.com', // прямой домен капчи Cloudflare
        // 'recaptcha.net',          // Google reCAPTCHA — раскомментировать если сломается
        // 'hcaptcha.com',           // hCaptcha — раскомментировать если сломается
    ];

    // Проверка: является ли URL трекером
    const isTracker = url => {
        try {
            const h = new URL(url, location.origin).hostname;
            return TRACKERS.some(t => h.endsWith(t));
        } catch { return false; }
    };

    // Проверка: находится ли URL в списке исключений (не трогаем даже если трекер)
    const isException = url => {
        try {
            const h = new URL(url, location.origin).hostname;
            return TRACKER_EXCEPTIONS.some(e => h.endsWith(e));
        } catch { return false; }
    };

    // Блокируем sendBeacon трекеров — исключения не в TRACKERS, отдельно не нужны
    const _beacon = navigator.sendBeacon.bind(navigator);
    navigator.sendBeacon = (url, data) => isTracker(url) ? false : _beacon(url, data);

    // font-display:swap — текст виден сразу системным шрифтом, веб-шрифт подгружается потом
    const _fs = document.createElement('style');
    _fs.textContent = '@font-face{font-display:swap}';
    (document.head || document.documentElement).appendChild(_fs);

    /* ── EXTREME MODE: ранние оптимизации ──────────────────────────────────────
     * Применяются ДО проверки SITE_KEY — чтобы работали даже при OFF-состоянии
     * кнопки если EXTREME_MODE=true глобально.
     *
     * ИСТОЧНИКИ IDEAS (с указанием эффекта):
     * • Fake Save-Data+slow-2g (Grok): сайты сами снижают качество картинок,
     *   отключают автозапуск каруселей и видео-фонов. Нет DOM-вмешательства.
     * • Aggressive CSS (Grok+DeepSeek+Claude): отключает GPU-тяжёлые эффекты
     *   (filter, backdrop-filter, box-shadow, text-shadow, background-image).
     *   Дизайн становится «плоским», но сайт функционирует корректно.
     * • Удаление link[rel*=pre] кросс-домен (Grok+Claude+DeepSeek): убирает
     *   5–15 лишних запросов на старте страницы.
     * • srcset removal (ChatGPT+Claude): экономит трафик на новостных/фото
     *   сайтах, исключая выбор тяжёлой версии изображения.
     * • preload=none для видео (Claude+DeepSeek): не качаем видео пока не нужно.
     * • decoding=async (Qwen+ChatGPT): снимает блокировку main thread при
     *   декодировании изображений (−5–10% jank при массовых картинках).
     * • autofocus removal (Qwen+ChatGPT): предотвращает forced layout + scroll-прыжок.
     * • spellcheck=false (Qwen+ChatGPT): снижает CPU при вводе текста.
     * • border-radius:0 (DeepSeek): ускоряет отрисовку на слабых GPU.
     * • fetchPriority=low для iframe (Qwen+ChatGPT): iframe — один из тяжёлых ресурсов.
     ────────────────────────────────────────────────────────────────────────── */
    if (EXTREME_MODE) {
        // Fake Save-Data + slow-2g: сайты сами адаптируют контент
        // Единственный способ заставить СЕРВЕР экономить трафик без изменения DOM
        try {
            const _fakeConn = { effectiveType: 'slow-2g', saveData: true, rtt: 2200, downlink: 0.05 };
            Object.defineProperty(navigator, 'connection', { value: _fakeConn, configurable: true });
            log('[IHW Extreme] Fake Save-Data + slow-2g активирован');
        } catch (e) {}

        // Aggressive CSS: отключаем GPU-тяжёлые эффекты и декоративные элементы
        // Nemotron п.5: добавляем класс на <html> вместо голого `* { }`.
        // Браузер начинает поиск совпадений с корня и может пропустить
        // поддеревья без целевых элементов — это быстрее чем матч каждого узла.
        // Также упрощает отладку: `html.ihw-extreme` видно в DevTools.
        // Null guard: при run-at:document-start в Firefox documentElement может
        // ещё не существовать при Ctrl+Shift+R — без guard скрипт падает и кнопка исчезает
        document.documentElement?.classList.add('ihw-extreme');

        const _xCss = document.createElement('style');
        _xCss.textContent =
            // Удаляем фоны, тени, фильтры — основная нагрузка на GPU
            'html.ihw-extreme *,html.ihw-extreme :before,html.ihw-extreme :after{' +
            'background-image:none!important;filter:none!important;' +
            'backdrop-filter:none!important;box-shadow:none!important;' +
            'text-shadow:none!important;border-radius:0!important;' +
            // Отключаем анимации и переходы
            'animation:none!important;transition:none!important}' +
            'html.ihw-extreme img,html.ihw-extreme video{image-rendering:crisp-edges!important}';
        (document.head || document.documentElement).appendChild(_xCss);
        log('[IHW Extreme] Агрессивный CSS активирован');

        // preload=none для всех медиа — не качаем видео/аудио до клика
        document.querySelectorAll('video,audio').forEach(m => {
            m.preload = 'none'; m.autoplay = false;
        });

        // autofocus removal — предотвращает forced layout + scroll-прыжок при загрузке
        document.querySelectorAll('[autofocus]').forEach(el => el.removeAttribute('autofocus'));

        // spellcheck=false — снижает CPU при вводе текста (особенно соцсети/форумы)
        document.querySelectorAll('input,textarea,[contenteditable]')
            .forEach(el => { el.spellcheck = false; });

        log('[IHW Extreme] Режим активирован — трафик и GPU экономятся');
    }

    // ---------- CSS: принудительная видимость и опционально отключение плавного скролла ----------

    /* ── СПИСОК САЙТОВ-ЧАТОВ ────────────────────────────── */
    // Используется тремя блоками: scroll-behavior, content-visibility, lazy-iframe.
    // БАГ v1.1.5: content-visibility:auto на main>div и lazy-iframe ломают
    // динамический скролл к полю ввода (поле "улетает" вверх после отправки).
    // FIX v1.1.6: для этих сайтов пропускаем runRenderOpts() и iframe-lazy целиком.
    const noScrollBehaviorSites = [
        'chat.deepseek.com',
        'chatgpt.com',
        'grok.com',
        'qwen.ai',
        'claude.ai',
        'claude.site',
        'kagi.com',       // другие чаты с динамическим скроллом
        'perplexity.ai',
        'alice.yandex.ru',
		'openrouter.ai',
		'chat.mistral.ai',

    ];

    const currentHost = window.location.hostname;
    // shouldSkipScroll === true означает: это AI-чат с динамическим скроллом.
    // На таких сайтах НЕ отключаем smooth scroll, НЕ применяем content-visibility,
    // НЕ делаем lazy-iframe — всё это ломает поле ввода.
    const shouldSkipScroll = noScrollBehaviorSites.some(site => currentHost.includes(site));

    // Базовый стиль: принудительная видимость (идея из PureRender)
    // scroll-behavior:auto — отменяет smooth scroll сайта, скролл реагирует 1-в-1.
    // visibility/opacity — некоторые сайты прячут body до загрузки рекламы/попапов,
    // это принудительно раскрывает страницу сразу.
    const baseCss = 'html,body{visibility:visible!important;opacity:1!important}';
    const _css = document.createElement('style');
    _css.textContent = baseCss;
    (document.head || document.documentElement).appendChild(_css);

    // Дополнительный стиль: отключение плавного скролла (если сайт не в исключениях)
    if (!shouldSkipScroll) {
        const scrollCss = 'html,body{scroll-behavior:auto!important}';
        const _scrollCss = document.createElement('style');
        _scrollCss.textContent = scrollCss;
        (document.head || document.documentElement).appendChild(_scrollCss);
    }

    /* ── НАБЛЮДАТЕЛЬ ЗА DOM ─────────────────────────────── */
    const seen = new WeakSet();

    const processNode = node => {
        if (!(node instanceof HTMLElement) || seen.has(node)) return;
        seen.add(node);

        const tag = node.tagName;
        const src = node.src || node.href || '';

        // Трекеры — удаляем при вставке в DOM, но только если не в списке исключений
        if (src && isTracker(src) && !isException(src)) { node.remove(); _blockedCount++; return; }

        // prefetch-ссылки — удаляем (не тратим ресурсы впрок),
        // кроме исключений (Cloudflare Challenge использует prefetch для своей проверки).
        // На видеохостингах: сохраняем same-origin prefetch — это SPA-навигация
        // (YouTube, VK и др. префетчат JSON следующей вкладки; без них вкладки
        // «Videos», «Streams» и переходы между страницами остаются пустыми).
        if (tag === 'LINK' && node.rel === 'prefetch' && !isException(src)) {
            if (isVideoHost) {
                try { if (new URL(src, location.origin).hostname.endsWith(location.hostname)) return; } catch {}
            }
            node.remove(); _blockedCount++; return;
        }

        // Внешние шрифты — откладываем, не блокируем рендер текста
        // ── Приоритет canplay > шрифты (Grok+Qwen+DeepSeek, v2.2.5.1) ─────
        //   Mixed Content  → 2000 мс: LCP/FCP в приоритете, шрифты грузятся быстро
        //   Video Content  → 6000 мс: видео-сегменты не конкурируют со шрифтами
        //   YouTube стратегия 1: _deferFontsUntilCanplay(main) уже управляет шрифтами;
        //     атрибут data-ihwFontDeferred блокирует наш setTimeout (нет конфликта)

		// 		Вместо (Google Fonts): /fonts\.g(oogle|static)apis\.com/.test(src)
		// 		расширяем список возможных внешних шрифтов для откладывая загрузки: Roboto, Open Sans; Adobe Fonts; GDPR-альтернатива Google Fonts; Monotype; cloudflare, jsDelivr, unpkg.
		// 		
		if (tag === 'LINK' && /fonts\.(googleapis|gstatic|bunny\.net)|use\.typekit\.net|fast\.fonts\.net/.test(src)) {
			if (node.dataset.ihwFontDeferred) return;
			node.media = 'print';
			const _fd = PAGE === 'Video Content' ? 6000 : 2000;
			log(`[IHW Font] отложен ${_fd}мс: ${src.split('?')[0].slice(-40)}`);
			setTimeout(() => {
				if (node.parentNode && !node.dataset.ihwFontDeferred) {
					node.media = 'all';
					log('[IHW Font] восстановлен (таймер)');
				}
			}, _fd);
			return;
		}

        // Изображения — ленивая загрузка + async decoding
        if (tag === 'IMG') {
            if (!node.loading) node.loading = 'lazy';
            node.decoding = 'async'; // снимает блокировку main thread (Qwen+ChatGPT)
        }

        // autofocus — предотвращает forced layout + scroll-прыжок на всех режимах (Qwen+ChatGPT)
        // Безопасно: браузер по-прежнему фокусирует элемент при явном вызове .focus()
        if (node.hasAttribute && node.hasAttribute('autofocus')) node.removeAttribute('autofocus');

        // fetchPriority=low для не-плеерных iframe вне первого экрана (Qwen+ChatGPT)
        // Применяем только если iframe не является главным плеером (проверяем размер)
        if (tag === 'IFRAME' && !node.fetchPriority && !isVideoHost) {
            if (node.offsetWidth < 200 || node.offsetHeight < 100) node.fetchPriority = 'low';
        }

        // ── EXTREME MODE: дополнительная обработка новых узлов ───────────────
        if (EXTREME_MODE) {
            // srcset removal — экономия трафика (ChatGPT+Claude)
            if (tag === 'IMG' && node.hasAttribute('srcset')) node.removeAttribute('srcset');

            // Кросс-доменные resource hints — убираем 5–15 лишних запросов (Grok+DeepSeek+Claude)
            // dns-prefetch добавлен по замечанию DeepSeek/Grok к v2.0.0
            if (tag === 'LINK') {
                const rel = node.getAttribute('rel') || '';
                if (/preload|preconnect|modulepreload|dns-prefetch/.test(rel)) {
                    try { if (!new URL(node.href||'',location.origin).hostname.endsWith(location.hostname)) { node.remove(); _blockedCount++; return; } } catch {}
                }
            }

            // fetchPriority=low для медиа-тегов (кроме IFRAME — уже в общем processNode)
			// кроме главного видео с установленным нашим тэгом
            if (['IMG','VIDEO','AUDIO','SCRIPT'].includes(tag) && !node.fetchPriority) {
					if (!(tag === 'VIDEO' && node.hasAttribute('data-ihw-boosted'))) node.fetchPriority = 'low';
                    }

            // preload=none для нового медиа, кроме главного ивдео с заданым нашим атрибутом 
			// Работает даже при асинхронном порядке – если processNode сработает после boostMainVideo, атрибут уже будет, и сброс не произойдёт. Если до – то boostMainVideo потом переставит preload='auto' (и добавит атрибут), и processNode при повторном вызове (если он будет) уже не тронет видео.			
            if (tag === 'VIDEO' || tag === 'AUDIO') {
				// Не сбрасываем preload, если видео уже помечено как "главное"
                if (!node.hasAttribute('data-ihw-boosted')) {
					node.preload = 'none';
					node.autoplay = false;
				}
            }

            // spellcheck + contenteditable — снижает CPU при вводе (Qwen+ChatGPT)
            // contenteditable добавлен по замечанию Qwen к v2.0.0
            if (tag === 'INPUT' || tag === 'TEXTAREA' || node.hasAttribute('contenteditable'))
                node.spellcheck = false;
        }
    };

    const mo = new MutationObserver(muts => {
        for (const m of muts)
            for (const n of m.addedNodes) processNode(n);
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });

    /* ── ОБЩИЕ ОПТИМИЗАЦИИ РЕНДЕРА (Desktop + Mobile) ───── */
    // Запускаем на idle ПОСЛЕ поиска видео — иначе content-visibility:auto
    // обнуляет offsetWidth плеера и tryBoost его не находит.
    // FIX v1.1.6: на сайтах-чатах (shouldSkipScroll) не применяем content-visibility
    // и не убираем srcset — эти оптимизации ломают динамический скролл чата.
    // Причина: content-visibility:auto на main>div меняет layout-высоту
    // контейнера сообщений → containIntrinsicSize-заглушки (0 500px) сбивают
    // scroll anchor → поле ввода «улетает» вверх экрана.
    const runRenderOpts = () => {
        if (shouldSkipScroll) {
            log('[IHW] runRenderOpts: пропущено (AI-чат, shouldSkipScroll=true)');
            return;
        }
        const vh = window.innerHeight;

        // ── Фаза чтения: все getBoundingClientRect за один проход ─────────────
        // Разделяем чтение (BCR → reflow) и запись (style) чтобы не вызывать
        // "layout thrashing" (forced reflow на каждой итерации forEach).
        const imgs = [...document.querySelectorAll('img[srcset]')];
        const imgTops = imgs.map(img => img.getBoundingClientRect().top);

        const blocks = [...document.querySelectorAll('article,section,.post,.content,.entry,main>div')];
        const blockTops = blocks.map(el => el.getBoundingClientRect().top);

        // ── Фаза записи: меняем DOM только после того, как все позиции прочитаны ──
        // srcset у картинок вне вьюпорта — убираем, браузер не грузит тяжёлую версию впрок
        imgs.forEach((img, i) => { if (imgTops[i] > vh) img.removeAttribute('srcset'); });

        // content-visibility:auto на блоках ниже 2 экранов — пропускаем рендер того
        // что пользователь ещё не видит; плееры исключаем чтобы не обнулить их размер
        blocks.forEach((el, i) => {
            if (i > 1 && blockTops[i] > vh * 2
                && !el.querySelector('video,iframe,[class*="player"],[id*="player"]')) {
                el.style.cssText += ';content-visibility:auto;contain-intrinsic-size:0 500px';
            }
        });
    };

    /* ── Page: "Video Content" ──────────────────────────── */
    // Цель: найти главное видео (Detect & Play: Main Video on Page)
    // и запустить его как можно быстрее — это главный приоритет
    if (PAGE === 'Video Content') {
            initVideoPreconnect(); // preconnect к CDN для всех стратегий (Qwen+Grok+DeepSeek)

            /* ── _deferFontsUntilCanplay(): canplay > шрифты ────────────────────
             * Переводит шрифтовые <link> в media="print" — браузер парсит CSS,
             * но НЕ качает файлы шрифтов, текст рендерится системным шрифтом.
             * Освобождает HTTP/2-слоты и bandwidth для первых видео-сегментов.
             * После canplay: media="all" + fetchPriority="low" (фон).
             * Страховка 6с: если canplay не придёт — восстанавливаем шрифты.
             * Работает на всех видеохостингах: Rutube, VK, OK, Vimeo и др. (Qwen)
             ─────────────────────────────────────────────────────────────────── */
            function _deferFontsUntilCanplay(mainVideo) {
				const fontLinks = document.querySelectorAll(
					'link[href*="fonts.googleapis"], link[href*="fonts.gstatic"], ' +
					'link[href*="bunny.net"], link[href*="typekit.net"], link[href*="fast.fonts.net"], ' +
					'link[rel="preload"][as="font"]'
				);
                if (!fontLinks.length) return;
				
				// ✅ ЛОГ 1: сколько шрифтов найдено
				log(`[IHW Font] найдено ${fontLinks.length} шрифт(ов) → ждём canplay`);
				
                const _restore = () => {
                    clearTimeout(_timer);
                    mainVideo.removeEventListener('error', _restore);
                    fontLinks.forEach(lnk => {
                        if (!lnk.dataset.ihwFontDeferred) return;
						
						// ── ИЗМЕНЕНИЕ: сначала снимаем флаг, потом восстанавливаем ──
						// Это гарантирует, что любой параллельный processNode увидит
						// чистое состояние до того, как браузер начнёт грузить шрифт.
                        delete lnk.dataset.ihwFontDeferred;
                        delete lnk.dataset.ihwOrigMedia;
						// Теперь восстанавливаем media — браузер начнёт загрузку шрифта
                        lnk.media = lnk.dataset.ihwOrigMedia || 'all';
                        if (lnk.rel === 'preload' && lnk.as === 'font') lnk.fetchPriority = 'low';
                    });
                    log('[IHW Video] Шрифты восстановлены (canplay/fallback)');
                };
                const _timer = setTimeout(_restore, 6000); // страховка: если canplay не придёт
                mainVideo.addEventListener('canplay', _restore, { once: true });
                mainVideo.addEventListener('error',   _restore, { once: true });
                fontLinks.forEach(lnk => {
                    lnk.dataset.ihwOrigMedia    = lnk.media || 'all';
                    lnk.media                   = 'print'; // не качать, использовать системный шрифт
                    lnk.dataset.ihwFontDeferred = '1';
                });
                log('[IHW Video] Шрифты отложены до canplay (bandwidth для видео)');
            }

        // Заглушка внутренней аналитики YouTube (ytcsi — YouTube Client Side Instrumentation).
        // Эта система собирает тайминги и метрики в фоне, отправляет данные на серверы YouTube.
        // Замена на noop экономит CPU на каждой странице. (идея из PureYouTube)
        // Применяем только на видеохостингах — на обычных сайтах ytcsi нет.
        if (location.hostname.endsWith('youtube.com') || location.hostname.endsWith('youtu.be')) {
            const noop = () => {};
            window.ytcsi = { tick: noop, span: noop, info: noop, setTick: noop, lastTick: noop };
            window.ytStats = noop;
            log('[IHW] YouTube аналитика (ytcsi) заглушена');

            // Дополнительная заглушка yt.config_: отключаем внутреннее логирование YouTube.
            // ENABLE_LOGGING = false — безопасно, только блокирует отправку телеметрии.
            // ADS_DATA НЕ трогаем — YouTube использует его для внутренней логики страниц
            // (не только рекламы); обнуление ломает SPA-рендер вкладок «Видео»/«Трансляции».
            // YouTube устанавливает yt.config_ через inline-скрипты в <head>, поэтому
            // патчим после DOMContentLoaded, когда объект уже создан.
            // CSS will-change:transform на #masthead-container подсказывает браузеру
            // поднять шапку на отдельный GPU-слой — убирает дёргание при скролле.
            const _ytBootstrap = () => {
                try {
                    if (window.yt?.config_) {
                        window.yt.config_.ENABLE_LOGGING = false;
                        log('[IHW] YouTube yt.config_.ENABLE_LOGGING заглушен');
                    }
                } catch (e) { log('[IHW] yt.config_ недоступен:', e); }

                // CSS: masthead GPU + убираем placeholder превью + отключаем ambient blur.
                // Ambient mode — тяжёлый backdrop-filter на всю страницу, часто не замечается.
                // CSS containment на comments/sidebar — изменения внутри не ломают весь layout.
                // Firefox: scrollbar-width:thin экономит ~15px layout + уменьшает repaint-область.
                // ── Shorts + Ads: CSS-блокировка до первого paint (Qwen+Grok) ──
                // ytd-* компоненты существуют ТОЛЬКО на youtube.com → безопасно на других сайтах.
                // CSS graceful degradation: если YouTube поменяет теги — просто перестанет работать,
                // сайт останется функциональным. Проверять раз в квартал.
                let _ytCss = 'ytd-masthead,#masthead-container{will-change:transform}'
                    // Shorts: скрываем полки до рендера (браузер не строит layout для них)
                    + 'ytd-rich-shelf-renderer[is-shorts],ytd-reel-shelf-renderer,#shorts-container{display:none!important}'
                    // Реклама: блокируем на уровне CSS, раньше чем uBlock/AdGuard
                    + 'ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-promoted-video-renderer,#player-ads{display:none!important}'
                    + 'yt-img-shadow{background-color:transparent!important}'
                    + '.ytp-ambient-light,.ytp-ambient-mode-enabled,'
                    + 'ytd-watch-flexy[ambient-mode-enabled] .ytp-ambient-light{display:none!important}'
                    + 'ytd-watch-flexy,#cinematics{backdrop-filter:none!important}'
                    + '#comments,#secondary,ytd-watch-next-secondary-results-renderer'
                    + '{contain:layout style paint}';
                if (isFirefox) _ytCss += 'html{scrollbar-width:thin}';
                const _yt = document.createElement('style');
                _yt.textContent = _ytCss;
                document.head.appendChild(_yt);
                log('[IHW] YouTube: masthead GPU + ambient off + CSS containment');

                // EXPERIMENT_FLAGS: отключаем анимации и cinematic эффекты через
                // внутренний конфиг YouTube (безопаснее CSS — работает до рендера).
                // Идея: YT CPU Enhancer (greasyfork #552190).
                try {
                    const _expf = window.yt?.config_?.EXPERIMENT_FLAGS;
                    if (_expf && typeof _expf === 'object') {
                        Object.assign(_expf, {
                            web_animated_actions:                  false,
                            web_animated_like:                     false,
                            kevlar_watch_cinematics:               false,
                            web_cinematic_theater_mode:            false,
                            web_cinematic_fullscreen:              false,
                            enable_cinematic_blur_desktop_loading: false,
                            kevlar_measure_ambient_mode_idle:      false,
                            smartimation_background:               false,
                        });
                        log('[IHW] YouTube EXPERIMENT_FLAGS: ambient/cinematic отключены');
                    }
                } catch (e) { log('[IHW] EXPERIMENT_FLAGS недоступен:', e); }

                // Lazy-load comments: скрываем ytd-comments пока не доскроллили.
                // contentVisibility:hidden — не рендерим совсем (сильнее чем auto).
                // Идея: YouTube Performance CPU Optimized (greasyfork #548105).
                const _cmts = document.querySelector('ytd-comments#comments');
                if (_cmts) {
                    _cmts.style.contentVisibility = 'hidden';
                    new IntersectionObserver(entries => {
                        if (entries[0].isIntersecting) { _cmts.style.contentVisibility = ''; }
                    }, { rootMargin: '200px' }).observe(_cmts);
                }
            };
            // Если DOM уже готов (SPA-навигация) — сразу, иначе ждём
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', _ytBootstrap, { once: true });
            } else {
                _ytBootstrap();
            }
        }

        const boostMainVideo = () => {
            log('[IHW] Главное видео ищется на странице...');

            // --- Стратегия 1: нативный <video> ---
            const videos = [...document.querySelectorAll('video')];
            // Фильтруем видео: видимые по размеру + не дальше 1.5 экрана вниз
            // (исключаем плееры которые рендерятся вне viewport, но технически имеют размер)
            const visibleVideos = videos.filter(v =>
                v.offsetWidth > 0 && v.offsetHeight > 0 &&
                v.getBoundingClientRect().top < window.innerHeight * 1.5
            );

            if (visibleVideos.length) {
                const main = visibleVideos.reduce((a, b) =>
                    (b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a
                );
                log(`[IHW] Главное видео найдено (<video>): ${main.offsetWidth}x${main.offsetHeight}px | src: ${main.src || main.currentSrc || '(загружается)'}`);
				main.setAttribute('data-ihw-boosted', 'true');   // маркер главного видео (защита от EXTREME)
                main.preload = 'auto';
                main.fetchPriority = 'high';
				initDynamicPreconnect(main);
                _deferFontsUntilCanplay(main); // шрифты ждут canplay — bandwidth для видео
				
                // ── POSTER PRELOAD: улучшает LCP — poster часто является крупнейшим элементом (Grok+Qwen)
                if (main.poster) {
                    try {
                        const _pl = document.createElement('link');
                        _pl.rel = 'preload'; _pl.as = 'image';
                        _pl.href = main.poster; _pl.fetchPriority = 'high';
                        document.head.appendChild(_pl);
                        log('[IHW Video] poster preload + high priority');
                    } catch(e) {}
                }

                // ── SOURCE HIGH PRIORITY: fetchPriority=high для дочерних <source> (Qwen)
                main.querySelectorAll('source').forEach(src => {
                    if (!src.fetchPriority) src.fetchPriority = 'high';
                });

                // ── HLS MANIFEST PRELOAD: предзагрузка манифеста → плеер быстрее получает первый сегмент (Grok+Qwen+DeepSeek)
                // Эффект: +400–900 мс к первому сегменту на медленных соединениях
                const _videoSrc = main.src || main.currentSrc || '';
                if (_videoSrc.includes('.m3u8')) {
                    try {
                        const _ml = document.createElement('link');
                        _ml.rel = 'preload'; _ml.as = 'fetch';
                        _ml.fetchPriority = 'high'; _ml.href = _videoSrc;
                        _ml.crossOrigin = 'anonymous';
                        document.head.appendChild(_ml);
                        log('[IHW Video] HLS manifest preloaded');
                    } catch(e) {}
                }

				// iOS Safari: playsinline предотвращает принудительный fullscreen при play().
                // Без атрибута iOS разворачивает видео на весь экран — нежелательно на
                // сайтах с нативным плеером (VK, OK, Dzen). Безвреден на Desktop.
                main.setAttribute('playsinline', '');
                visibleVideos.forEach(v => { if (v !== main && v.preload === 'auto') v.preload = 'metadata'; });
                if (main.readyState >= 2) {
                    log('[IHW] Главное видео: готово (readyState=' + main.readyState + '), запускаем');
                    main.play().catch(e => log('[IHW] play() заблокирован —', e.message));
                    _canplayMs = Math.round(performance.now());
                    if (DEBUG) console.log(`[IHW] Видео готово: ${_canplayMs} мс от навигации (readyState уже ≥ 2)`);
                } else {
                    log('[IHW] Главное видео: ожидаем canplay (readyState=' + main.readyState + ')');
                    const _vt0 = performance.now();
                    main.addEventListener('canplay', () => {
                        log('[IHW] canplay сработал, запускаем воспроизведение');
                        main.play().catch(e => log('[IHW] play() заблокирован —', e.message));
                        // ── SEEK HACK: микро-сдвиг триггерит буферизацию следующего сегмента (Grok+DeepSeek)
                        // Не применяем на YouTube — нестабильно из-за частых изменений плеера
						if (SEEK_HACK && main.duration > 10 && !isNaN(main.duration)) {
							// Проверка, не входит ли текущий хост в список исключений
							const isExcluded = SEEK_HACK_EXCLUDE.some(host => location.hostname.includes(host));
							if (!isExcluded) {
								const _st = main.currentTime;
								main.currentTime = _st + 0.01;
								setTimeout(() => { try { main.currentTime = _st; } catch(e) {} }, 50);
								log('[IHW Video] seek-hack applied');
							}
						}
                        _canplayMs = Math.round(performance.now() - _vt0);
                        if (DEBUG) console.log(`[IHW] Видео готово за: ${_canplayMs} мс (canplay от старта поиска)`);
                    }, { once: true });
                }
                return true;
            }

            // --- Стратегия 2: плеер в iframe cross-origin ---
            const playerIframes = [...document.querySelectorAll('iframe')].filter(fr => {
                if (fr.offsetWidth < 200 || fr.offsetHeight < 100) return false;
                const s = (fr.src || fr.name || fr.id || fr.className || '').toLowerCase();
                return /video|player|embed|rutube|vimeo|vk|ok\.ru|dzen|yandex|twitch|dailymotion|bilibili|tiktok/i.test(s);
            });
            if (playerIframes.length) {
                const main = playerIframes.reduce((a, b) =>
                    (b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a
                );
                log(`[IHW] Главное видео найдено (<iframe> плеер): ${main.offsetWidth}x${main.offsetHeight}px | src: ${main.src || '(нет src)'}`);
                if (main.dataset.lazySrc) { main.src = main.dataset.lazySrc; delete main.dataset.lazySrc; }
				main.setAttribute('data-ihw-boosted', 'true');   // атрибут найденного главного видео
                main.loading = 'eager';
                main.fetchPriority = 'high';
                return true;
            }

            // --- Стратегия 3: кастомный плеер (div с data-атрибутами: VK,Rutube,OK) ---
            const customPlayer = document.querySelector(
                '[class*="player"],[id*="player"],[class*="Player"],[id*="Player"],' +
                '[data-video],[data-player],[data-src*="video"]'
            );
            if (customPlayer && customPlayer.offsetWidth > 200) {
                log(`[IHW] Главное видео найдено (кастомный плеер): ${customPlayer.tagName}#${customPlayer.id}.${customPlayer.className.split(' ')[0]} | ${customPlayer.offsetWidth}x${customPlayer.offsetHeight}px`);
				customPlayer.setAttribute('data-ihw-boosted', 'true');   // атрибут найденного главного видео
                return true;
            }

            if (videos.length) {
                log(`[IHW] Главное видео: найдено ${videos.length} <video>, но все нулевого размера (плеер ещё не отрисован)`);
            } else {
                log('[IHW] Главное видео: не найдено ни <video>, ни iframe-плеера, ни кастомного плеера');
            }
            return false;
        };

        // Повторяем поиск с нарастающим интервалом — плеер в SPA может появиться через 3–8с
        // Экспоненциальный backoff: 1500 → 3000 → 6000 → 8000 мс (Nemotron p.4)
        // Без массива — масштабируется чище, проще сбрасывать при SPA-навигации.
        // Math.min ограничивает максимальный интервал 8000 мс.
        const _MAX_BOOST_ATTEMPTS = 4;
        let _boostAttempt = 0;

        const tryBoost = () => {
            if (boostMainVideo()) return;
            if (_boostAttempt >= _MAX_BOOST_ATTEMPTS) {
                log('[IHW] Главное видео: все попытки исчерпаны');
                return;
            }
            const delay = Math.min(1500 * Math.pow(2, _boostAttempt), 8000);
            _boostAttempt++;
            log(`[IHW] Главное видео: повтор через ${delay / 1000}с (попытка ${_boostAttempt}/${_MAX_BOOST_ATTEMPTS})`);
            setTimeout(tryBoost, delay);
        };

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', tryBoost, { once: true });
        } else {
            tryBoost();
        }

        // Пауза при скрытии вкладки — экономит CPU/GPU/АКБ.
        // Работает только для плееров с <video> в ГЛАВНОМ документе страницы:
        // YouTube и Twitch — нативный <video> доступен через querySelectorAll в main DOM.
        // Остальные хостинги (VK, OK, Bilibili, TikTok, Dzen и др.) держат плеер в
        // cross-origin iframe — браузер запрещает доступ к чужому документу из userscript.
        // PiP: document.pictureInPictureElement → видео в плавающем окне → не трогаем.
        // _ihw_wasPlaying — не возобновляем видео которое пользователь сам поставил на паузу.
        if (PAUSE_ON_HIDDEN) {
            document.addEventListener('visibilitychange', () => {
                // Ищем самый крупный видимый <video> — тот же критерий что и в boostMainVideo
                const vs = [...document.querySelectorAll('video')]
                    .filter(v => v.offsetWidth > 0 && v.offsetHeight > 0);
                if (!vs.length) return;
                const main = vs.reduce((a, b) =>
                    b.offsetWidth * b.offsetHeight > a.offsetWidth * a.offsetHeight ? b : a
                );
                // PiP активен — не вмешиваемся
                if (document.pictureInPictureElement) return;

                if (document.hidden) {
                    main._ihw_wasPlaying = !main.paused;
                    if (main._ihw_wasPlaying) {
                        main.pause();
                        log('[IHW] visibilitychange: вкладка скрыта → пауза');
                    }
                } else {
                    if (main._ihw_wasPlaying) {
                        // preload='auto' остался с boostMainVideo — не меняли при паузе.
                        // fetchPriority повторно ставим в 'high': после паузы браузер
                        // мог понизить приоритет сетевых запросов видеоэлемента.
                        main.fetchPriority = 'high';
                        main.play().catch(e => log('[IHW] visibilitychange: resume заблокирован —', e.message));
                        log('[IHW] visibilitychange: вкладка активна → воспроизведение');
                    }
                    main._ihw_wasPlaying = undefined;
                }
            });
        }
    }

    /* ── Page: "Mixed Content" ──────────────────────────── */
    // 1-й приоритет: текст + базовые стили (браузер делает сам)
    // 2-й приоритет: картинки первого экрана — eager + fetchPriority:high
    // 3-й приоритет: всё за экраном — lazy, iframe откладываем на скролл
    if (PAGE === 'Mixed Content') {

        // Восстанавливаем iframe при приближении (запас 300px до появления)
        const lazyIframeObserver = new IntersectionObserver(entries => {
            for (const e of entries) {
                if (e.isIntersecting && e.target.dataset.lazySrc) {
                    e.target.src = e.target.dataset.lazySrc;
                    delete e.target.dataset.lazySrc;
                    lazyIframeObserver.unobserve(e.target);
                }
            }
        }, { rootMargin: '300px' });

        const initMixed = () => {
            const vh = window.innerHeight;

            document.querySelectorAll('img').forEach(img => {
                const top = img.getBoundingClientRect().top;
                if (top <= vh) {
                    img.loading = 'eager';
                    img.decoding = 'async';
                    img.fetchPriority = 'high';
                } else {
                    img.loading = 'lazy';
                }
            });

            // FIX v1.1.6: пропускаем lazy-iframe на AI-чатах.
            // ChatGPT / Claude / Qwen могут использовать iframe для поля ввода
            // или вспомогательных компонентов. Удаление src у таких iframe приводит к
            // тому, что при программном скролле чата браузер не находит целевой элемент,
            // и поле ввода "улетает" вверх страницы.
            if (!shouldSkipScroll) {
                // Батчинг BCR: читаем позиции до записи — нет принудительного reflow (DeepSeek)
                const _iframes = [...document.querySelectorAll('iframe')];
                const _iframeTops = _iframes.map(fr => fr.getBoundingClientRect().top);
                _iframes.forEach((fr, i) => {
                    if (_iframeTops[i] > vh * 2 && fr.src) {
                        fr.dataset.lazySrc = fr.src;
                        fr.removeAttribute('src');
                        lazyIframeObserver.observe(fr);
                    }
                });
            } else {
                log('[IHW] initMixed: lazy-iframe пропущено (AI-чат)');
            }

            document.querySelectorAll('video').forEach(v => {
                v.autoplay = false;
                v.preload = 'metadata';
            });
        };

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initMixed, { once: true });
        } else {
            initMixed();
        }
    }

    /* ── Mobile: специфичные оптимизации ───────────────── */
    if (isMobile) {
        // Hover-анимации — на сенсоре не нужны, потребляют CPU при скролле
        const _mh = document.createElement('style');
        _mh.textContent = '@media(hover:none){*{transition:none!important;animation:none!important}}'
            // touch-action: manipulation — убирает 300 мс задержку клика на сенсорных экранах.
            // Браузер больше не ждёт «может это двойной тап?» перед обработкой одиночного клика.
            // Применяем только к интерактивным элементам — не к * — чтобы не блокировать
            // свайп-жесты в слайдерах, галереях и плеерах (Grok+Qwen+DeepSeek).
            // -webkit-tap-highlight-color: transparent — убирает синюю/серую вспышку при тапе.
            + 'a,button,[role="button"],input,select,textarea,label,summary'
            + '{touch-action:manipulation;-webkit-tap-highlight-color:transparent}';
        (document.head || document.documentElement).appendChild(_mh);

        // Жёсткий запрет autoplay — на мобиле это трафик + АКБ
        document.addEventListener('play', e => {
            const el = e.target;
            if ((el.tagName === 'VIDEO' || el.tagName === 'AUDIO') && el.autoplay) {
                el.pause();
                el.autoplay = false;
            }
        }, { capture: true, passive: true });

        // Пассивные слушатели touchstart/wheel — явная подсказка браузеру
        // что scroll-обработчик не вызовет preventDefault.
        // Без passive браузер ждёт завершения JS перед каждым кадром скролла.
        document.addEventListener('touchstart', () => {}, { passive: true });
        window.addEventListener('wheel', () => {}, { passive: true });
    }

    /* ── КНОПКА УПРАВЛЕНИЯ ────────────────────────────────────────────────────
     * Три режима — карусель по одиночному клику/тапу (вариант Б):
     *   ON  → OFF → ON[E] → ON → …
     * Долгое удержание (600 мс) → показывает диагностику TTFB·DOM·↓·✕ на 4 с
     *   (не переключает режим).
     * ON[E] = Extreme Mode: агрессивная экономия трафика/ресурсов.
     * Цвета:
     *   ON      = #5a9fd4  (голубой)
     *   OFF     = #888     (серый)
     *   ON[E]   = #7a4a1e  (тёмный янтарь, отличается от всех шкал)
     ─────────────────────────────────────────────────────────────────────── */
    function _getMode() {
        const h = location.hostname;
        const isOff  = localStorage.getItem(SITE_KEY) === '1';
        const isExt  = localStorage.getItem('ihw:extreme:' + h) === '1';
        const isAuto = localStorage.getItem('ihw:auto:' + h) === '1';
        // Явно был задан ON (ранее снят с AUTO, но не переведён в EXT/OFF)
        const isExplicitON = localStorage.getItem('ihw:on:' + h) === '1';
        if (isOff)  return 'OFF';
        if (isExt)  return 'EXT';
        if (isExplicitON) return 'ON'; // пользователь явно выбрал ON
        if (isAuto) return 'AUTO';
        return 'AUTO'; // по-умолчанию — Авто-режим
    }

    function _renderBtn() {
        const mode = _getMode();
        const btn  = document.createElement('button');
        // Цвета: ON=голубой, OFF=серый, ON[E]=тёмный янтарь, ON[A]=приглушённый оливковый
        const bg   = mode === 'OFF' ? '#888' : mode === 'EXT' ? '#7a4a1e'
                   : mode === 'AUTO' ? '#3a5a3a' : '#5a9fd4';
        const fg   = mode === 'OFF' ? '#ddd' : '#fff';
        const lbl  = mode === 'OFF' ? 'OFF' : mode === 'EXT' ? 'ON[E]'
                   : mode === 'AUTO' ? 'ON[A]' : 'ON';
        btn.style.cssText = [
            'position:fixed', 'bottom:52px', 'right:12px', 'z-index:2147483647',
            'font-size:11px', 'padding:3px 7px', 'border:none', 'border-radius:4px',
            'cursor:pointer', 'opacity:0.5', 'transition:opacity .2s,background .2s',
            'font-family:system-ui,sans-serif', 'line-height:1.4',
            `background:${bg};color:${fg}`
        ].join(';');

		btn.textContent = lbl;
		btn._originalText = lbl;      // сохраняем неизменяемый базовый текст
		btn._metricActive = false;    // флаг: показываются ли сейчас метрики (long-press)
		
        // Hover (desktop)
        btn.addEventListener('mouseenter', () => {
            // Подсказки описывают СЛЕДУЮЩЕЕ состояние после клика (переход)
			if (btn._metricActive) return; // не мешаем показу метрик
            const _tips = {
                ON:   'Вкл. ускорение Extreme? ON[E]',	// ON→ON[E] (Extreme)
                OFF:  'Вкл. обычное ускорение? (ON)',  // OFF→ON (задание п.3)
                EXT:  'Вкл. режим Авто? ON[A]',        // EXT→AUTO (задание п.3)
                AUTO: 'Выкл. ускорение? OFF'            // AUTO→OFF (задание п.3)
            };
            // Не перезаписываем если сейчас показывается метрика
			btn.textContent = _tips[mode] || mode;
			btn.style.opacity = '0.95';
        });
		
		btn.addEventListener('mouseleave', () => {
			btn.style.opacity = '0.5';
			if (!btn._metricActive) {
				btn.textContent = btn._originalText; // возвращаем базовый текст
			}
		});
		
        // Long-press (600 мс) — показать диагностику без переключения
		let _pressTimer = null;
        let _longFired = false;
        const _startPress = () => {
            _longFired = false;
            _pressTimer = setTimeout(() => {
                _longFired = true;
                const nav = performance.getEntriesByType('navigation')[0];
                if (!nav) return;
                const ttfb = Math.round(nav.responseStart - nav.requestStart);
                const dom = document.getElementsByTagName('*').length;
                const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;
                const savedBg = btn.style.background;
                const _modeDisp = mode === 'AUTO'
                    ? `ON[A]=${_globalAutoMode === 'EXT' ? 'ON[E]' : 'ON'}`
                    : mode;
                btn._metricActive = true;
                btn.textContent = `TTFB:${ttfb} DOM:${dom>999?(dom/1000).toFixed(1)+'k':dom} ↓${kb||'кэш'}kb ✕${_blockedCount} mode:${_modeDisp}`;
                btn.style.cssText += ';font-size:9px;white-space:nowrap';
                setTimeout(() => {
                    if (btn) {
                        btn._metricActive = false;
                        btn.textContent = btn._originalText;
                        btn.style.background = savedBg;
                        btn.style.fontSize = '11px';
                        btn.style.whiteSpace = '';
                    }
                }, 4000);
            }, 600);
        };

        btn.addEventListener('pointerdown',  _startPress, { passive: true });
        btn.addEventListener('pointerup',    () => clearTimeout(_pressTimer), { passive: true });
        btn.addEventListener('pointerleave', () => clearTimeout(_pressTimer), { passive: true });
        btn.addEventListener('pointercancel',() => clearTimeout(_pressTimer), { passive: true });

        // Карусель режимов: ON[A] → OFF → ON → ON[E] → ON[A] → …
        // Граф-кольцо из 4 состояний. Каждый клик/тап — следующий узел кольца.
        btn.addEventListener('click', () => {
            if (_longFired) { _longFired = false; return; }
            const cur = _getMode();
            const h = location.hostname;
            if (cur === 'AUTO') {
                // ON[A] → OFF: выключить ускорение для этого сайта
                localStorage.setItem(SITE_KEY, '1');
                localStorage.removeItem('ihw:extreme:' + h);
                localStorage.removeItem('ihw:auto:' + h);
                localStorage.removeItem('ihw:on:' + h);
            } else if (cur === 'OFF') {
                // OFF → ON: явное обычное ускорение
                localStorage.removeItem(SITE_KEY);
                localStorage.removeItem('ihw:extreme:' + h);
                localStorage.removeItem('ihw:auto:' + h);
                localStorage.setItem('ihw:on:' + h, '1'); // явный ON (не AUTO)
            } else if (cur === 'ON') {
                // ON → ON[E]: включить Extreme
                localStorage.removeItem(SITE_KEY);
                localStorage.setItem('ihw:extreme:' + h, '1');
                localStorage.removeItem('ihw:auto:' + h);
                localStorage.removeItem('ihw:on:' + h);
            } else {
                // ON[E] → ON[A]: вернуться в Авто-режим (сбросить все локальные предпочтения)
                localStorage.removeItem('ihw:extreme:' + h);
                localStorage.removeItem(SITE_KEY);
                localStorage.removeItem('ihw:on:' + h);
                localStorage.setItem('ihw:auto:' + h, '1');
            }
            location.reload();
        });

        _btn = btn;
        document.documentElement.appendChild(btn);
    }


    // Кнопка управления — всегда показываем независимо от режима
    _renderBtn();

    /* ── DNS PREFETCH ДЛЯ ВТОРОГО ЭКРАНА (Mixed Content) ── */
    // Sentinel на top:2200px — когда пользователь долистывает до границы второго
    // экрана, на idle собираем внешние домены и добавляем dns-prefetch.
    // Трекеры фильтруются через isTracker() — в prefetch не попадают.
    // sendBeacon трекеров уже заблокирован выше — дополнительная фильтрация не нужна.
    let dnsPrefetchDone = false;

    const addDnsPrefetch = domains => {
        if (dnsPrefetchDone || !domains.length) return;

        // Отсеиваем трекеры — им dns-prefetch не нужен, мы их блокируем
        const clean = domains.filter(d => !isTracker('https://' + d));
        if (!clean.length) {
            log('[IHW] DNS prefetch: все найденные домены — трекеры, пропускаем');
            return;
        }

        log(`[IHW] DNS prefetch: резолвим ${clean.length} доменов → ${clean.join(', ')}`);
        const head = document.head || document.documentElement;
        clean.forEach(d => {
            const lnk = document.createElement('link');
            lnk.rel = 'dns-prefetch';
            lnk.href = '//' + d;
            head.appendChild(lnk);
        });
        dnsPrefetchDone = true;
    };

	const createSecondScreenSentinel = () => {
		if (dnsPrefetchDone) return;
		if (document.querySelector('[data-ihw-sentinel]')) return;
		// Если sentinel уже существует в DOM, не создаём второй
		if (document.querySelector('.ihw-sentinel')) return;
		const sentinel = document.createElement('div');
		sentinel.className = 'ihw-sentinel';   // уникальный класс
		sentinel.style.cssText = 'position:absolute;top:2200px;left:0;width:1px;height:1px;pointer-events:none;visibility:hidden';
		document.documentElement.appendChild(sentinel);
			log('[IHW] Sentinel создан для второго экрана');

        const obs = new IntersectionObserver(entries => {
            if (!entries[0].isIntersecting || dnsPrefetchDone) return;
            obs.disconnect();
            sentinel.remove();
            log('[IHW] Второй экран подгружен — пользователь долистал до его границы');

            // Сбор доменов на idle — не тормозим скролл
            (window.requestIdleCallback || setTimeout).bind(window)(() => {
                const externalDomains = new Set();
                document.querySelectorAll('a[href^="http"], img[src^="http"], iframe[src^="http"]')
                    .forEach(el => {
                        try {
                            // location.origin как base — страховка для относительных путей
                            const h = new URL(el.href || el.src, location.origin).hostname;
                            // endsWith точнее чем includes — не отсеет notexample.com
                            if (h && !h.endsWith(location.hostname)) externalDomains.add(h);
                        } catch {}
                    });

                const list = [...externalDomains].slice(0, 10);
                if (list.length) {
                    addDnsPrefetch(list);
                } else {
                    log('[IHW] DNS prefetch: внешних доменов не найдено');
                }
            }, { timeout: 1000 });
        }, { rootMargin: '500px 0px' });

        obs.observe(sentinel);
    };

    /* ── ФИНАЛИЗАЦИЯ ────────────────────────────────────── */
    const onLoadHandler = () => {
        if (_initDone) return; // защита от двойного запуска
        _initDone = true;

        // runRenderOpts только для Mixed Content — на видеохостингах приоритет только видео,
        // а content-visibility и srcset-оптимизации там не нужны (нет статейных блоков).
        // requestIdleCallback ждёт реального простоя браузера; setTimeout — fallback (Safari < 18).
        if (PAGE === 'Mixed Content') {
            // runRenderOpts() содержит внутреннюю проверку shouldSkipScroll
            (window.requestIdleCallback || setTimeout).bind(window)(runRenderOpts, window.requestIdleCallback ? { timeout: 500 } : 500);
            // DNS prefetch sentinel — только не на чатах (там нет "второго экрана" в классическом понимании)
            if (!shouldSkipScroll) {
                setTimeout(createSecondScreenSentinel, 1200);
            }
        }

        setTimeout(() => mo.disconnect(), 4000);

        // <noscript> — не исполняется в JS-окружении, занимает DOM-память и время парсинга.
        // Эффект минимальный (5–10 узлов DOM), безопасно удалять. (Qwen+ChatGPT)
        document.querySelectorAll('noscript').forEach(n => n.remove());

        // ── EXTREME MODE: пост-load оптимизации ──────────────────────────────
        if (EXTREME_MODE) {
            // Глобальное удаление srcset (lazy images добавились после processNode)
            document.querySelectorAll('img[srcset]').forEach(img => img.removeAttribute('srcset'));
			log('[IHW Extreme] Post-load: srcset удалён (догонка)');
            // disablePictureInPicture: некоторые сайты форсируют PiP при скролле → лишний GPU/CPU (Qwen+DeepSeek)
            document.querySelectorAll('video:not([data-ihw-boosted])').forEach(v => {
                try { v.disablePictureInPicture = true; } catch(e) {}
            });
            log('[IHW Extreme] Post-load: srcset удалён, PiP отключён у не-главных видео');
        }

        // ── DEBUG: полные метрики после load ─────────────────────────────────
        // Вызывается строго после load-события — loadEventEnd здесь всегда > 0.
        // console.group/log используется намеренно для структурированного вывода в DevTools.
        // При DEBUG=false блок не выполняется вообще — нулевая нагрузка.
		if (DEBUG) {
			_logExtendedMetrics(_getModeLabel());
		}

    };

    // Надёжный запуск — три варианта на случай разного поведения Tampermonkey
    if (document.readyState === 'complete') {
        onLoadHandler();
    } else if (document.readyState === 'interactive') {
        onLoadHandler();
    } else {
        window.addEventListener('load', onLoadHandler, { once: true });
    }

    // Fallback: 7с достаточно для любой страницы
    setTimeout(() => {
        if (PAGE === 'Mixed Content' && !dnsPrefetchDone && !shouldSkipScroll) {
            createSecondScreenSentinel();
        }
    }, 7000);

})();