I Hate Waiting

Ускоряет загрузку страниц: защита LCP, корректный lazy-loading, приоритет видео на хостингах.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         I Hate Waiting
// @name:en      I Hate Waiting
// @namespace    https://tampermonkey.net/
// @version      3.0.22
// @license      MIT
// @description  Ускоряет загрузку страниц: защита LCP, корректный lazy-loading, приоритет видео на хостингах.
// @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       Kimi + 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';

    if (window.top !== window) return;

    // v3.0.21: SPA mode bridge перенесён внутрь _getMode() — читает sessionStorage 'ihw:cur'
    // напрямую, без отдельного restore-кода в начале скрипта.

    /* ── ВРЕМЕННЫЙ ПЕРЕКЛЮЧАТЕЛЬ ───────────────────────── */
    // Раскомментировать нужную строку для тестирования в режиме инкогнито
    // или без нажатия на кнопку. Работает благодаря тому, что sessionStorage
    // читается _getMode() первым (до localStorage).
    // После теста — закомментировать обратно.
    //
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'off');  localStorage.setItem('ihw:off:' + location.hostname, '1'); ['extreme','auto','on'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname));
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'on');   ['off','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:on:' + location.hostname, '1');
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'ext');  ['off','on','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:extreme:' + location.hostname, '1');
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'auto'); ['off','on','extreme'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:auto:' + location.hostname, '1');
    // sessionStorage.removeItem('ihw:cur:' + location.hostname); ['off','on','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); // сброс в AUTO

    /* ── ОТЛАДКА ────────────────────────────────────────── */
    const DEBUG = false;
    const log = (...args) => { if (DEBUG) console.log(...args); };

    /* ── УСТРОЙСТВО ─────────────────────────────────────── */
    const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
        || (navigator.maxTouchPoints > 1 && window.innerWidth < 1024);
    const MODE = isMobile ? 'Mobile' : 'Desktop';

    /* ── НАСТРОЙКИ ──────────────────────────────────────── */
    const PAUSE_ON_HIDDEN = true;
    const BTN_BOTTOM = '70px';

    /* ── РЕЖИМ ──────────────────────────────────────────── */
    function _resolveGlobalMode() { return isMobile ? 'EXT' : 'ON'; }
    const _globalAutoMode = _resolveGlobalMode();
    const _localExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';
    const _isAutoMode = localStorage.getItem('ihw:auto:' + location.hostname) === '1'
        && !localStorage.getItem('ihw:off:' + location.hostname);
    const EXTREME_MODE = _localExt || (_isAutoMode && _globalAutoMode === 'EXT');

    let _initDone = false;
    const _t0 = performance.now();
    let _btn = null;
    let _blockedCount = 0;
    let _canplayMs = 0;
    let _videoBoosted = false;

    function _getMode() {
        // v3.0.21: sessionStorage читается первым — он переживает VK SPA-навигацию,
        // тогда как VK асинхронно очищает localStorage после перехвата location.reload().
        // При настоящем hard-reload (F5 / Ctrl+Shift+R) sessionStorage сбрасывается
        // браузером сам по себе, и мы корректно падаем обратно на localStorage.
        const h = location.hostname;
        try {
            const ss = sessionStorage.getItem('ihw:cur:' + h);
            if (ss === 'off')  return 'OFF';
            if (ss === 'ext')  return 'EXT';
            if (ss === 'on')   return 'ON';
            if (ss === 'auto') return 'AUTO';
        } catch (e) {}
        if (localStorage.getItem(SITE_KEY) === '1') return 'OFF';
        if (localStorage.getItem('ihw:extreme:' + h) === '1') return 'EXT';
        if (localStorage.getItem('ihw:on:' + h) === '1') return 'ON';
        return 'AUTO';
    }
    function _getModeLabel() {
        // v3.0.22: используем _getMode() — он читает sessionStorage первым,
        // что важно на VK/OK где localStorage может быть очищен SPA-навигацией.
        const mode = _getMode();
        if (mode === 'OFF')  return '[OFF]';
        if (mode === 'EXT')  return '[ON[E]]';
        if (mode === 'AUTO') return `[ON[A]=${EXTREME_MODE ? 'ON[E]' : 'ON'}]`;
        return '[ON]';
    }

    /* ── КНОПКА (hoisted, используется в OFF-guard) ─────── */
    function _renderBtn() {
        const mode = _getMode();
        const btn = document.createElement('button');
        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:' + BTN_BOTTOM, '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;

        btn.addEventListener('mouseenter', () => {
            if (btn._metricActive) return;
            const tips = { ON: 'Вкл. ускорение Extreme? ON[E]', OFF: 'Вкл. обычное ускорение? (ON)', EXT: 'Вкл. режим Авто? ON[A]', AUTO: 'Выкл. ускорение? OFF' };
            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;
        });

        let _pressTimer = null, _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 disp = 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:${disp}`;
                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 });

        btn.addEventListener('click', () => {
            if (_longFired) { _longFired = false; return; }
            const cur = _getMode(), h = location.hostname;

            // Карусель режимов: AUTO → OFF → ON → EXT → AUTO
            const nextMode = cur === 'AUTO' ? 'OFF' : cur === 'OFF' ? 'ON' : cur === 'ON' ? 'EXT' : 'AUTO';

            // v3.0.21: sessionStorage 'ihw:cur' — основной мост через VK SPA-навигацию.
            // _getMode() читает его первым. При настоящем hard-reload sessionStorage
            // сбрасывается браузером, мы корректно падаем на localStorage.
            const ssKey = { 'OFF': 'off', 'ON': 'on', 'EXT': 'ext', 'AUTO': 'auto' }[nextMode];
            try { sessionStorage.setItem('ihw:cur:' + h, ssKey); } catch (e) {}

            // localStorage — для hard-reload и сайтов без SPA-перехвата reload()
            ['off', 'extreme', 'auto', 'on'].forEach(k => localStorage.removeItem('ihw:' + k + ':' + h));
            if (nextMode === 'OFF')  localStorage.setItem(SITE_KEY, '1');
            else if (nextMode === 'EXT')  localStorage.setItem('ihw:extreme:' + h, '1');
            else if (nextMode === 'ON')   localStorage.setItem('ihw:on:' + h, '1');
            else if (nextMode === 'AUTO') localStorage.setItem('ihw:auto:' + h, '1');

            // Немедленно перерисовываем кнопку — на VK SPA reload() перехватывается
            // и скрипт не реинициализируется, поэтому это единственный визуальный отклик.
            if (_btn) { _btn.remove(); _btn = null; }
            _renderBtn();

            location.reload();
        });

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

    /* ── OFF GUARD: полное молчание ─────────────────────── */
    const SITE_KEY = 'ihw:off:' + location.hostname;
    if (localStorage.getItem(SITE_KEY) === '1') {
        _renderBtn();
        return;
    }

    /* ── LCP METRIC OBSERVER (только DEBUG, только не-OFF) ── */
    // v3.0.19: перемещён после OFF guard — в OFF консоль должна быть чистой.
    // Показывает: элемент, размер, время, fetchPriority и кто его назначил.
    if (DEBUG && window.PerformanceObserver) {
        try {
            new PerformanceObserver(list => {
                const e = list.getEntries().pop();
                if (!e) return;
                const el = e.element;
                const tag = el
                    ? el.tagName + (el.id ? '#' + el.id : '') + (el.className ? '.' + String(el.className).trim().split(/\s+/)[0] : '')
                    : '(no element)';
                const fp = el?.fetchPriority || el?.getAttribute?.('fetchpriority') || 'unset';
                const who = el?.hasAttribute?.('data-ihw-boosted') ? 'IHW' : 'browser';
                console.log(`[IHW DEBUG] LCP: ${tag} | ${(e.size / 1024).toFixed(1)}KB | ${Math.round(e.startTime)}ms | fetchPriority:${fp} | assigned-by:${who}`);
            }).observe({ type: 'largest-contentful-paint', buffered: true });
        } catch (e) { }
    }

    /* ── МЕТРИКИ (только DEBUG, только не-OFF) ───────────── */
    function _logExtendedMetrics(modeLabel) {
        const nav = performance.getEntriesByType('navigation')[0];
        if (!nav || nav.loadEventEnd <= 0) {
            setTimeout(() => _logExtendedMetrics(modeLabel), 100);
            return;
        }
        const ttfb = Math.round(nav.responseStart - nav.requestStart);
        const dDCL = Math.round(nav.loadEventEnd - nav.domContentLoadedEventEnd);
        const loadMs = Math.round(nav.loadEventEnd - _t0);
        const dom = document.getElementsByTagName('*').length;
        const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;

        let line = `[IHW] ${modeLabel} ${location.hostname} | ` +
                   `TTFB:${ttfb}ms load:${loadMs}ms ΔDCL:${dDCL}ms ` +
                   `DOM:${dom} KB:${kb || 'cache'} ✕${_blockedCount}`;
        if (_canplayMs) line += ` canplay:${_canplayMs}ms`;
        console.log(line);
    }

    /* ── БРАУЗЕР ────────────────────────────────────────── */
    const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
    const isChromium = !isFirefox && !!window.chrome;
    log(`[IHW] Browser: ${isFirefox ? 'Firefox' : isChromium ? 'Chromium' : 'Other'}`);

    /* ── ТИП СТРАНИЦЫ ───────────────────────────────────── */
    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'
    ];
    const VIDEO_HOST_EXCEPTIONS = ['alice.yandex.ru'];
    const VIDEO_PATH_SEGMENTS = new Set(['video', 'videos', 'live', 'clip', 'stream', 'watch', 'player']);
    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'],
    };

    const _preconnected = new Set();

    function _doPreconnect(host) {
        if (!host || _preconnected.has(host) || host === location.hostname) return;
        if (isTracker('https://' + host)) return;
        _preconnected.add(host);
        const l = document.createElement('link');
        l.rel = 'preconnect'; l.href = 'https://' + host; l.crossOrigin = 'anonymous';
        document.head.appendChild(l);
        log('[IHW Video] Preconnect →', host);
    }

    // v3.0.19: _warmupCDN удалён.
    // QUIC beacon → uBlock блокирует на уровне webRequest (до JS).
    // HEAD warmup → googlevideo.com/videoplayback?expire=… подписан только под GET,
    //   всегда отвечает 405 Method Not Allowed или 403 Forbidden.
    // Оба метода не работают в реальных условиях и дают шум в консоли.
    // _doPreconnect уже покрывает DNS prefetch + TLS handshake warmup — достаточно.

    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;
        return /googlevideo|cdnvideohub|akamaized|ttvnw|bilivideo|odycdn|vimeocdn|dmcdn|userapi|vkuser/i.test(host);
    }

    function initDynamicPreconnect(video) {
        if (isMobile || !video) return;
        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 obs = new PerformanceObserver((list) => {
                    for (const entry of list.getEntries()) {
                        const n = entry.name;
                        if (!/\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(n) &&
                            !/\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4|videoplayback|initplayback/i.test(n) &&
                            !/googlevideo|cdnvideohub|vimeocdn|dmcdn/i.test(n)) continue;
                        try {
                            const h = new URL(n).hostname;
                            if (h === 'i.vimeocdn.com') continue;
                            if (_isVideoCDN(h) || !isTracker('https://' + h)) _doPreconnect(h);
                        } catch (e) { }
                    }
                });
                obs.observe({ type: 'resource', buffered: true });
                setTimeout(() => { try { obs.disconnect(); } catch (e) { } }, 20000);
            } catch (e) { log('[IHW] PO error:', e); }
        }
        if (video.readyState >= 1) tryDirect();
        else video.addEventListener('loadedmetadata', tryDirect, { once: true });
    }

    function initVideoPreconnect() {
        if (isMobile) return;
        const h = location.hostname.replace(/^www\./, '');
        (VIDEO_CDN_MAP[h] || []).forEach(c => _doPreconnect(c));
        if (!window.PerformanceObserver) return;
        try {
            const po = new PerformanceObserver((list, obs) => {
                for (const entry of list.getEntries()) {
                    const n = entry.name;
                    if (!/\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(n) &&
                        !/\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4|videoplayback|initplayback/i.test(n)) continue;
                    try {
                        const sh = new URL(n).hostname;
                        if (sh === 'i.vimeocdn.com') continue;
                        if (_isVideoCDN(sh) || (!isTracker('https://' + sh) && sh !== location.hostname)) {
                            _doPreconnect(sh); obs.disconnect(); break;
                        }
                    } catch (e) { }
                }
            });
            po.observe({ type: 'resource', buffered: true });
            setTimeout(() => { try { po.disconnect(); } catch (e) { } }, 15000);
        } catch (e) { log('[IHW Video] PO unavail:', e); }
    }

    const isVideoHost = (() => {
        const host = location.hostname;
        if (VIDEO_HOST_EXCEPTIONS.includes(host)) return false;
        if (VIDEO_HOSTS.some(h => host.endsWith(h))) return true;
        return location.pathname.split('/').some(s => VIDEO_PATH_SEGMENTS.has(s.toLowerCase().split('?')[0]));
    })();
    const PAGE = isVideoHost ? 'Video Content' : 'Mixed Content';
    // v3.0.22: полный лог режима только при DEBUG=true.
    // Показывает Platform (Desktop/Mobile), полный Mode-лейбл и тип страницы.
    log(`[IHW] Platform:${MODE} | Mode:${_getModeLabel()} | Page:${PAGE}`);

    /* ── ЗАЩИТА AI-ЧАТОВ (v3.0.16) ─────────────────────── */
    // Гибридный подход: ручной массив известных чатов (0 мс) + лёгкий фоллбэк
    // для неизвестных доменов. Цель: не применять агрессивные оптимизации
    // (lazy-iframe, content-visibility, scroll-behavior:auto) на страницах,
    // где поле ввода может "улететь" или застрять.
    const CHAT_SKIP_HOSTS = new Set([
        'chatgpt.com', 'claude.ai', 'chat.deepseek.com', 'qwen.ai',
        'grok.com', 'gemini.google.com', 'perplexity.ai', 'alice.yandex.ru'
    ]);

    // Лёгкий фоллбэк-детект (<0.5 мс, синхронный, без тяжёлого скана DOM).
    // Проверяет hostname, pathname, title и 1–2 быстрых ARIA-селектора.
    function _isLikelyChat() {
        const host = location.hostname;
        if (CHAT_SKIP_HOSTS.has(host)) return true;

        // Дешёвые строковые проверки (O(1), не вызывают reflow)
        if (/chat\.|messanger|conversation/i.test(host)) return true;
        const path = location.pathname.toLowerCase();
        if (/(^|\/)(chat|messages|conversation|thread|dialog)(\/|$)/.test(path)) return true;
        if (/чат|chat|messages|conversation/i.test(document.title)) return true;

        // Быстрые специфичные селекторы только если DOM уже доступен
        if (document.readyState !== 'loading') {
            if (document.querySelector('[role="log"], [aria-live="polite"], .chat-input, [data-testid*="chat"]'))
                return true;
        }
        return false;
    }

    // Флаг shouldSkipScroll + кэш в sessionStorage (1 проверка на сессию вкладки).
    // Это защищает от повторных проверок при SPA-навигации и экономит батарею.
    const _chatKey = 'ihw:skip_chat:' + location.hostname;
    let shouldSkipScroll = sessionStorage.getItem(_chatKey);
    if (shouldSkipScroll === null) {
        shouldSkipScroll = _isLikelyChat();
        sessionStorage.setItem(_chatKey, shouldSkipScroll ? '1' : '0');
    } else {
        shouldSkipScroll = shouldSkipScroll === '1';
    }

    if (shouldSkipScroll) log('[IHW] Chat detected, aggressive opts skipped');

    /* ── ТРЕКЕРЫ ────────────────────────────────────────── */
    const TRACKERS = [
        'google-analytics.com', 'googletagmanager.com', 'doubleclick.net',
        'googlesyndication.com', 'googleadservices.com', 'googletagservices.com', 'google.com/ads',
        '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',
        'deltarockme.com', 'vak345.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',
        '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',
        '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',
    ];
    const TRACKER_EXCEPTIONS = ['cloudflare.com', 'challenges.cloudflare.com'];

    const isTracker = url => { try { return TRACKERS.some(t => new URL(url, location.origin).hostname.endsWith(t)); } catch { return false; } };
    const isException = url => { try { return TRACKER_EXCEPTIONS.some(e => new URL(url, location.origin).hostname.endsWith(e)); } catch { return false; } };

    const _origBeacon = navigator.sendBeacon.bind(navigator);
    navigator.sendBeacon = (url, data) => isTracker(url) ? false : _origBeacon(url, data);

    /* ── font-display:swap (всегда, кроме OFF) ──────────── */
    const _fs = document.createElement('style');
    _fs.textContent = '@font-face{font-display:swap}';
    (document.head || document.documentElement).appendChild(_fs);

    /* ── EXTREME MODE ───────────────────────────────────── */
    if (EXTREME_MODE) {
        try {
            Object.defineProperty(navigator, 'connection', {
                value: { effectiveType: 'slow-2g', saveData: true, rtt: 2200, downlink: 0.05 },
                configurable: true
            });
            log('[IHW Extreme] Fake Save-Data + slow-2g');
        } catch (e) { }
        document.documentElement?.classList.add('ihw-extreme');
        const _xCss = document.createElement('style');
        _xCss.textContent =
            '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);
        document.querySelectorAll('video,audio').forEach(m => {
            if (!m.hasAttribute('data-ihw-boosted')) { m.preload = 'none'; m.autoplay = false; }
        });
        document.querySelectorAll('[autofocus]').forEach(el => el.removeAttribute('autofocus'));
        document.querySelectorAll('input,textarea,[contenteditable]').forEach(el => el.spellcheck = false);
    }

    /* ── Базовый CSS ────────────────────────────────────── */
    const _css = document.createElement('style');
    _css.textContent = 'html,body{visibility:visible!important;opacity:1!important}';
    (document.head || document.documentElement).appendChild(_css);

    /* ── VK / OK / vkvideo.ru: хост-специфичные оптимизации (v3.0.18) ──── */
    // Аналогично YouTube-блоку: только стабильные structural anchors и
    // product-level классы. Без webpack-патчинга, MutationObserver, DOM removal.
    // display:none для скелетонов — верный выбор (Kimi прав):
    //   скелетоны — чисто визуальные заглушки, не несут функционала.
    //   display:none полностью исключает из render tree → лучше для FCP чем
    //   visibility:hidden, который всё ещё резервирует место и требует paint.
    // #rightColumn для OK: product-level ID, стабилен годами. Если OK когда-
    //   либо переименует его в navigation+ads — просто перестанет работать,
    //   но сайт останется цел (только display:none, не DOM removal).
    // [class*="ad-overlay"] — НЕ берём: слишком широко, риск задеть subtitles/controls.
    if (/\b(vk\.com|vkvideo\.ru|ok\.ru)\b/.test(location.hostname)) {
        const _vkCss = document.createElement('style');
        _vkCss.textContent =
            // Скелетоны: убираем из render tree до гидратации React/Vue.
            // Именованные VK-классы стабильны (product-level, не CSS-modules).
            '#FeedPageSkeleton,.LeftMenuLegacySkeleton,.TopSearchRoot .SkeletonIso,' +
            '.ProfileMenuSkeletonRoot,.skeleton,.skeleton-loader,.placeholder-animation,' +
            '[class*="skeleton"],[class*="Skeleton"],.animated-background' +
            '{display:none!important}' +
            // Рекламные structural anchors: VK
            '#ads_left,.videoplayer_ads,.videoplayer_ads_actions,' +
            '.videoplayer_ads_media_el,.rb-adman-ad-actions' +
            '{display:none!important}' +
            // Рекламные structural anchors: OK
            '#rightColumn,#hook_Block_StickyBannerContainer' +
            '{display:none!important}' +
            // VK Video: blur restriction overlay
            // data-testid — stableнее чем hashed class, но следим:
            // если VK сменит testid — просто перестанет работать.
            '[data-testid="video_card_restriction_overlay"]{display:none!important}' +
            // Blur на превью (только img, не глобально — чтобы не задеть UI)
            'img[class*="Blur"],img[class*="blur"]{filter:none!important;-webkit-filter:none!important}' +
            // Изоляция плеера: уменьшаем объём перерисовок при обновлении UI вокруг
            '.videoplayer,.videoplayer_media,[class*="videoplayer"]:not([class*="ads"])' +
            '{contain:layout style!important}' +
            // Фиксированные элементы в отдельный compositor layer
            '#masthead,.vkuiFixedLayout,.TopNav,.TopSearchRoot,.LeftMenu' +
            '{will-change:transform!important}';
        (document.head || document.documentElement).appendChild(_vkCss);
        log('[IHW] VK/OK: applied host-specific optimizations');
    }

    /* ── Яндекс SERP: скрытие рекламных меток (v3.0.18) ─────────────── */
    // Только стабильные ARIA-атрибуты и named-классы.
    // НЕ используем :has(.serp-item) для скрытия целых карточек —
    //   риск ложных срабатываний + стоимость CSS engine на :has() высокого уровня.
    // НЕ используем MutationObserver/TreeWalker — антипаттерн для perf-скрипта.
    // Цель: убрать рекламные метки и баннеры до first paint → улучшение LCP
    //   (реклама часто занимает above-the-fold на поиске Яндекса).
    // Сетевые запросы Директа CSS не отменяет — только визуальный выигрыш.
    if (location.hostname.includes('yandex.') && PAGE !== 'Video Content') {
        const _yaCss = document.createElement('style');
        _yaCss.textContent =
            // ARIA-метки: семантически стабильны (часть a11y-контракта)
            '[aria-label="Реклама"],[aria-label="Промо"]' +
            '{display:none!important}' +
            // Named-классы Яндекса: product-level, не CSS-modules
            '.PromoOffer,.AdvLabel,.AdvCaption,.direct-label,' +
            '.mg-adv-label,[data-baobab-name="adv"],.DistributionLinkBro' +
            '{display:none!important}' +
            // Рекламные карточки недвижимости/финансов (структурные, не generated)
            '.RealtyListing-AdvItem,.OfferSnippet_highlight' +
            '{display:none!important}';
        (document.head || document.documentElement).appendChild(_yaCss);
        log('[IHW] Yandex: ad labels hidden');
    }

    /* ── Универсальная защита скроллеров ────────────────── */
    function isInsideScroller(el) {
        let p = el.parentElement;
        while (p && p !== document.body) {
            const s = window.getComputedStyle(p);
            if (/auto|scroll/.test(s.overflowY) || /auto|scroll/.test(s.overflow)) return true;
            p = p.parentElement;
        }
        return false;
    }

    /* ── v3.0.15: упрощённый поиск видео (без VK/OK хаков) ── */
    function _findVideosDeep() {
        const found = new Set([...document.querySelectorAll('video')]);
        // Для плееров с известными обёртками — ищем внутри, но без VK/OK специфики
        const hosts = ['.videoplayer_media', '#video-poplayer-cnt', '.video-page-layout-module__player'];
        hosts.forEach(sel => {
            document.querySelectorAll(sel).forEach(host => {
                host.querySelectorAll('video').forEach(v => found.add(v));
            });
        });
        return [...found];
    }

    /* ── MutationObserver ───────────────────────────────── */
    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 || '';

        if (src && isTracker(src) && !isException(src)) { node.remove(); _blockedCount++; return; }

        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;
        }
        // Шрифты откладываем ТОЛЬКО на Video Content
        if (tag === 'LINK' && /fonts\.(googleapis|gstatic|bunny\.net)|use\.typekit\.net|fast\.fonts\.net/.test(src)) {
            if (PAGE !== 'Video Content') return;
            if (node.dataset.ihwFontDeferred) return;
            node.media = 'print';
            setTimeout(() => {
                if (node.parentNode && !node.dataset.ihwFontDeferred) node.media = 'all';
            }, 6000);
            return;
        }

        // v3.0.4: loading=lazy только для поздней динамики (>1000мс).
        // Preload scanner уже обработал начальный HTML.
        // Защита LCP на Pinterest/Unsplash и других фотобанках.
        if (tag === 'IMG') {
            if (!node.decoding) node.decoding = 'async';
            if (performance.now() > 1000 && !node.hasAttribute('loading')) {
                node.loading = 'lazy';
            }
        }

        if (node.hasAttribute('autofocus')) node.removeAttribute('autofocus');

        // v3.0.16: на AI-чатах не трогаем iframe (виджеты ввода, превью)
        if (shouldSkipScroll && tag === 'IFRAME') return;
        if (tag === 'IFRAME' && !node.fetchPriority && !isVideoHost) {
            if (node.offsetWidth < 200 || node.offsetHeight < 100) node.fetchPriority = 'low';
        }


        /* v3.0.12: VIDEO появился в DOM → мягкий boost через 150ms для layout.
           Не зависит от счётчика попыток — работает даже после исчерпания _maxAttempts.
           На Mobile: одиночный setTimeout 150ms практически бесплатен. */
        if (tag === 'VIDEO' && PAGE === 'Video Content' && !_videoBoosted) {
            log('[IHW] New <video> detected by MO, trying boost...');
            setTimeout(() => { if (!_videoBoosted && boostMainVideo() === true) _videoBoosted = true; }, 150);
        }

        if (EXTREME_MODE) {
            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 { }
                }
            }
            if (['VIDEO', 'AUDIO', 'SCRIPT'].includes(tag) && !node.fetchPriority) {
                if (!(tag === 'VIDEO' && node.hasAttribute('data-ihw-boosted'))) node.fetchPriority = 'low';
            }
            if (tag === 'VIDEO' || tag === 'AUDIO') {
                if (!node.hasAttribute('data-ihw-boosted')) { node.preload = 'none'; node.autoplay = false; }
            }
            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 });

    /* ── runRenderOpts (v3.0.2) ─────────────────────────── */
	// Запускается после load. Браузер уже приоритизировал видимые изображения.
    // Оставшиеся без loading — безопасно перевести в lazy.
    const runRenderOpts = () => {
        // v3.0.16: на AI-чатах не рискуем — пропускаем batch-оптимизацию img,
        // чтобы не сломать динамическую подгрузку аватарок/превью сообщений.
        if (shouldSkipScroll) return;
        if (document.readyState !== 'complete') {
            window.addEventListener('load', runRenderOpts, { once: true });
            return;
        }
        requestIdleCallback((deadline) => {
            const images = document.querySelectorAll('img:not([loading])');
            let idx = 0;
            function processBatch() {
                while (idx < images.length && deadline.timeRemaining() > 0) {
                    images[idx++].loading = 'lazy';
                }
                if (idx < images.length) {
                    requestIdleCallback(processBatch, { timeout: 2000 });
                } else {
                    log('[IHW] runRenderOpts: lazy applied to', images.length, 'images');
                }
            }
            processBatch();
        }, { timeout: 2000 });
    };

    /* ── VIDEO CONTENT ──────────────────────────────────── */
    if (PAGE === 'Video Content') {
		// Задержка до DCL+idle: YouTube устанавливает свои соединения первым
        // Без задержки preconnect перехватывает HTTP/2 слоты → ON хуже OFF на 8%
        const _schedulePreconnect = () =>
            (window.requestIdleCallback || setTimeout).bind(window)(
                () => initVideoPreconnect(),
                window.requestIdleCallback ? { timeout: 1000 } : 500);
        if (document.readyState === 'loading')
            document.addEventListener('DOMContentLoaded', _schedulePreconnect, { once: true });
        else _schedulePreconnect();

        /* ── v3.0.15: шрифты — canplay + таймаут fallback ── */
        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;
            let restored = false;
            const _restore = () => {
                if (restored) return;
                restored = true;
                clearTimeout(_timer);
                mainVideo.removeEventListener('error', _restore);
                fontLinks.forEach(lnk => {
                    if (!lnk.dataset.ihwFontDeferred) return;
                    delete lnk.dataset.ihwFontDeferred;
                    delete lnk.dataset.ihwOrigMedia;
                    lnk.media = lnk.dataset.ihwOrigMedia || 'all';
                    if (lnk.rel === 'preload' && lnk.as === 'font') lnk.fetchPriority = 'low';
                });
                log('[IHW Video] Шрифты восстановлены');
            };
            const _timer = setTimeout(_restore, 8000); // fallback 8s
            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';
            });
        }

        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;
            const _ytBootstrap = () => {
                try { if (window.yt?.config_) window.yt.config_.ENABLE_LOGGING = false; } catch (e) { }
                let c = 'ytd-masthead,#masthead-container{will-change:transform}' +
                    'ytd-rich-shelf-renderer[is-shorts],ytd-reel-shelf-renderer,#shorts-container{display:none!important}' +
                    // v3.0.18: расширенный набор рекламных селекторов (из YouTube Ad-Bypass 2025-2026)
                    // .ad-showing>video / .ad-interrupting>video — НЕ скрываем:
                    //   YouTube переиспользует один <video> для рекламы и контента;
                    //   скрытие сломает boostMainVideo и canplay основного видео.
                    'ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-promoted-video-renderer,' +
                    '#player-ads,ytd-in-feed-ad-layout-renderer,' +
                    'ytd-rich-item-renderer:has(ytd-ad-slot-renderer),' +
                    '#masthead-ad,.ytp-ad-player-overlay,.ytp-ad-message-container,' +
                    '.yt-mealbar-promo-renderer,' +
                    'tp-yt-paper-dialog:has(#feedback.ytd-enforcement-message-view-model)' +
                    '{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) c += 'html{scrollbar-width:thin}';
                // v3.0.16-fix7: скрываем hover-превью на шкале — только EXTREME.
                // В обычном режиме пользователи ориентируются по превью при перемотке.
                if (EXTREME_MODE) c += '.ytp-inline-preview{display:none!important}';
                const s = document.createElement('style'); s.textContent = c; document.head.appendChild(s);
                try {
                    const f = window.yt?.config_?.EXPERIMENT_FLAGS;
                    if (f && typeof f === 'object') Object.assign(f, {
                        web_animated_actions: false, web_animated_like: false,
                        web_animated_like_lazy_load: 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,
                        kevlar_refresh_on_theme_change: false,
                    });
                } catch (e) { }
                const cm = document.querySelector('ytd-comments#comments');
                if (cm) { cm.style.contentVisibility = 'hidden'; new IntersectionObserver(e => { if (e[0].isIntersecting) cm.style.contentVisibility = ''; }, { rootMargin: '200px' }).observe(cm); }
            };
            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _ytBootstrap, { once: true });
            else _ytBootstrap();
        }

        const boostMainVideo = () => {
            const videos = _findVideosDeep();

            const visible = videos.filter(v => {
                const rect = v.getBoundingClientRect();
                return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight * 1.5;
            });

            if (visible.length) {
                const main = visible.reduce((a, b) => {
                    const sa = a.getBoundingClientRect(), sb = b.getBoundingClientRect();
                    return (sb.width * sb.height || 1) > (sa.width * sa.height || 1) ? b : a;
                });
                log(`[IHW] Found main <video>: ${main.tagName}`);

                // inline boost (v3.0.15: убран applyVideoBoost, VK/OK не поддерживаются)
                main.setAttribute('data-ihw-boosted', 'true');
                main.preload = 'auto';
                main.fetchPriority = 'high';
                initDynamicPreconnect(main);
                _deferFontsUntilCanplay(main);

                if (main.poster) {
                    try {
                        const l = document.createElement('link');
                        l.rel = 'preload'; l.as = 'image'; l.href = main.poster; l.fetchPriority = 'high';
                        document.head.appendChild(l);
                    } catch (e) {}
                }
                main.querySelectorAll('source').forEach(s => { if (!s.fetchPriority) s.fetchPriority = 'high'; });
                const vs = main.src || main.currentSrc || '';
                if (vs.includes('.m3u8')) {
                    try {
                        const l = document.createElement('link');
                        l.rel = 'preload'; l.as = 'fetch'; l.fetchPriority = 'high'; l.href = vs; l.crossOrigin = 'anonymous';
                        document.head.appendChild(l);
                    } catch (e) {}
                }
                main.setAttribute('playsinline', '');
                // rVFC: first-frame metric (только DEBUG, НЕ prewarm — только измерение)
                if (DEBUG && main.requestVideoFrameCallback) {
                    main.requestVideoFrameCallback(() => {
                        log(`[IHW DEBUG] First frame rendered: ${Math.round(performance.now() - _t0)}ms`);
                    });
                }

                if (main.readyState >= 2) {
                    main.play().catch(e => log('[IHW] play() blocked:', e.message));
                    _canplayMs = Math.round(performance.now() - _t0);
                    log(`[IHW] Video ready immediately: ${_canplayMs}ms`);
                } else {
                    main.addEventListener('canplay', () => {
                        main.play().catch(e => log('[IHW] play() blocked:', e.message));
                        _canplayMs = Math.round(performance.now() - _t0);
                        log(`[IHW] canplay after: ${_canplayMs}ms`);
                    }, { once: true });
                }
                return true;
            }

            const iframes = [...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 (iframes.length) {
                const main = iframes.reduce((a, b) => (b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a);
                log(`[IHW] Main video <iframe>: ${main.offsetWidth}x${main.offsetHeight}`);
                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;
            }

            // v3.0.15: VK/OK — ускоряем контейнер, не ждём <video>
            const isVKOK = /vk\.com|vkvideo\.ru|ok\.ru/.test(location.hostname);
            const custom = document.querySelector('[class*="player"],[id*="player"],[class*="Player"],[id*="Player"],[data-video],[data-player],[data-src*="video"]');
            if (custom && custom.offsetWidth > 200) {
                log(`[IHW] Custom player wrapper: <${custom.tagName}>#${custom.id || '(no-id)'} .${(custom.className || '').split(' ')[0]}`);
                custom.setAttribute('data-ihw-boosted', 'true');
                if (isVKOK) {
                    // VK/OK: ускоряем рендер контейнера, preconnect уже сделан
                    custom.style.contentVisibility = 'visible';
                    log('[IHW] VK/OK container boost (no <video> in DOM)');
                }
                return isVKOK ? 'custom-vk' : 'custom';
            }

            log('[IHW] No main video or player wrapper found');
            return false;
        };

        let _boostAttempt = 0;
        const _maxAttempts = PAGE === 'Video Content' ? 6 : 4;
        const _maxCustomAttempts = PAGE === 'Video Content' ? 5 : 3;

        const tryBoost = () => {
            if (_videoBoosted) return;
            log(`[IHW] Boost attempt ${_boostAttempt + 1}/${_maxAttempts}...`);

            const result = boostMainVideo();
            if (result === true) {
                _videoBoosted = true;
                log('[IHW] Main video boosted successfully');
                return;
            }

            if (result === 'custom' || result === 'custom-vk') {
                log('[IHW] Custom player wrapper detected, waiting for native <video>...');
                if (_boostAttempt >= _maxCustomAttempts) {
                    log('[IHW] Custom player confirmed (no native <video> found after all attempts)');
                    return;
                }
            } else {
                log('[IHW] No video element found yet');
            }

            if (_boostAttempt >= _maxAttempts) {
                log('[IHW] Boost attempts exhausted (no video found)');
                return;
            }

            const d = Math.min(1500 * Math.pow(2, _boostAttempt), 8000);
            _boostAttempt++;
            log(`[IHW] Retrying boost in ${d}ms`);
            setTimeout(tryBoost, d);
        };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryBoost, { once: true });
        else tryBoost();

        if (PAUSE_ON_HIDDEN) {
            document.addEventListener('visibilitychange', () => {
                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);
                if (document.pictureInPictureElement) return;
                if (document.hidden) {
                    main._ihw_wasPlaying = !main.paused;
                    if (main._ihw_wasPlaying) main.pause();
                } else {
                    if (main._ihw_wasPlaying) { main.fetchPriority = 'high'; main.play().catch(() => { }); }
                    main._ihw_wasPlaying = undefined;
                }
            });
        }
    }

    /* ── MIXED CONTENT ──────────────────────────────────── */
    if (PAGE === 'Mixed Content') {
        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' });

        // v3.0.20: безопасная установка preload/autoplay для video.
        // НЕ трогаем video с пустым/about:blank src и без <source> детей:
        //   браузер попытается загрузить placeholder как медиа → ERR_UNKNOWN_URL_SCHEME
        //   → video переходит в ERROR state → сайт реактивно блокирует overlay-кнопку play.
        // Также пропускаем video помеченные как ihw-boosted (уже под нашим контролем).
        const _safeSetVideoMeta = (v) => {
            if (v.hasAttribute('data-ihw-boosted')) return;
            const src = v.currentSrc || v.getAttribute('src') || '';
            const hasSources = v.querySelector('source[src]') !== null;
            if (!src && !hasSources) return;                       // нет src вообще
            if (/^about:|^javascript:/i.test(src)) return;        // placeholder src
            v.autoplay = false;
            v.preload = 'metadata';
        };

        const initMixed = () => {
            // v3.0.16: на AI-чатах применяем только безопасное (async decoding,
            // autoplay off), но НЕ трогаем lazy-iframe и скроллеры.
            if (shouldSkipScroll) {
                document.querySelectorAll('img').forEach(img => { if (!img.decoding) img.decoding = 'async'; });
                document.querySelectorAll('video').forEach(v => { _safeSetVideoMeta(v); });
                return;
            }
            const vh = window.innerHeight;
            document.querySelectorAll('img').forEach(img => {
                if (!img.decoding) img.decoding = 'async';
            });
            [...document.querySelectorAll('iframe')].forEach(fr => {
                const top = fr.getBoundingClientRect().top;
                if (top > vh * 2 && fr.src) {
                    if (isInsideScroller(fr)) {
                        log('[IHW] lazy-iframe: пропущен (внутри скроллера)');
                        return;
                    }
                    const rect = fr.getBoundingClientRect();
                    const pb = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
                    if (fr.offsetHeight < 120 && rect.top > pb - vh * 1.5) return;
                    fr.dataset.lazySrc = fr.src; fr.removeAttribute('src'); lazyIframeObserver.observe(fr);
                }
            });
            document.querySelectorAll('video').forEach(v => { _safeSetVideoMeta(v); });
        };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initMixed, { once: true });
        else initMixed();
    }

    /* ── MOBILE ─────────────────────────────────────────── */
    if (isMobile) {
        const _mh = document.createElement('style');
        _mh.textContent = '@media(hover:none){*{transition:none!important;animation:none!important}}' +
            'a,button,[role="button"],input,select,textarea,label,summary' +
            '{touch-action:manipulation;-webkit-tap-highlight-color:transparent}';
        (document.head || document.documentElement).appendChild(_mh);
        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 });
        document.addEventListener('touchstart', () => { }, { passive: true });
        window.addEventListener('wheel', () => { }, { passive: true });
    }

    /* ── КНОПКА (не-OFF) ────────────────────────────────── */
    _renderBtn();

    /* ── DNS PREFETCH (v3.0.16-fix4: обёртка с гарантированным fallback) ── */
    let dnsPrefetchDone = false;
    
    // Надёжная обёртка: requestIdleCallback → setTimeout fallback
    const _runWhenIdle = (fn, timeout = 2000) => {
        if (window.requestIdleCallback) {
            const id = requestIdleCallback(fn, { timeout });
            // Двойная страховка: если idle не вызовется за timeout, force через setTimeout
            setTimeout(() => cancelIdleCallback(id), timeout + 100);
        } else {
            setTimeout(fn, timeout);
        }
    };

    const addDnsPrefetch = domains => {
        if (dnsPrefetchDone || !domains.length) return;
        const head = document.head || document.documentElement;
        domains.forEach(d => { 
            const l = document.createElement('link'); 
            l.rel = 'dns-prefetch'; 
            l.href = '//' + d; 
            head.appendChild(l); 
        });
        dnsPrefetchDone = true;
        log('[IHW] DNS prefetch added:', domains);
    };

    const createSecondScreenSentinel = () => {
        if (dnsPrefetchDone) { log('[IHW] Sentinel: already done'); return; }
        if (document.querySelector('.ihw-sentinel')) { log('[IHW] Sentinel: already exists'); return; }
        
        log('[IHW] Sentinel: creating...');
        const s = document.createElement('div');
        s.className = 'ihw-sentinel';
        s.style.cssText = 'position:absolute;top:2200px;left:0;width:1px;height:1px;pointer-events:none;visibility:hidden';
        document.documentElement.appendChild(s);

        let obs = null;
        let fallbackTimer = null;
        
        const cleanup = () => {
            if (obs) { try { obs.disconnect(); } catch(e) {} obs = null; }
            if (fallbackTimer) { clearTimeout(fallbackTimer); fallbackTimer = null; }
            if (s.parentNode) s.remove();
        };

        const doScan = (source) => {
            if (dnsPrefetchDone) return;
            cleanup();
            
            const ext = new Set();
            document.querySelectorAll('a[href^="http"], img[src^="http"], iframe[src^="http"]').forEach(el => {
                try { 
                    const h = new URL(el.href || el.src, location.origin).hostname; 
                    if (h && !h.endsWith(location.hostname)) ext.add(h); 
                } catch {}
            });
            
            const list = [...ext].slice(0, 10).filter(d => !isTracker('https://' + d));
            if (list.length) {
                addDnsPrefetch(list);
            } else {
                log('[IHW] Sentinel: no clean domains');
                sessionStorage.setItem('ihw:dns_done:' + location.hostname, '1');
            }
        };

        // Пробуем через IntersectionObserver (пользователь скроллит)
        try {
            obs = new IntersectionObserver(entries => {
                if (entries[0].isIntersecting) {
                    log('[IHW] Sentinel: viewport intersected');
                    doScan('observer');
                }
            }, { rootMargin: '500px 0px' });
            obs.observe(s);
        } catch(e) {
            log('[IHW] Sentinel: observer failed, force scan');
            doScan('observer-fail');
            return;
        }

        // Fallback 1: через 3с если страница короткая (sentinel уже в viewport)
        fallbackTimer = setTimeout(() => {
            const rect = s.getBoundingClientRect();
            if (rect.top < window.innerHeight + 500) {
                log('[IHW] Sentinel: fallback short-page');
                doScan('fallback-short');
            }
        }, 3000);

        // Fallback 2: через 8с в любом случае (force scan)
        setTimeout(() => {
            if (!dnsPrefetchDone) {
                log('[IHW] Sentinel: fallback force');
                doScan('fallback-force');
            }
        }, 8000);
    };

    /* ── ФИНАЛИЗАЦИЯ ────────────────────────────────────── */
    const onLoadHandler = () => {
        if (_initDone) return;
        _initDone = true;
        log('[IHW] Finalize: running');
        if (PAGE === 'Mixed Content') {
            _runWhenIdle(runRenderOpts, 500);
            log('[IHW] Finalize: launching sentinel');
            createSecondScreenSentinel();
        }
        setTimeout(() => mo.disconnect(), PAGE === 'Video Content' ? 30000 : 4000);
        document.querySelectorAll('noscript').forEach(n => n.remove());
        if (EXTREME_MODE) {
            document.querySelectorAll('video:not([data-ihw-boosted])').forEach(v => { try { v.disablePictureInPicture = true; } catch (e) { } });
        }
        if (DEBUG) _logExtendedMetrics(_getModeLabel());
    };

    // v3.0.16-fix5: гарантированный запуск — если уже complete, вызываем сразу
    if (document.readyState === 'complete') {
        log('[IHW] Finalize: document already complete, calling directly');
        onLoadHandler();
    } else {
        log('[IHW] Finalize: waiting for load event');
        window.addEventListener('load', onLoadHandler, { once: true });
    }

})();