Ускоряет загрузку страниц: защита LCP, корректный lazy-loading, приоритет видео на хостингах.
// ==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 });
}
})();