Speeds up page loading: on video-hosting sites, priority is given to the main video; on other sites, priority is given to visible content.
// ==UserScript==
// @name I Hate Waiting
// @name:en I Hate Waiting
// @namespace https://tampermonkey.net/
// @version 2.2.5.5
// @license MIT
// @description Ускоряет загрузку страниц: на видеохостингах — приоритет главному видео, на остальных — приоритет видимому контенту.
// @description:en Speeds up page loading: on video-hosting sites, priority is given to the main video; on other sites, priority is given to visible content.
// @author Qwen + Claude + Grok + DeepSeek + twicks other programmers
// @match *://*/*
// @grant none
// @run-at document-start
// @compatible firefox 132+ Violentmonkey
// @compatible firefox 132+ Tampermonkey
// @compatible firefox 132+ GreaseMonkey
// @compatible chrome 101+ Violentmonkey
// @compatible chrome 101+ Tampermonkey
// @compatible chrome 101+ ScriptCat
// @compatible safari 18.0+ Stay
// @compatible edge 101+ Tampermonkey
// @compatible opera 87+ Tampermonkey
// @compatible android Via Browser
// ==/UserScript==
(function () {
'use strict';
/* ── ЗАЩИТА ОТ ФРЕЙМОВ ──────────────────────────────── */
// Скрипт работает только на основной странице.
// Плюсы: нет дублирования логов, не трогаем DOM внутри iframe-плееров,
// меньше MutationObserver-ов в памяти. Видео ищем через DOM основной
// страницы — boostMainVideo не теряет доступ к iframe-плеерам.
if (window.top !== window) return;
// ═══════════════════════════════════════════════════════════════
// 🔧 ВРЕМЕННЫЙ ПЕРЕКЛЮЧАТЕЛЬ РЕЖИМОВ ДЛЯ ТЕСТОВ (раскомментировать нужную строку)
// ═══════════════════════════════════════════════════════════════
// Для OFF:
// localStorage.setItem('ihw:off:' + location.hostname, '1');
// Для ON (явный):
// localStorage.setItem('ihw:on:' + location.hostname, '1');
// Для ON[E] (Extreme):
// localStorage.setItem('ihw:extreme:' + location.hostname, '1');
// Для AUTO (очистить всё, вернуть стандартное поведение):
// ['off','on','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname));
// ═══════════════════════════════════════════════════════════════
/* ── ОТЛАДКА ────────────────────────────────────────── */
// true — все сообщения видны в консоли F12 (режим разработки)
// false — лог отключён полностью (режим релиза, нет затрат на вывод)
const DEBUG = false;
const log = (...args) => { if (DEBUG) console.log(...args); };
/* ── ОПРЕДЕЛЕНИЕ УСТРОЙСТВА ─────────────────────────── */
// Mode: "Mobile" или "Desktop"
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
|| (navigator.maxTouchPoints > 1 && window.innerWidth < 1024);
const MODE = isMobile ? 'Mobile' : 'Desktop';
/* ── НАСТРОЙКИ ──────────────────────────────────────── */
// true — при скрытии вкладки ставить видео на паузу, при возврате — возобновлять.
// Не влияет на картинку-в-картинке (PiP): если видео в PiP — оно продолжает играть.
// Полезно на мобиле (АКБ) и Desktop (CPU/GPU в фоне). Отключить если сайт сам
// управляет паузой (на YouTube Desktop) или поведение кажется лишним.
// работает только на Youtube (там где плеер в главном окне, а не в защищенном iframe)
const PAUSE_ON_HIDDEN = true;
// ── SEEK HACK ─────────────────────────────────────────────────
// Эксперимент: микро-сдвиг currentTime (+0.01) триггерит догрузку
// следующего HLS/DASH сегмента до начала воспроизведения.
// Работает только на сайтах, где главное видео представлено нативным элементом <video> и скрипт определяет его по стратегии 1.
// НЕ применять на YouTube, Twitch и др. (нестабильно).
// По умолчанию ВЫКЛЮЧЕНО. Включить можно двумя способами:
// 1. Установить константу ниже в true
// 2. Включить через localStorage: localStorage.setItem('ihw:seekhack', '1')
const SEEK_HACK_ENABLED = false;
const SEEK_HACK = SEEK_HACK_ENABLED || localStorage.getItem('ihw:seekhack') === '1';
// Список доменов, на которых seek-hack НЕ применяется (нестабильно)
const SEEK_HACK_EXCLUDE = [
// 'youtube.com',
// 'youtu.be',
// 'twitch.tv'
];
// ── EXTREME MODE ──────────────────────────────────────────
// Агрессивный режим экономии трафика и ресурсов. Управляется каруселью кнопки.
// Для ручного тестирования: вручную задать '1' в localStorage ('ihw:extreme:hostname').
// EXTREME_MODE: читаем ДО объявления SITE_KEY — используем inline ключ
// В режиме ON[A] EXTREME_MODE определяется через _resolveGlobalMode() ниже.
// Прямое значение используется только при явном локальном ON[E].
const _localExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';
/* ── АВТООПРЕДЕЛЕНИЕ ГЛОБАЛЬНОГО РЕЖИМА (_resolveGlobalMode) ─────────────
* Функция выносится отдельно для будущего расширения условий (качество сети и т.д.)
* Текущая логика:
* Mobile → ON[E] (Extreme): экономим трафик и АКБ на мобиле всегда
* Desktop → ON: обычное ускорение без визуального ущерба
*
* Будущие условия (не реализованы):
* navigator.connection.effectiveType === '2g' || 'slow-2g' → ON[E]
* DOM > 5000 узлов → ON[E]
* TTFB > 1500 мс → ON[E]
─────────────────────────────────────────────────────────────────────── */
function _resolveGlobalMode() {
// В будущем: добавить проверки сети, DOM-размера и TTFB
return isMobile ? 'EXT' : 'ON'; // Mobile→Extreme, Desktop→Normal
}
// Определяем глобальный режим по-умолчанию (используется при ON[A])
const _globalAutoMode = _resolveGlobalMode(); // 'ON' или 'EXT'
// EXTREME_MODE = true если:
// - явный локальный ON[E] (пользователь выбрал Extreme для этого сайта)
// - ИЛИ режим ON[A] + _globalAutoMode === 'EXT' (авто: мобиль)
const _isAutoMode = localStorage.getItem('ihw:auto:' + location.hostname) === '1'
&& !localStorage.getItem('ihw:off:' + location.hostname);
const EXTREME_MODE = _localExt || (_isAutoMode && _globalAutoMode === 'EXT');
// Флаг защиты от двойного запуска onLoadHandler —
// при readyState=interactive вызываем сразу, но событие load всё равно придёт
let _initDone = false;
// _t0 — момент запуска скрипта (мс от навигации). Вне DEBUG-блока: нужен всегда
// для показа «load − скрипт_запустился» на кнопке независимо от DEBUG-флага.
const _t0 = performance.now();
function _getModeLabel() {
const isOff = localStorage.getItem(SITE_KEY) === '1';
const isExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';
const isAuto = localStorage.getItem('ihw:auto:' + location.hostname) === '1';
if (isOff) return '[OFF]';
if (isExt) return '[ON[E]]';
if (isAuto) {
const actual = EXTREME_MODE ? 'ON[E]' : 'ON';
return `[ON[A]=${actual}]`;
}
// Явный ON (установленный через кнопку)
return '[ON]';
}
/* ── ВЫВОД РАСШИРЕННЫХ МЕТРИК (после load) ──────────────────────────── */
/* ── ЗАМЕР ВРЕМЕНИ ЗАГРУЗКИ ────────────────────────────────── */
// _showDeltaOnBtn — показывает «▲ X мс» на кнопке ON/OFF после загрузки страницы:
// X = load − скрипт_запустился (= loadEventEnd − _t0)
// Это время от старта нашего скрипта до полной загрузки страницы.
// При ON браузер работает с нашими оптимизациями, при OFF — без них.
// Разница X(OFF) − X(ON) = реальный выигрыш от скрипта.
// Кнопка показывает значение 3 секунды, затем возвращает ON/OFF.
// _printTiming — вывод всех метрик в консоль (только при DEBUG=true):
// • DOMContentLoaded — страница видна (HTML разобран, синхронные скрипты выполнены)
// • load — всё загружено (картинки, шрифты, iframe)
// • load − DCL — время догрузки ресурсов после первого рендера
// • load − старт — КЛЮЧЕВАЯ метрика для сравнения ON vs OFF
// • скрипт запустился — для справки, когда Tampermonkey внедрил скрипт
// Всё запускается строго после load-события — до него loadEventEnd = 0.
// Работает при ON и при OFF одинаково (вызов ниже, до early-return SITE_KEY).
function _logExtendedMetrics(modeLabel) {
const nav = performance.getEntriesByType('navigation')[0];
// Если load ещё не произошёл, пробуем снова через 100 мс
if (!nav || nav.loadEventEnd <= 0) {
setTimeout(() => _logExtendedMetrics(modeLabel), 100);
return;
}
const re_d = nav.responseEnd;
const ttfb = Math.round(nav.responseStart - nav.requestStart);
const dcl = nav.domContentLoadedEventEnd.toFixed(0);
const load = nav.loadEventEnd.toFixed(0);
const dDCL = Math.round(nav.loadEventEnd - nav.domContentLoadedEventEnd);
const ldRaw= Math.round(nav.loadEventStart - re_d);
const dom = document.getElementsByTagName('*').length;
const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;
const loadMs = Math.round(nav.loadEventEnd - _t0);
const fcp = performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint');
const fcpMs= fcp ? Math.round(fcp.startTime - re_d) : null;
console.group(`[IHW] ${modeLabel} ${location.hostname}`);
console.log(` скрипт запустился : ${_t0.toFixed(0)} мс`);
console.log(` TTFB : ${ttfb} мс ← время ответа сервера`);
console.log(` loadStart−responseEnd: ${ldRaw} мс ← not-lazy ресурсы без TTFB`);
console.log(` DOMContentLoaded : ${dcl} мс`);
console.log(` load : ${load} мс`);
console.log(` load − DCL (▼) : ${dDCL} мс ← откл. ресурсы (>0 = скрипт помог)`);
console.log(` load − старт (▲) : ${loadMs} мс ← СРАВНИВАЙ ON vs OFF`);
console.log(` DOM узлов : ${dom}${dom > 3000 ? ' ← тяжёлая страница' : ''}`);
console.log(` Размер страницы : ${kb ? kb + ' кб' : 'кэш'}`);
console.log(` Заблокировано : ${_blockedCount}`);
if (_canplayMs) console.log(` Canplay плеера : ${_canplayMs} мс ← нативный <video>`);
console.groupEnd();
}
let _btn = null; // ссылка на кнопку — для кратковременного показа времени загрузки
let _blockedCount = 0; // счётчик заблокированных трекеров/prefetch (для диагностики)
let _canplayMs = 0; // время до готовности плеера (canplay) — только Strategy 1 (нативный <video>)
/* ── ЧИСТЫЕ МЕТРИКИ (без влияния сети и тяжёлого медиа) ─────────────
* Идея DeepSeek: вычитаем responseEnd (получен последний байт HTML) из
* времён DOM-событий. Получаем время, потраченное ТОЛЬКО на обработку —
* без TTFB, без загрузки картинок/видео/шрифтов.
*
* domInteractive − responseEnd = парсинг HTML + синхронные скрипты
* loadEventStart − responseEnd = всё вышеперечисленное + отложенные ресурсы
* (но НЕ картинки если loading=lazy)
*
* Работает при ON и при OFF → честное сравнение влияния скрипта.
* Шрифты (font-display:swap) — не искажают domInteractive (async, после парсинга).
* Изображения с loading=lazy — не ждутся при domInteractive.
* ВЫВОД только при DEBUG=true — нулевая нагрузка в релизе.
──────────────────────────────────────────────────────────────────── */
if (DEBUG) {
const _logClean = () => {
const nav = performance.getEntriesByType('navigation')[0];
if (!nav) return;
const re = nav.responseEnd; // конец получения HTML
const modeLabel = _getModeLabel();
// Для краткости можно оставить только метку, без лишних пробелов
console.group(`[IHW] ${modeLabel} Чистые метрики (без сети) — ${location.hostname}`);
const diRaw = Math.round(nav.domInteractive - re);
const dclRaw = Math.round(nav.domContentLoadedEventStart - re);
console.log(` domInteractive − responseEnd : ${diRaw} мс ← HTML разобран, DOM построен (текст виден)`);
console.log(` domContentLoaded − responseEnd: ${dclRaw} мс ← + выполнены sync скрипты`);
// loadEventStart недоступен при DOMContentLoaded (равен 0 до load события).
// Полное время загрузки смотри на кнопке (▲) после load.
console.log(' (Сравни ON vs OFF — разница = чистый эффект скрипта)');
// FCP если доступен
const fcp = performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint');
if (fcp) console.log(` FCP − responseEnd : ${Math.round(fcp.startTime - re)} мс ← первый контент на экране`);
console.groupEnd();
};
// Запускаем после DOMContentLoaded — раньше некоторые значения могут быть 0
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(_logClean, 0), { once: true });
} else {
setTimeout(_logClean, 0);
}
}
/* ── ИСКЛЮЧЕНИЯ САЙТОВ ──────────────────────────────── */
const SITE_KEY = 'ihw:off:' + location.hostname;
if (localStorage.getItem(SITE_KEY) === '1') {
_renderBtn(); // OFF state reflected via _getMode()
// OFF режим: полные метрики выводим в консоль после load (Qwen: OFF должен давать те же метрики)
if (DEBUG) {
_logExtendedMetrics('[OFF]');
}
return;
}
/* ── ОПРЕДЕЛЕНИЕ БРАУЗЕРА ───────────────────────────────────
* Feature-detection, не UA-sniffing: надёжно при смене UA.
* isFirefox: InstallTrigger присутствует только в Gecko.
* isChromium: window.chrome без Gecko → Chromium-based.
* CSS.supports guard eliminé — браузеры без поддержки content-visibility игнорируют стиль (Chrome 85+ / Firefox 125+).
─────────────────────────────────────────────────────────── */
const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
const isChromium = !isFirefox && !!window.chrome;
log(`[IHW] Browser: ${isFirefox ? 'Firefox' : isChromium ? 'Chromium' : 'Other'}`);
/* ── ОПРЕДЕЛЕНИЕ ТИПА СТРАНИЦЫ ──────────────────────── */
// Page: "Video Content" — популярные видеохостинги
// Page: "Mixed Content" — все остальные сайты
const VIDEO_HOSTS = [
'youtube.com', 'youtu.be',
'vimeo.com', 'rutube.ru', 'twitch.tv', 'dailymotion.com',
'ok.ru', 'vk.com', 'vkvideo.ru', 'video.mail.ru', 'my.mail.ru',
'bilibili.com', 'bilibili.tv', 'tiktok.com', 'odysee.com',
'smotret.tv', 'platform.rambler.ru', 'kinopoisk.ru',
'zetflix.bet', 'zetflix.to', 'zetflix.online', 'zetflix.app' // кино-стриминг
];
// Поддомены или hostname, которые НЕ должны считаться видеохостингами
// (даже если домен попал в VIDEO_HOSTS или путь совпал с VIDEO_PATH_SEGMENTS)
const VIDEO_HOST_EXCEPTIONS = [
'alice.yandex.ru', // AI-чат (полный hostname)
];
/* ── VIDEO_PATH_SEGMENTS ─────────────────────────────────────────────────
* Сегменты пути URL, по которым обычный сайт считается Video Content.
* Проверяются как ЦЕЛЫЕ сегменты пути (не подстроки), поэтому:
* /video ✓ /live ✓ /videos/12345 ✓
* /developer ✗ /surveillance ✗ /livewire ✗
* Добавляйте/удаляйте по необходимости.
─────────────────────────────────────────────────────────────────────── */
const VIDEO_PATH_SEGMENTS = new Set([
'video', // example.com/video или example.com/video/...
'videos', // example.com/videos/12345-abc
'live', // example.com/live или example.com/live/stream
'clip', // example.com/clip/...
'stream', // example.com/stream
'watch', // example.com/watch?v=... (не-YouTube)
'player', // example.com/player/...
]);
/* ── КАРТА CDN ДЛЯ PRECONNECT ───────────────────────────────────────────
* Статический preconnect по хостингу — греет соединения до нахождения видео.
* Особенно полезно для iframe-плееров (Vimeo, Twitch, VK, Dailymotion),
* где PerformanceObserver не видит сетевые запросы внутри cross-origin iframe.
* Данные: Qwen+Grok+DeepSeek анализ реальных CDN 2026.
─────────────────────────────────────────────────────────────────────── */
const VIDEO_CDN_MAP = {
'youtube.com': ['googlevideo.com', 'ytimg.com', 'ggpht.com'],
'youtu.be': ['googlevideo.com', 'ytimg.com'],
'rutube.ru': ['cdnvideohub.com', 'yandex.net', 'rtbcdn.ru'],
'vimeo.com': ['akamaized.net', 'vimeocdn.com', 'player.vimeo.com'],
'ok.ru': ['mycdn.me', 'ok.ru', 'okcdn.ru'],
'vk.com': ['userapi.com', 'vkuser.net'],
'vkvideo.ru': ['userapi.com', 'vkuser.net'],
'dailymotion.com': ['dmcdn.net', 'cloudfront.net'],
'twitch.tv': ['ttvnw.net', 'jtvnw.net', 'player.twitch.tv'],
'bilibili.com': ['bilivideo.com', 'akamaized.net'],
'tiktok.com': ['tiktokcdn.com', 'bytecdn.com'],
'odysee.com': ['odycdn.com', 'cloudflare-stream.com'],
'dzen.ru': ['dzen.ru', 'yandex.net'],
};
// Глобальный Set дедупликации — один preconnect на хост за сессию
const _preconnected = new Set();
// Добавить <link rel="preconnect"> с дедупликацией (Qwen+DeepSeek)
function _doPreconnect(host) {
if (!host || _preconnected.has(host) || host === location.hostname) return;
if (isTracker('https://' + host)) return; // не греем трекеры
_preconnected.add(host);
const _lnk = document.createElement('link');
_lnk.rel = 'preconnect';
_lnk.href = 'https://' + host;
_lnk.crossOrigin = 'anonymous';
document.head.appendChild(_lnk);
log('[IHW Video] Preconnect →', host);
}
// Проверить — является ли хост валидным видео-CDN (не трекер, не same-origin) (Qwen)
function _isVideoCDN(host) {
if (!host || host === location.hostname) return false;
if (isTracker('https://' + host)) return false;
// Проверка по карте
const h = location.hostname.replace(/^www\./, '');
const cdns = VIDEO_CDN_MAP[h];
if (cdns && cdns.some(c => host.includes(c))) return true;
// Универсальные паттерны видео-CDN
return /googlevideo|cdnvideohub|akamaized|ttvnw|bilivideo|odycdn|vimeocdn|dmcdn|userapi|vkuser/i.test(host);
}
/* ── ДИНАМИЧЕСКИЙ PRECONNECT ДЛЯ НАТИВНОГО ВИДЕО ─────────────────
* Вызывается из стратегии 1 после нахождения главного видео.
* Пытается извлечь CDN из currentSrc (прямые mp4) или через PerformanceObserver (MSE/HLS).
* Использует уже существующие _doPreconnect, _isVideoCDN, isTracker.
* Только Desktop.
*/
function initDynamicPreconnect(video) {
if (isMobile || !video) return;
log('[IHW Video] Dynamic preconnect observer started for', video.src ? 'direct URL' : 'MSE/blob');
const tryDirect = () => {
try {
const src = video.currentSrc || video.src;
if (src && !src.startsWith('blob:') && !src.startsWith('data:')) {
const host = new URL(src, location.origin).hostname;
if (host !== location.hostname) {
_doPreconnect(host);
return true;
}
}
} catch(e) {}
return false;
};
if (!tryDirect() && window.PerformanceObserver) {
try {
const observer = new PerformanceObserver((list, obs) => {
for (const entry of list.getEntries()) {
const name = entry.name;
// Расширенный фильтр специально для YouTube + универсальные сегменты
const isVideoResource = /\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(name) ||
/\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4|videoplayback|initplayback/i.test(name) ||
/googlevideo|cdnvideohub|vimeocdn|dmcdn/i.test(name);
if (!isVideoResource) continue;
try {
const segHost = new URL(name).hostname;
if (segHost === 'i.vimeocdn.com') continue; // известный ложный срабатывающий хост
if (_isVideoCDN(segHost) || !isTracker('https://' + segHost)) {
_doPreconnect(segHost); // логируем даже поддомены
// Не disconnect — пусть ловит все поддомены YouTube
}
} catch(e) {}
}
});
observer.observe({ type: 'resource', buffered: true });
log('[IHW Video] PerformanceObserver для CDN запущен (YouTube-style)');
// Таймаут 20 сек — YouTube иногда долго буферизирует первый сегмент
setTimeout(() => { try { observer.disconnect(); } catch(e) {} }, 20000);
} catch(e) {
log('[IHW] PerformanceObserver error:', e);
}
}
if (video.readyState >= 1) tryDirect();
else video.addEventListener('loadedmetadata', tryDirect, { once: true });
}
/* ── initVideoPreconnect(): единая функция preconnect для ВСЕХ стратегий ──
* Вызывается один раз при обнаружении Video Content страницы.
* Действие 1: статический preconnect по карте CDN — работает для iframe-плееров
* (Vimeo, Twitch, VK, Dailymotion — там PerformanceObserver не видит iframe-сегменты).
* Действие 2: PerformanceObserver — ловит реальные CDN-хосты из первых сегментов
* MSE/DASH/HLS. Работает для YouTube, Rutube, OK, Bilibili (нативный <video>).
* Расширенный паттерн Qwen: .ts .m4s .mp4 .webm .m3u8 .mpd + ?range= /seg- /chunk-
* Только Desktop — мобиль не тратит трафик (isMobile guard).
─────────────────────────────────────────────────────────────────────── */
function initVideoPreconnect() {
if (isMobile) return;
// ── Шаг 1: статический preconnect по карте для текущего хостинга ──
const _h = location.hostname.replace(/^www\./, '');
const _cdns = VIDEO_CDN_MAP[_h] || [];
_cdns.forEach(cdn => _doPreconnect(cdn));
if (_cdns.length) log('[IHW Video] Статический preconnect:', _cdns.join(', '));
// ── Шаг 2: PerformanceObserver для динамических CDN (MSE/DASH сегменты) ──
// Ловит первый реальный видео-сегмент и греет его CDN-хост.
// Расширенный паттерн (Qwen+DeepSeek): .ts .m4s .mp4 .webm .mpd + query-паттерны
if (!window.PerformanceObserver) return;
try {
const _po = new PerformanceObserver((list, obs) => {
for (const entry of list.getEntries()) {
const n = entry.name;
// Паттерн видео-сегментов: расширения + query-строки HLS/DASH
if (!/\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)/i.test(n)
&& !/\/seg-|\/chunk-|\/fragment-|\?range=|init\.mp4/i.test(n)) continue;
try {
const segHost = new URL(n).hostname;
// Пропускаем i.vimeocdn.com (постеры, не видео-CDN)
if (segHost === 'i.vimeocdn.com') continue;
if (_isVideoCDN(segHost) || (!isTracker('https://' + segHost) && segHost !== location.hostname)) {
_doPreconnect(segHost);
obs.disconnect();
break;
}
} catch(e) {}
}
});
_po.observe({ type: 'resource', buffered: true });
// Самоочистка через 15с (DeepSeek: сегменты обычно идут в первые 3–8с)
setTimeout(() => { try { _po.disconnect(); } catch(e) {} }, 15000);
} catch(e) { log('[IHW Video] PerformanceObserver unavail:', e); }
}
// const isVideoHost = VIDEO_HOSTS.some(h => location.hostname.endsWith(h));
const isVideoHost = (() => {
const host = location.hostname;
// 1. Исключения по hostname — всегда Mixed Content
if (VIDEO_HOST_EXCEPTIONS.includes(host)) return false;
// 2. Домен в списке видеохостингов
if (VIDEO_HOSTS.some(h => host.endsWith(h))) return true;
// 3. Путь URL содержит видео-сегмент как ЦЕЛЫЙ сегмент пути
// /live ✓ /live/stream ✓ /livewire ✗ /surveillance ✗
const segments = location.pathname.split('/');
return segments.some(seg => VIDEO_PATH_SEGMENTS.has(seg.toLowerCase().split('?')[0]));
})();
const PAGE = isVideoHost ? 'Video Content' : 'Mixed Content';
log(`[IHW] Mode:${MODE} | Page:${PAGE}`);
/* ── ТРЕКЕРЫ И ИСКЛЮЧЕНИЯ ───────────────────────────── */
// Домены трекеров — блокируем sendBeacon, удаляем из DOM, исключаем из dns-prefetch
const TRACKERS = [
// ── Глобальные (Google/Meta/Microsoft) ──────────────────────
'google-analytics.com', 'googletagmanager.com', 'doubleclick.net',
'googlesyndication.com', 'googleadservices.com', 'google-analytics.com',
'googletagservices.com', 'google.com/ads', // 'gstatic.com', (не грузится promt Gemini)
'facebook.com', 'connect.facebook.net', 'fbcdn.net', 'fb.com',
'scorecardresearch.com', 'quantserve.com', 'outbrain.com', 'taboola.com',
'moatads.com', 'adnxs.com', 'openx.net', 'adtelligent.com',
'advertising.com', 'adsrvr.org', 'rubiconproject.com', 'pubmatic.com',
'casalemedia.com', 'smartadserver.com', 'appnexus.com', 'criteo.com',
'bidswitch.net', 'rlcdn.com', 'bluekai.com', 'demdex.net',
'amazon-adsystem.com', 'adcolony.com', 'media.net', '33across.com',
// ── Аналитика и метрики ──────────────────────────────────────
'clarity.ms', 'hotjar.com', 'mixpanel.com', 'segment.io', 'segment.com',
'heap.io', 'heapanalytics.com', 'amplitude.com', 'fullstory.com',
'logrocket.com', 'mouseflow.com', 'inspectlet.com', 'clicktale.net',
'contentsquare.net', 'optimizely.com', 'crazy-egg.com',
// ── Российские/СНГ-трекеры ───────────────────────────────────
'mc.yandex.ru', 'mc.yandex.net', 'top-fwz1.mail.ru',
'redirect.appmetrica.yandex.com', 'counter.yadro.ru',
'cnt.tbz.liveinternet.ru', 'open.rambler.ru',
'ads.vk.com', 'vk.com/rtr', 'target.my.com', 'mail.ru/counter',
'rb.mail.ru', 'tbgcounter.com',
// ── Сессионные записи и CX-аналитика ────────────────────────
'static.hotjar.com', 'vc.hotjar.io',
'stats.g.doubleclick.net',
'bat.bing.com', 'ad.doubleclick.net', 'ad.atdmt.com',
// ── Рекламные сети ───────────────────────────────────────────
'adsymptotic.com', 'creativecdn.com', 'go.sonobi.com',
'snigelweb.com', 'sharethrough.com', 'triplelift.com',
'yieldmo.com', 'yieldlab.net', 'smartclip.net',
'spoutable.com', 'undertone.com', 'indexexchange.com',
'sovrn.com', 'lijit.com', 'contextweb.com', 'pulsepoint.com',
// ── Push-уведомления и подписки ──────────────────────────────
'onesignal.com', 'pusher.com', 'pushcrew.com',
'aimtell.com', 'subscribers.com', 'pushassist.com',
// ── Прочие трекеры ───────────────────────────────────────────
'chartbeat.com', 'chartbeat.net', 'krxd.net',
'clickagy.com', 'agkn.com', 'exelator.com', 'eyeota.net',
'spotxchange.com', 'cxense.com', 'adobedtm.com', 'omtrdc.net',
'2mdn.net', 'hlserve.com', 'cdn.syndication.twimg.com',
];
// Домены-исключения — НЕ удаляем их элементы из DOM даже если совпадают с трекером.
// Добавлять сюда сервисы, которые ломаются при агрессивной фильтрации DOM
// (капчи, антибот-проверки, платёжные виджеты и т.п.)
const TRACKER_EXCEPTIONS = [
'cloudflare.com', // Cloudflare Challenge / Turnstile капча
'challenges.cloudflare.com', // прямой домен капчи Cloudflare
// 'recaptcha.net', // Google reCAPTCHA — раскомментировать если сломается
// 'hcaptcha.com', // hCaptcha — раскомментировать если сломается
];
// Проверка: является ли URL трекером
const isTracker = url => {
try {
const h = new URL(url, location.origin).hostname;
return TRACKERS.some(t => h.endsWith(t));
} catch { return false; }
};
// Проверка: находится ли URL в списке исключений (не трогаем даже если трекер)
const isException = url => {
try {
const h = new URL(url, location.origin).hostname;
return TRACKER_EXCEPTIONS.some(e => h.endsWith(e));
} catch { return false; }
};
// Блокируем sendBeacon трекеров — исключения не в TRACKERS, отдельно не нужны
const _beacon = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = (url, data) => isTracker(url) ? false : _beacon(url, data);
// font-display:swap — текст виден сразу системным шрифтом, веб-шрифт подгружается потом
const _fs = document.createElement('style');
_fs.textContent = '@font-face{font-display:swap}';
(document.head || document.documentElement).appendChild(_fs);
/* ── EXTREME MODE: ранние оптимизации ──────────────────────────────────────
* Применяются ДО проверки SITE_KEY — чтобы работали даже при OFF-состоянии
* кнопки если EXTREME_MODE=true глобально.
*
* ИСТОЧНИКИ IDEAS (с указанием эффекта):
* • Fake Save-Data+slow-2g (Grok): сайты сами снижают качество картинок,
* отключают автозапуск каруселей и видео-фонов. Нет DOM-вмешательства.
* • Aggressive CSS (Grok+DeepSeek+Claude): отключает GPU-тяжёлые эффекты
* (filter, backdrop-filter, box-shadow, text-shadow, background-image).
* Дизайн становится «плоским», но сайт функционирует корректно.
* • Удаление link[rel*=pre] кросс-домен (Grok+Claude+DeepSeek): убирает
* 5–15 лишних запросов на старте страницы.
* • srcset removal (ChatGPT+Claude): экономит трафик на новостных/фото
* сайтах, исключая выбор тяжёлой версии изображения.
* • preload=none для видео (Claude+DeepSeek): не качаем видео пока не нужно.
* • decoding=async (Qwen+ChatGPT): снимает блокировку main thread при
* декодировании изображений (−5–10% jank при массовых картинках).
* • autofocus removal (Qwen+ChatGPT): предотвращает forced layout + scroll-прыжок.
* • spellcheck=false (Qwen+ChatGPT): снижает CPU при вводе текста.
* • border-radius:0 (DeepSeek): ускоряет отрисовку на слабых GPU.
* • fetchPriority=low для iframe (Qwen+ChatGPT): iframe — один из тяжёлых ресурсов.
────────────────────────────────────────────────────────────────────────── */
if (EXTREME_MODE) {
// Fake Save-Data + slow-2g: сайты сами адаптируют контент
// Единственный способ заставить СЕРВЕР экономить трафик без изменения DOM
try {
const _fakeConn = { effectiveType: 'slow-2g', saveData: true, rtt: 2200, downlink: 0.05 };
Object.defineProperty(navigator, 'connection', { value: _fakeConn, configurable: true });
log('[IHW Extreme] Fake Save-Data + slow-2g активирован');
} catch (e) {}
// Aggressive CSS: отключаем GPU-тяжёлые эффекты и декоративные элементы
// Nemotron п.5: добавляем класс на <html> вместо голого `* { }`.
// Браузер начинает поиск совпадений с корня и может пропустить
// поддеревья без целевых элементов — это быстрее чем матч каждого узла.
// Также упрощает отладку: `html.ihw-extreme` видно в DevTools.
// Null guard: при run-at:document-start в Firefox documentElement может
// ещё не существовать при Ctrl+Shift+R — без guard скрипт падает и кнопка исчезает
document.documentElement?.classList.add('ihw-extreme');
const _xCss = document.createElement('style');
_xCss.textContent =
// Удаляем фоны, тени, фильтры — основная нагрузка на GPU
'html.ihw-extreme *,html.ihw-extreme :before,html.ihw-extreme :after{' +
'background-image:none!important;filter:none!important;' +
'backdrop-filter:none!important;box-shadow:none!important;' +
'text-shadow:none!important;border-radius:0!important;' +
// Отключаем анимации и переходы
'animation:none!important;transition:none!important}' +
'html.ihw-extreme img,html.ihw-extreme video{image-rendering:crisp-edges!important}';
(document.head || document.documentElement).appendChild(_xCss);
log('[IHW Extreme] Агрессивный CSS активирован');
// preload=none для всех медиа — не качаем видео/аудио до клика
document.querySelectorAll('video,audio').forEach(m => {
m.preload = 'none'; m.autoplay = false;
});
// autofocus removal — предотвращает forced layout + scroll-прыжок при загрузке
document.querySelectorAll('[autofocus]').forEach(el => el.removeAttribute('autofocus'));
// spellcheck=false — снижает CPU при вводе текста (особенно соцсети/форумы)
document.querySelectorAll('input,textarea,[contenteditable]')
.forEach(el => { el.spellcheck = false; });
log('[IHW Extreme] Режим активирован — трафик и GPU экономятся');
}
// ---------- CSS: принудительная видимость и опционально отключение плавного скролла ----------
/* ── СПИСОК САЙТОВ-ЧАТОВ ────────────────────────────── */
// Используется тремя блоками: scroll-behavior, content-visibility, lazy-iframe.
// БАГ v1.1.5: content-visibility:auto на main>div и lazy-iframe ломают
// динамический скролл к полю ввода (поле "улетает" вверх после отправки).
// FIX v1.1.6: для этих сайтов пропускаем runRenderOpts() и iframe-lazy целиком.
const noScrollBehaviorSites = [
'chat.deepseek.com',
'chatgpt.com',
'grok.com',
'qwen.ai',
'claude.ai',
'claude.site',
'kagi.com', // другие чаты с динамическим скроллом
'perplexity.ai',
'alice.yandex.ru',
'openrouter.ai',
'chat.mistral.ai',
];
const currentHost = window.location.hostname;
// shouldSkipScroll === true означает: это AI-чат с динамическим скроллом.
// На таких сайтах НЕ отключаем smooth scroll, НЕ применяем content-visibility,
// НЕ делаем lazy-iframe — всё это ломает поле ввода.
const shouldSkipScroll = noScrollBehaviorSites.some(site => currentHost.includes(site));
// Базовый стиль: принудительная видимость (идея из PureRender)
// scroll-behavior:auto — отменяет smooth scroll сайта, скролл реагирует 1-в-1.
// visibility/opacity — некоторые сайты прячут body до загрузки рекламы/попапов,
// это принудительно раскрывает страницу сразу.
const baseCss = 'html,body{visibility:visible!important;opacity:1!important}';
const _css = document.createElement('style');
_css.textContent = baseCss;
(document.head || document.documentElement).appendChild(_css);
// Дополнительный стиль: отключение плавного скролла (если сайт не в исключениях)
if (!shouldSkipScroll) {
const scrollCss = 'html,body{scroll-behavior:auto!important}';
const _scrollCss = document.createElement('style');
_scrollCss.textContent = scrollCss;
(document.head || document.documentElement).appendChild(_scrollCss);
}
/* ── НАБЛЮДАТЕЛЬ ЗА DOM ─────────────────────────────── */
const seen = new WeakSet();
const processNode = node => {
if (!(node instanceof HTMLElement) || seen.has(node)) return;
seen.add(node);
const tag = node.tagName;
const src = node.src || node.href || '';
// Трекеры — удаляем при вставке в DOM, но только если не в списке исключений
if (src && isTracker(src) && !isException(src)) { node.remove(); _blockedCount++; return; }
// prefetch-ссылки — удаляем (не тратим ресурсы впрок),
// кроме исключений (Cloudflare Challenge использует prefetch для своей проверки).
// На видеохостингах: сохраняем same-origin prefetch — это SPA-навигация
// (YouTube, VK и др. префетчат JSON следующей вкладки; без них вкладки
// «Videos», «Streams» и переходы между страницами остаются пустыми).
if (tag === 'LINK' && node.rel === 'prefetch' && !isException(src)) {
if (isVideoHost) {
try { if (new URL(src, location.origin).hostname.endsWith(location.hostname)) return; } catch {}
}
node.remove(); _blockedCount++; return;
}
// Внешние шрифты — откладываем, не блокируем рендер текста
// ── Приоритет canplay > шрифты (Grok+Qwen+DeepSeek, v2.2.5.1) ─────
// Mixed Content → 2000 мс: LCP/FCP в приоритете, шрифты грузятся быстро
// Video Content → 6000 мс: видео-сегменты не конкурируют со шрифтами
// YouTube стратегия 1: _deferFontsUntilCanplay(main) уже управляет шрифтами;
// атрибут data-ihwFontDeferred блокирует наш setTimeout (нет конфликта)
// Вместо (Google Fonts): /fonts\.g(oogle|static)apis\.com/.test(src)
// расширяем список возможных внешних шрифтов для откладывая загрузки: Roboto, Open Sans; Adobe Fonts; GDPR-альтернатива Google Fonts; Monotype; cloudflare, jsDelivr, unpkg.
//
if (tag === 'LINK' && /fonts\.(googleapis|gstatic|bunny\.net)|use\.typekit\.net|fast\.fonts\.net/.test(src)) {
if (node.dataset.ihwFontDeferred) return;
node.media = 'print';
const _fd = PAGE === 'Video Content' ? 6000 : 2000;
log(`[IHW Font] отложен ${_fd}мс: ${src.split('?')[0].slice(-40)}`);
setTimeout(() => {
if (node.parentNode && !node.dataset.ihwFontDeferred) {
node.media = 'all';
log('[IHW Font] восстановлен (таймер)');
}
}, _fd);
return;
}
// Изображения — ленивая загрузка + async decoding
if (tag === 'IMG') {
if (!node.loading) node.loading = 'lazy';
node.decoding = 'async'; // снимает блокировку main thread (Qwen+ChatGPT)
}
// autofocus — предотвращает forced layout + scroll-прыжок на всех режимах (Qwen+ChatGPT)
// Безопасно: браузер по-прежнему фокусирует элемент при явном вызове .focus()
if (node.hasAttribute && node.hasAttribute('autofocus')) node.removeAttribute('autofocus');
// fetchPriority=low для не-плеерных iframe вне первого экрана (Qwen+ChatGPT)
// Применяем только если iframe не является главным плеером (проверяем размер)
if (tag === 'IFRAME' && !node.fetchPriority && !isVideoHost) {
if (node.offsetWidth < 200 || node.offsetHeight < 100) node.fetchPriority = 'low';
}
// ── EXTREME MODE: дополнительная обработка новых узлов ───────────────
if (EXTREME_MODE) {
// srcset removal — экономия трафика (ChatGPT+Claude)
if (tag === 'IMG' && node.hasAttribute('srcset')) node.removeAttribute('srcset');
// Кросс-доменные resource hints — убираем 5–15 лишних запросов (Grok+DeepSeek+Claude)
// dns-prefetch добавлен по замечанию DeepSeek/Grok к v2.0.0
if (tag === 'LINK') {
const rel = node.getAttribute('rel') || '';
if (/preload|preconnect|modulepreload|dns-prefetch/.test(rel)) {
try { if (!new URL(node.href||'',location.origin).hostname.endsWith(location.hostname)) { node.remove(); _blockedCount++; return; } } catch {}
}
}
// fetchPriority=low для медиа-тегов (кроме IFRAME — уже в общем processNode)
// кроме главного видео с установленным нашим тэгом
if (['IMG','VIDEO','AUDIO','SCRIPT'].includes(tag) && !node.fetchPriority) {
if (!(tag === 'VIDEO' && node.hasAttribute('data-ihw-boosted'))) node.fetchPriority = 'low';
}
// preload=none для нового медиа, кроме главного ивдео с заданым нашим атрибутом
// Работает даже при асинхронном порядке – если processNode сработает после boostMainVideo, атрибут уже будет, и сброс не произойдёт. Если до – то boostMainVideo потом переставит preload='auto' (и добавит атрибут), и processNode при повторном вызове (если он будет) уже не тронет видео.
if (tag === 'VIDEO' || tag === 'AUDIO') {
// Не сбрасываем preload, если видео уже помечено как "главное"
if (!node.hasAttribute('data-ihw-boosted')) {
node.preload = 'none';
node.autoplay = false;
}
}
// spellcheck + contenteditable — снижает CPU при вводе (Qwen+ChatGPT)
// contenteditable добавлен по замечанию Qwen к v2.0.0
if (tag === 'INPUT' || tag === 'TEXTAREA' || node.hasAttribute('contenteditable'))
node.spellcheck = false;
}
};
const mo = new MutationObserver(muts => {
for (const m of muts)
for (const n of m.addedNodes) processNode(n);
});
mo.observe(document.documentElement, { childList: true, subtree: true });
/* ── ОБЩИЕ ОПТИМИЗАЦИИ РЕНДЕРА (Desktop + Mobile) ───── */
// Запускаем на idle ПОСЛЕ поиска видео — иначе content-visibility:auto
// обнуляет offsetWidth плеера и tryBoost его не находит.
// FIX v1.1.6: на сайтах-чатах (shouldSkipScroll) не применяем content-visibility
// и не убираем srcset — эти оптимизации ломают динамический скролл чата.
// Причина: content-visibility:auto на main>div меняет layout-высоту
// контейнера сообщений → containIntrinsicSize-заглушки (0 500px) сбивают
// scroll anchor → поле ввода «улетает» вверх экрана.
const runRenderOpts = () => {
if (shouldSkipScroll) {
log('[IHW] runRenderOpts: пропущено (AI-чат, shouldSkipScroll=true)');
return;
}
const vh = window.innerHeight;
// ── Фаза чтения: все getBoundingClientRect за один проход ─────────────
// Разделяем чтение (BCR → reflow) и запись (style) чтобы не вызывать
// "layout thrashing" (forced reflow на каждой итерации forEach).
const imgs = [...document.querySelectorAll('img[srcset]')];
const imgTops = imgs.map(img => img.getBoundingClientRect().top);
const blocks = [...document.querySelectorAll('article,section,.post,.content,.entry,main>div')];
const blockTops = blocks.map(el => el.getBoundingClientRect().top);
// ── Фаза записи: меняем DOM только после того, как все позиции прочитаны ──
// srcset у картинок вне вьюпорта — убираем, браузер не грузит тяжёлую версию впрок
imgs.forEach((img, i) => { if (imgTops[i] > vh) img.removeAttribute('srcset'); });
// content-visibility:auto на блоках ниже 2 экранов — пропускаем рендер того
// что пользователь ещё не видит; плееры исключаем чтобы не обнулить их размер
blocks.forEach((el, i) => {
if (i > 1 && blockTops[i] > vh * 2
&& !el.querySelector('video,iframe,[class*="player"],[id*="player"]')) {
el.style.cssText += ';content-visibility:auto;contain-intrinsic-size:0 500px';
}
});
};
/* ── Page: "Video Content" ──────────────────────────── */
// Цель: найти главное видео (Detect & Play: Main Video on Page)
// и запустить его как можно быстрее — это главный приоритет
if (PAGE === 'Video Content') {
initVideoPreconnect(); // preconnect к CDN для всех стратегий (Qwen+Grok+DeepSeek)
/* ── _deferFontsUntilCanplay(): canplay > шрифты ────────────────────
* Переводит шрифтовые <link> в media="print" — браузер парсит CSS,
* но НЕ качает файлы шрифтов, текст рендерится системным шрифтом.
* Освобождает HTTP/2-слоты и bandwidth для первых видео-сегментов.
* После canplay: media="all" + fetchPriority="low" (фон).
* Страховка 6с: если canplay не придёт — восстанавливаем шрифты.
* Работает на всех видеохостингах: Rutube, VK, OK, Vimeo и др. (Qwen)
─────────────────────────────────────────────────────────────────── */
function _deferFontsUntilCanplay(mainVideo) {
const fontLinks = document.querySelectorAll(
'link[href*="fonts.googleapis"], link[href*="fonts.gstatic"], ' +
'link[href*="bunny.net"], link[href*="typekit.net"], link[href*="fast.fonts.net"], ' +
'link[rel="preload"][as="font"]'
);
if (!fontLinks.length) return;
// ✅ ЛОГ 1: сколько шрифтов найдено
log(`[IHW Font] найдено ${fontLinks.length} шрифт(ов) → ждём canplay`);
const _restore = () => {
clearTimeout(_timer);
mainVideo.removeEventListener('error', _restore);
fontLinks.forEach(lnk => {
if (!lnk.dataset.ihwFontDeferred) return;
// ── ИЗМЕНЕНИЕ: сначала снимаем флаг, потом восстанавливаем ──
// Это гарантирует, что любой параллельный processNode увидит
// чистое состояние до того, как браузер начнёт грузить шрифт.
delete lnk.dataset.ihwFontDeferred;
delete lnk.dataset.ihwOrigMedia;
// Теперь восстанавливаем media — браузер начнёт загрузку шрифта
lnk.media = lnk.dataset.ihwOrigMedia || 'all';
if (lnk.rel === 'preload' && lnk.as === 'font') lnk.fetchPriority = 'low';
});
log('[IHW Video] Шрифты восстановлены (canplay/fallback)');
};
const _timer = setTimeout(_restore, 6000); // страховка: если canplay не придёт
mainVideo.addEventListener('canplay', _restore, { once: true });
mainVideo.addEventListener('error', _restore, { once: true });
fontLinks.forEach(lnk => {
lnk.dataset.ihwOrigMedia = lnk.media || 'all';
lnk.media = 'print'; // не качать, использовать системный шрифт
lnk.dataset.ihwFontDeferred = '1';
});
log('[IHW Video] Шрифты отложены до canplay (bandwidth для видео)');
}
// Заглушка внутренней аналитики YouTube (ytcsi — YouTube Client Side Instrumentation).
// Эта система собирает тайминги и метрики в фоне, отправляет данные на серверы YouTube.
// Замена на noop экономит CPU на каждой странице. (идея из PureYouTube)
// Применяем только на видеохостингах — на обычных сайтах ytcsi нет.
if (location.hostname.endsWith('youtube.com') || location.hostname.endsWith('youtu.be')) {
const noop = () => {};
window.ytcsi = { tick: noop, span: noop, info: noop, setTick: noop, lastTick: noop };
window.ytStats = noop;
log('[IHW] YouTube аналитика (ytcsi) заглушена');
// Дополнительная заглушка yt.config_: отключаем внутреннее логирование YouTube.
// ENABLE_LOGGING = false — безопасно, только блокирует отправку телеметрии.
// ADS_DATA НЕ трогаем — YouTube использует его для внутренней логики страниц
// (не только рекламы); обнуление ломает SPA-рендер вкладок «Видео»/«Трансляции».
// YouTube устанавливает yt.config_ через inline-скрипты в <head>, поэтому
// патчим после DOMContentLoaded, когда объект уже создан.
// CSS will-change:transform на #masthead-container подсказывает браузеру
// поднять шапку на отдельный GPU-слой — убирает дёргание при скролле.
const _ytBootstrap = () => {
try {
if (window.yt?.config_) {
window.yt.config_.ENABLE_LOGGING = false;
log('[IHW] YouTube yt.config_.ENABLE_LOGGING заглушен');
}
} catch (e) { log('[IHW] yt.config_ недоступен:', e); }
// CSS: masthead GPU + убираем placeholder превью + отключаем ambient blur.
// Ambient mode — тяжёлый backdrop-filter на всю страницу, часто не замечается.
// CSS containment на comments/sidebar — изменения внутри не ломают весь layout.
// Firefox: scrollbar-width:thin экономит ~15px layout + уменьшает repaint-область.
// ── Shorts + Ads: CSS-блокировка до первого paint (Qwen+Grok) ──
// ytd-* компоненты существуют ТОЛЬКО на youtube.com → безопасно на других сайтах.
// CSS graceful degradation: если YouTube поменяет теги — просто перестанет работать,
// сайт останется функциональным. Проверять раз в квартал.
let _ytCss = 'ytd-masthead,#masthead-container{will-change:transform}'
// Shorts: скрываем полки до рендера (браузер не строит layout для них)
+ 'ytd-rich-shelf-renderer[is-shorts],ytd-reel-shelf-renderer,#shorts-container{display:none!important}'
// Реклама: блокируем на уровне CSS, раньше чем uBlock/AdGuard
+ 'ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-promoted-video-renderer,#player-ads{display:none!important}'
+ 'yt-img-shadow{background-color:transparent!important}'
+ '.ytp-ambient-light,.ytp-ambient-mode-enabled,'
+ 'ytd-watch-flexy[ambient-mode-enabled] .ytp-ambient-light{display:none!important}'
+ 'ytd-watch-flexy,#cinematics{backdrop-filter:none!important}'
+ '#comments,#secondary,ytd-watch-next-secondary-results-renderer'
+ '{contain:layout style paint}';
if (isFirefox) _ytCss += 'html{scrollbar-width:thin}';
const _yt = document.createElement('style');
_yt.textContent = _ytCss;
document.head.appendChild(_yt);
log('[IHW] YouTube: masthead GPU + ambient off + CSS containment');
// EXPERIMENT_FLAGS: отключаем анимации и cinematic эффекты через
// внутренний конфиг YouTube (безопаснее CSS — работает до рендера).
// Идея: YT CPU Enhancer (greasyfork #552190).
try {
const _expf = window.yt?.config_?.EXPERIMENT_FLAGS;
if (_expf && typeof _expf === 'object') {
Object.assign(_expf, {
web_animated_actions: false,
web_animated_like: false,
kevlar_watch_cinematics: false,
web_cinematic_theater_mode: false,
web_cinematic_fullscreen: false,
enable_cinematic_blur_desktop_loading: false,
kevlar_measure_ambient_mode_idle: false,
smartimation_background: false,
});
log('[IHW] YouTube EXPERIMENT_FLAGS: ambient/cinematic отключены');
}
} catch (e) { log('[IHW] EXPERIMENT_FLAGS недоступен:', e); }
// Lazy-load comments: скрываем ytd-comments пока не доскроллили.
// contentVisibility:hidden — не рендерим совсем (сильнее чем auto).
// Идея: YouTube Performance CPU Optimized (greasyfork #548105).
const _cmts = document.querySelector('ytd-comments#comments');
if (_cmts) {
_cmts.style.contentVisibility = 'hidden';
new IntersectionObserver(entries => {
if (entries[0].isIntersecting) { _cmts.style.contentVisibility = ''; }
}, { rootMargin: '200px' }).observe(_cmts);
}
};
// Если DOM уже готов (SPA-навигация) — сразу, иначе ждём
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', _ytBootstrap, { once: true });
} else {
_ytBootstrap();
}
}
const boostMainVideo = () => {
log('[IHW] Главное видео ищется на странице...');
// --- Стратегия 1: нативный <video> ---
const videos = [...document.querySelectorAll('video')];
// Фильтруем видео: видимые по размеру + не дальше 1.5 экрана вниз
// (исключаем плееры которые рендерятся вне viewport, но технически имеют размер)
const visibleVideos = videos.filter(v =>
v.offsetWidth > 0 && v.offsetHeight > 0 &&
v.getBoundingClientRect().top < window.innerHeight * 1.5
);
if (visibleVideos.length) {
const main = visibleVideos.reduce((a, b) =>
(b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a
);
log(`[IHW] Главное видео найдено (<video>): ${main.offsetWidth}x${main.offsetHeight}px | src: ${main.src || main.currentSrc || '(загружается)'}`);
main.setAttribute('data-ihw-boosted', 'true'); // маркер главного видео (защита от EXTREME)
main.preload = 'auto';
main.fetchPriority = 'high';
initDynamicPreconnect(main);
_deferFontsUntilCanplay(main); // шрифты ждут canplay — bandwidth для видео
// ── POSTER PRELOAD: улучшает LCP — poster часто является крупнейшим элементом (Grok+Qwen)
if (main.poster) {
try {
const _pl = document.createElement('link');
_pl.rel = 'preload'; _pl.as = 'image';
_pl.href = main.poster; _pl.fetchPriority = 'high';
document.head.appendChild(_pl);
log('[IHW Video] poster preload + high priority');
} catch(e) {}
}
// ── SOURCE HIGH PRIORITY: fetchPriority=high для дочерних <source> (Qwen)
main.querySelectorAll('source').forEach(src => {
if (!src.fetchPriority) src.fetchPriority = 'high';
});
// ── HLS MANIFEST PRELOAD: предзагрузка манифеста → плеер быстрее получает первый сегмент (Grok+Qwen+DeepSeek)
// Эффект: +400–900 мс к первому сегменту на медленных соединениях
const _videoSrc = main.src || main.currentSrc || '';
if (_videoSrc.includes('.m3u8')) {
try {
const _ml = document.createElement('link');
_ml.rel = 'preload'; _ml.as = 'fetch';
_ml.fetchPriority = 'high'; _ml.href = _videoSrc;
_ml.crossOrigin = 'anonymous';
document.head.appendChild(_ml);
log('[IHW Video] HLS manifest preloaded');
} catch(e) {}
}
// iOS Safari: playsinline предотвращает принудительный fullscreen при play().
// Без атрибута iOS разворачивает видео на весь экран — нежелательно на
// сайтах с нативным плеером (VK, OK, Dzen). Безвреден на Desktop.
main.setAttribute('playsinline', '');
visibleVideos.forEach(v => { if (v !== main && v.preload === 'auto') v.preload = 'metadata'; });
if (main.readyState >= 2) {
log('[IHW] Главное видео: готово (readyState=' + main.readyState + '), запускаем');
main.play().catch(e => log('[IHW] play() заблокирован —', e.message));
_canplayMs = Math.round(performance.now());
if (DEBUG) console.log(`[IHW] Видео готово: ${_canplayMs} мс от навигации (readyState уже ≥ 2)`);
} else {
log('[IHW] Главное видео: ожидаем canplay (readyState=' + main.readyState + ')');
const _vt0 = performance.now();
main.addEventListener('canplay', () => {
log('[IHW] canplay сработал, запускаем воспроизведение');
main.play().catch(e => log('[IHW] play() заблокирован —', e.message));
// ── SEEK HACK: микро-сдвиг триггерит буферизацию следующего сегмента (Grok+DeepSeek)
// Не применяем на YouTube — нестабильно из-за частых изменений плеера
if (SEEK_HACK && main.duration > 10 && !isNaN(main.duration)) {
// Проверка, не входит ли текущий хост в список исключений
const isExcluded = SEEK_HACK_EXCLUDE.some(host => location.hostname.includes(host));
if (!isExcluded) {
const _st = main.currentTime;
main.currentTime = _st + 0.01;
setTimeout(() => { try { main.currentTime = _st; } catch(e) {} }, 50);
log('[IHW Video] seek-hack applied');
}
}
_canplayMs = Math.round(performance.now() - _vt0);
if (DEBUG) console.log(`[IHW] Видео готово за: ${_canplayMs} мс (canplay от старта поиска)`);
}, { once: true });
}
return true;
}
// --- Стратегия 2: плеер в iframe cross-origin ---
const playerIframes = [...document.querySelectorAll('iframe')].filter(fr => {
if (fr.offsetWidth < 200 || fr.offsetHeight < 100) return false;
const s = (fr.src || fr.name || fr.id || fr.className || '').toLowerCase();
return /video|player|embed|rutube|vimeo|vk|ok\.ru|dzen|yandex|twitch|dailymotion|bilibili|tiktok/i.test(s);
});
if (playerIframes.length) {
const main = playerIframes.reduce((a, b) =>
(b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a
);
log(`[IHW] Главное видео найдено (<iframe> плеер): ${main.offsetWidth}x${main.offsetHeight}px | src: ${main.src || '(нет src)'}`);
if (main.dataset.lazySrc) { main.src = main.dataset.lazySrc; delete main.dataset.lazySrc; }
main.setAttribute('data-ihw-boosted', 'true'); // атрибут найденного главного видео
main.loading = 'eager';
main.fetchPriority = 'high';
return true;
}
// --- Стратегия 3: кастомный плеер (div с data-атрибутами: VK,Rutube,OK) ---
const customPlayer = document.querySelector(
'[class*="player"],[id*="player"],[class*="Player"],[id*="Player"],' +
'[data-video],[data-player],[data-src*="video"]'
);
if (customPlayer && customPlayer.offsetWidth > 200) {
log(`[IHW] Главное видео найдено (кастомный плеер): ${customPlayer.tagName}#${customPlayer.id}.${customPlayer.className.split(' ')[0]} | ${customPlayer.offsetWidth}x${customPlayer.offsetHeight}px`);
customPlayer.setAttribute('data-ihw-boosted', 'true'); // атрибут найденного главного видео
return true;
}
if (videos.length) {
log(`[IHW] Главное видео: найдено ${videos.length} <video>, но все нулевого размера (плеер ещё не отрисован)`);
} else {
log('[IHW] Главное видео: не найдено ни <video>, ни iframe-плеера, ни кастомного плеера');
}
return false;
};
// Повторяем поиск с нарастающим интервалом — плеер в SPA может появиться через 3–8с
// Экспоненциальный backoff: 1500 → 3000 → 6000 → 8000 мс (Nemotron p.4)
// Без массива — масштабируется чище, проще сбрасывать при SPA-навигации.
// Math.min ограничивает максимальный интервал 8000 мс.
const _MAX_BOOST_ATTEMPTS = 4;
let _boostAttempt = 0;
const tryBoost = () => {
if (boostMainVideo()) return;
if (_boostAttempt >= _MAX_BOOST_ATTEMPTS) {
log('[IHW] Главное видео: все попытки исчерпаны');
return;
}
const delay = Math.min(1500 * Math.pow(2, _boostAttempt), 8000);
_boostAttempt++;
log(`[IHW] Главное видео: повтор через ${delay / 1000}с (попытка ${_boostAttempt}/${_MAX_BOOST_ATTEMPTS})`);
setTimeout(tryBoost, delay);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryBoost, { once: true });
} else {
tryBoost();
}
// Пауза при скрытии вкладки — экономит CPU/GPU/АКБ.
// Работает только для плееров с <video> в ГЛАВНОМ документе страницы:
// YouTube и Twitch — нативный <video> доступен через querySelectorAll в main DOM.
// Остальные хостинги (VK, OK, Bilibili, TikTok, Dzen и др.) держат плеер в
// cross-origin iframe — браузер запрещает доступ к чужому документу из userscript.
// PiP: document.pictureInPictureElement → видео в плавающем окне → не трогаем.
// _ihw_wasPlaying — не возобновляем видео которое пользователь сам поставил на паузу.
if (PAUSE_ON_HIDDEN) {
document.addEventListener('visibilitychange', () => {
// Ищем самый крупный видимый <video> — тот же критерий что и в boostMainVideo
const vs = [...document.querySelectorAll('video')]
.filter(v => v.offsetWidth > 0 && v.offsetHeight > 0);
if (!vs.length) return;
const main = vs.reduce((a, b) =>
b.offsetWidth * b.offsetHeight > a.offsetWidth * a.offsetHeight ? b : a
);
// PiP активен — не вмешиваемся
if (document.pictureInPictureElement) return;
if (document.hidden) {
main._ihw_wasPlaying = !main.paused;
if (main._ihw_wasPlaying) {
main.pause();
log('[IHW] visibilitychange: вкладка скрыта → пауза');
}
} else {
if (main._ihw_wasPlaying) {
// preload='auto' остался с boostMainVideo — не меняли при паузе.
// fetchPriority повторно ставим в 'high': после паузы браузер
// мог понизить приоритет сетевых запросов видеоэлемента.
main.fetchPriority = 'high';
main.play().catch(e => log('[IHW] visibilitychange: resume заблокирован —', e.message));
log('[IHW] visibilitychange: вкладка активна → воспроизведение');
}
main._ihw_wasPlaying = undefined;
}
});
}
}
/* ── Page: "Mixed Content" ──────────────────────────── */
// 1-й приоритет: текст + базовые стили (браузер делает сам)
// 2-й приоритет: картинки первого экрана — eager + fetchPriority:high
// 3-й приоритет: всё за экраном — lazy, iframe откладываем на скролл
if (PAGE === 'Mixed Content') {
// Восстанавливаем iframe при приближении (запас 300px до появления)
const lazyIframeObserver = new IntersectionObserver(entries => {
for (const e of entries) {
if (e.isIntersecting && e.target.dataset.lazySrc) {
e.target.src = e.target.dataset.lazySrc;
delete e.target.dataset.lazySrc;
lazyIframeObserver.unobserve(e.target);
}
}
}, { rootMargin: '300px' });
const initMixed = () => {
const vh = window.innerHeight;
document.querySelectorAll('img').forEach(img => {
const top = img.getBoundingClientRect().top;
if (top <= vh) {
img.loading = 'eager';
img.decoding = 'async';
img.fetchPriority = 'high';
} else {
img.loading = 'lazy';
}
});
// FIX v1.1.6: пропускаем lazy-iframe на AI-чатах.
// ChatGPT / Claude / Qwen могут использовать iframe для поля ввода
// или вспомогательных компонентов. Удаление src у таких iframe приводит к
// тому, что при программном скролле чата браузер не находит целевой элемент,
// и поле ввода "улетает" вверх страницы.
if (!shouldSkipScroll) {
// Батчинг BCR: читаем позиции до записи — нет принудительного reflow (DeepSeek)
const _iframes = [...document.querySelectorAll('iframe')];
const _iframeTops = _iframes.map(fr => fr.getBoundingClientRect().top);
_iframes.forEach((fr, i) => {
if (_iframeTops[i] > vh * 2 && fr.src) {
fr.dataset.lazySrc = fr.src;
fr.removeAttribute('src');
lazyIframeObserver.observe(fr);
}
});
} else {
log('[IHW] initMixed: lazy-iframe пропущено (AI-чат)');
}
document.querySelectorAll('video').forEach(v => {
v.autoplay = false;
v.preload = 'metadata';
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMixed, { once: true });
} else {
initMixed();
}
}
/* ── Mobile: специфичные оптимизации ───────────────── */
if (isMobile) {
// Hover-анимации — на сенсоре не нужны, потребляют CPU при скролле
const _mh = document.createElement('style');
_mh.textContent = '@media(hover:none){*{transition:none!important;animation:none!important}}'
// touch-action: manipulation — убирает 300 мс задержку клика на сенсорных экранах.
// Браузер больше не ждёт «может это двойной тап?» перед обработкой одиночного клика.
// Применяем только к интерактивным элементам — не к * — чтобы не блокировать
// свайп-жесты в слайдерах, галереях и плеерах (Grok+Qwen+DeepSeek).
// -webkit-tap-highlight-color: transparent — убирает синюю/серую вспышку при тапе.
+ 'a,button,[role="button"],input,select,textarea,label,summary'
+ '{touch-action:manipulation;-webkit-tap-highlight-color:transparent}';
(document.head || document.documentElement).appendChild(_mh);
// Жёсткий запрет autoplay — на мобиле это трафик + АКБ
document.addEventListener('play', e => {
const el = e.target;
if ((el.tagName === 'VIDEO' || el.tagName === 'AUDIO') && el.autoplay) {
el.pause();
el.autoplay = false;
}
}, { capture: true, passive: true });
// Пассивные слушатели touchstart/wheel — явная подсказка браузеру
// что scroll-обработчик не вызовет preventDefault.
// Без passive браузер ждёт завершения JS перед каждым кадром скролла.
document.addEventListener('touchstart', () => {}, { passive: true });
window.addEventListener('wheel', () => {}, { passive: true });
}
/* ── КНОПКА УПРАВЛЕНИЯ ────────────────────────────────────────────────────
* Три режима — карусель по одиночному клику/тапу (вариант Б):
* ON → OFF → ON[E] → ON → …
* Долгое удержание (600 мс) → показывает диагностику TTFB·DOM·↓·✕ на 4 с
* (не переключает режим).
* ON[E] = Extreme Mode: агрессивная экономия трафика/ресурсов.
* Цвета:
* ON = #5a9fd4 (голубой)
* OFF = #888 (серый)
* ON[E] = #7a4a1e (тёмный янтарь, отличается от всех шкал)
─────────────────────────────────────────────────────────────────────── */
function _getMode() {
const h = location.hostname;
const isOff = localStorage.getItem(SITE_KEY) === '1';
const isExt = localStorage.getItem('ihw:extreme:' + h) === '1';
const isAuto = localStorage.getItem('ihw:auto:' + h) === '1';
// Явно был задан ON (ранее снят с AUTO, но не переведён в EXT/OFF)
const isExplicitON = localStorage.getItem('ihw:on:' + h) === '1';
if (isOff) return 'OFF';
if (isExt) return 'EXT';
if (isExplicitON) return 'ON'; // пользователь явно выбрал ON
if (isAuto) return 'AUTO';
return 'AUTO'; // по-умолчанию — Авто-режим
}
function _renderBtn() {
const mode = _getMode();
const btn = document.createElement('button');
// Цвета: ON=голубой, OFF=серый, ON[E]=тёмный янтарь, ON[A]=приглушённый оливковый
const bg = mode === 'OFF' ? '#888' : mode === 'EXT' ? '#7a4a1e'
: mode === 'AUTO' ? '#3a5a3a' : '#5a9fd4';
const fg = mode === 'OFF' ? '#ddd' : '#fff';
const lbl = mode === 'OFF' ? 'OFF' : mode === 'EXT' ? 'ON[E]'
: mode === 'AUTO' ? 'ON[A]' : 'ON';
btn.style.cssText = [
'position:fixed', 'bottom:52px', 'right:12px', 'z-index:2147483647',
'font-size:11px', 'padding:3px 7px', 'border:none', 'border-radius:4px',
'cursor:pointer', 'opacity:0.5', 'transition:opacity .2s,background .2s',
'font-family:system-ui,sans-serif', 'line-height:1.4',
`background:${bg};color:${fg}`
].join(';');
btn.textContent = lbl;
btn._originalText = lbl; // сохраняем неизменяемый базовый текст
btn._metricActive = false; // флаг: показываются ли сейчас метрики (long-press)
// Hover (desktop)
btn.addEventListener('mouseenter', () => {
// Подсказки описывают СЛЕДУЮЩЕЕ состояние после клика (переход)
if (btn._metricActive) return; // не мешаем показу метрик
const _tips = {
ON: 'Вкл. ускорение Extreme? ON[E]', // ON→ON[E] (Extreme)
OFF: 'Вкл. обычное ускорение? (ON)', // OFF→ON (задание п.3)
EXT: 'Вкл. режим Авто? ON[A]', // EXT→AUTO (задание п.3)
AUTO: 'Выкл. ускорение? OFF' // AUTO→OFF (задание п.3)
};
// Не перезаписываем если сейчас показывается метрика
btn.textContent = _tips[mode] || mode;
btn.style.opacity = '0.95';
});
btn.addEventListener('mouseleave', () => {
btn.style.opacity = '0.5';
if (!btn._metricActive) {
btn.textContent = btn._originalText; // возвращаем базовый текст
}
});
// Long-press (600 мс) — показать диагностику без переключения
let _pressTimer = null;
let _longFired = false;
const _startPress = () => {
_longFired = false;
_pressTimer = setTimeout(() => {
_longFired = true;
const nav = performance.getEntriesByType('navigation')[0];
if (!nav) return;
const ttfb = Math.round(nav.responseStart - nav.requestStart);
const dom = document.getElementsByTagName('*').length;
const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;
const savedBg = btn.style.background;
const _modeDisp = mode === 'AUTO'
? `ON[A]=${_globalAutoMode === 'EXT' ? 'ON[E]' : 'ON'}`
: mode;
btn._metricActive = true;
btn.textContent = `TTFB:${ttfb} DOM:${dom>999?(dom/1000).toFixed(1)+'k':dom} ↓${kb||'кэш'}kb ✕${_blockedCount} mode:${_modeDisp}`;
btn.style.cssText += ';font-size:9px;white-space:nowrap';
setTimeout(() => {
if (btn) {
btn._metricActive = false;
btn.textContent = btn._originalText;
btn.style.background = savedBg;
btn.style.fontSize = '11px';
btn.style.whiteSpace = '';
}
}, 4000);
}, 600);
};
btn.addEventListener('pointerdown', _startPress, { passive: true });
btn.addEventListener('pointerup', () => clearTimeout(_pressTimer), { passive: true });
btn.addEventListener('pointerleave', () => clearTimeout(_pressTimer), { passive: true });
btn.addEventListener('pointercancel',() => clearTimeout(_pressTimer), { passive: true });
// Карусель режимов: ON[A] → OFF → ON → ON[E] → ON[A] → …
// Граф-кольцо из 4 состояний. Каждый клик/тап — следующий узел кольца.
btn.addEventListener('click', () => {
if (_longFired) { _longFired = false; return; }
const cur = _getMode();
const h = location.hostname;
if (cur === 'AUTO') {
// ON[A] → OFF: выключить ускорение для этого сайта
localStorage.setItem(SITE_KEY, '1');
localStorage.removeItem('ihw:extreme:' + h);
localStorage.removeItem('ihw:auto:' + h);
localStorage.removeItem('ihw:on:' + h);
} else if (cur === 'OFF') {
// OFF → ON: явное обычное ускорение
localStorage.removeItem(SITE_KEY);
localStorage.removeItem('ihw:extreme:' + h);
localStorage.removeItem('ihw:auto:' + h);
localStorage.setItem('ihw:on:' + h, '1'); // явный ON (не AUTO)
} else if (cur === 'ON') {
// ON → ON[E]: включить Extreme
localStorage.removeItem(SITE_KEY);
localStorage.setItem('ihw:extreme:' + h, '1');
localStorage.removeItem('ihw:auto:' + h);
localStorage.removeItem('ihw:on:' + h);
} else {
// ON[E] → ON[A]: вернуться в Авто-режим (сбросить все локальные предпочтения)
localStorage.removeItem('ihw:extreme:' + h);
localStorage.removeItem(SITE_KEY);
localStorage.removeItem('ihw:on:' + h);
localStorage.setItem('ihw:auto:' + h, '1');
}
location.reload();
});
_btn = btn;
document.documentElement.appendChild(btn);
}
// Кнопка управления — всегда показываем независимо от режима
_renderBtn();
/* ── DNS PREFETCH ДЛЯ ВТОРОГО ЭКРАНА (Mixed Content) ── */
// Sentinel на top:2200px — когда пользователь долистывает до границы второго
// экрана, на idle собираем внешние домены и добавляем dns-prefetch.
// Трекеры фильтруются через isTracker() — в prefetch не попадают.
// sendBeacon трекеров уже заблокирован выше — дополнительная фильтрация не нужна.
let dnsPrefetchDone = false;
const addDnsPrefetch = domains => {
if (dnsPrefetchDone || !domains.length) return;
// Отсеиваем трекеры — им dns-prefetch не нужен, мы их блокируем
const clean = domains.filter(d => !isTracker('https://' + d));
if (!clean.length) {
log('[IHW] DNS prefetch: все найденные домены — трекеры, пропускаем');
return;
}
log(`[IHW] DNS prefetch: резолвим ${clean.length} доменов → ${clean.join(', ')}`);
const head = document.head || document.documentElement;
clean.forEach(d => {
const lnk = document.createElement('link');
lnk.rel = 'dns-prefetch';
lnk.href = '//' + d;
head.appendChild(lnk);
});
dnsPrefetchDone = true;
};
const createSecondScreenSentinel = () => {
if (dnsPrefetchDone) return;
if (document.querySelector('[data-ihw-sentinel]')) return;
// Если sentinel уже существует в DOM, не создаём второй
if (document.querySelector('.ihw-sentinel')) return;
const sentinel = document.createElement('div');
sentinel.className = 'ihw-sentinel'; // уникальный класс
sentinel.style.cssText = 'position:absolute;top:2200px;left:0;width:1px;height:1px;pointer-events:none;visibility:hidden';
document.documentElement.appendChild(sentinel);
log('[IHW] Sentinel создан для второго экрана');
const obs = new IntersectionObserver(entries => {
if (!entries[0].isIntersecting || dnsPrefetchDone) return;
obs.disconnect();
sentinel.remove();
log('[IHW] Второй экран подгружен — пользователь долистал до его границы');
// Сбор доменов на idle — не тормозим скролл
(window.requestIdleCallback || setTimeout).bind(window)(() => {
const externalDomains = new Set();
document.querySelectorAll('a[href^="http"], img[src^="http"], iframe[src^="http"]')
.forEach(el => {
try {
// location.origin как base — страховка для относительных путей
const h = new URL(el.href || el.src, location.origin).hostname;
// endsWith точнее чем includes — не отсеет notexample.com
if (h && !h.endsWith(location.hostname)) externalDomains.add(h);
} catch {}
});
const list = [...externalDomains].slice(0, 10);
if (list.length) {
addDnsPrefetch(list);
} else {
log('[IHW] DNS prefetch: внешних доменов не найдено');
}
}, { timeout: 1000 });
}, { rootMargin: '500px 0px' });
obs.observe(sentinel);
};
/* ── ФИНАЛИЗАЦИЯ ────────────────────────────────────── */
const onLoadHandler = () => {
if (_initDone) return; // защита от двойного запуска
_initDone = true;
// runRenderOpts только для Mixed Content — на видеохостингах приоритет только видео,
// а content-visibility и srcset-оптимизации там не нужны (нет статейных блоков).
// requestIdleCallback ждёт реального простоя браузера; setTimeout — fallback (Safari < 18).
if (PAGE === 'Mixed Content') {
// runRenderOpts() содержит внутреннюю проверку shouldSkipScroll
(window.requestIdleCallback || setTimeout).bind(window)(runRenderOpts, window.requestIdleCallback ? { timeout: 500 } : 500);
// DNS prefetch sentinel — только не на чатах (там нет "второго экрана" в классическом понимании)
if (!shouldSkipScroll) {
setTimeout(createSecondScreenSentinel, 1200);
}
}
setTimeout(() => mo.disconnect(), 4000);
// <noscript> — не исполняется в JS-окружении, занимает DOM-память и время парсинга.
// Эффект минимальный (5–10 узлов DOM), безопасно удалять. (Qwen+ChatGPT)
document.querySelectorAll('noscript').forEach(n => n.remove());
// ── EXTREME MODE: пост-load оптимизации ──────────────────────────────
if (EXTREME_MODE) {
// Глобальное удаление srcset (lazy images добавились после processNode)
document.querySelectorAll('img[srcset]').forEach(img => img.removeAttribute('srcset'));
log('[IHW Extreme] Post-load: srcset удалён (догонка)');
// disablePictureInPicture: некоторые сайты форсируют PiP при скролле → лишний GPU/CPU (Qwen+DeepSeek)
document.querySelectorAll('video:not([data-ihw-boosted])').forEach(v => {
try { v.disablePictureInPicture = true; } catch(e) {}
});
log('[IHW Extreme] Post-load: srcset удалён, PiP отключён у не-главных видео');
}
// ── DEBUG: полные метрики после load ─────────────────────────────────
// Вызывается строго после load-события — loadEventEnd здесь всегда > 0.
// console.group/log используется намеренно для структурированного вывода в DevTools.
// При DEBUG=false блок не выполняется вообще — нулевая нагрузка.
if (DEBUG) {
_logExtendedMetrics(_getModeLabel());
}
};
// Надёжный запуск — три варианта на случай разного поведения Tampermonkey
if (document.readyState === 'complete') {
onLoadHandler();
} else if (document.readyState === 'interactive') {
onLoadHandler();
} else {
window.addEventListener('load', onLoadHandler, { once: true });
}
// Fallback: 7с достаточно для любой страницы
setTimeout(() => {
if (PAGE === 'Mixed Content' && !dnsPrefetchDone && !shouldSkipScroll) {
createSecondScreenSentinel();
}
}, 7000);
})();