Ускоряет загрузку страниц: на видеохостингах — приоритет главному видео, на остальных — приоритет видимому контенту.
// ==UserScript==
// @name I Hate Waiting
// @name:en I Hate Waiting
// @namespace https://tampermonkey.net/
// @version 1.1.5
// @license MIT
// @description Ускоряет загрузку страниц: на видеохостингах — приоритет главному видео, на остальных — приоритет видимому контенту.
// @description:en TSpeeds 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 + twicks other programmers
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
/* ── ЗАЩИТА ОТ ФРЕЙМОВ ──────────────────────────────── */
// Скрипт работает только на основной странице.
// Плюсы: нет дублирования логов, не трогаем DOM внутри iframe-плееров,
// меньше MutationObserver-ов в памяти. Видео ищем через DOM основной
// страницы — boostMainVideo не теряет доступ к iframe-плеерам.
if (window.top !== window) return;
/* ── ОТЛАДКА ────────────────────────────────────────── */
// true — все сообщения видны в консоли F12 (режим разработки)
// false — лог отключён полностью (режим релиза, нет затрат на вывод)
const DEBUG = false;
const log = (...args) => { if (DEBUG) console.log(...args); };
// Флаг защиты от двойного запуска onLoadHandler —
// при readyState=interactive вызываем сразу, но событие load всё равно придёт
let _initDone = false;
/* ── ИСКЛЮЧЕНИЯ САЙТОВ ──────────────────────────────── */
const SITE_KEY = 'ihw:off:' + location.hostname;
if (localStorage.getItem(SITE_KEY) === '1') {
_renderBtn(true);
return;
}
/* ── ОПРЕДЕЛЕНИЕ УСТРОЙСТВА ─────────────────────────── */
// 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';
/* ── ОПРЕДЕЛЕНИЕ ТИПА СТРАНИЦЫ ──────────────────────── */
// 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', 'mail.ru',
'bilibili.com', 'tiktok.com', 'odysee.com',
'dzen.ru', 'yandex.ru', 'ya.ru', 'smotret.tv', 'platform.rambler.ru'
];
const isVideoHost = VIDEO_HOSTS.some(h => location.hostname.endsWith(h));
const PAGE = isVideoHost ? 'Video Content' : 'Mixed Content';
log(`[IHW] Mode:${MODE} | Page:${PAGE}`);
/* ── ТРЕКЕРЫ И ИСКЛЮЧЕНИЯ ───────────────────────────── */
// Домены трекеров — блокируем sendBeacon, удаляем из DOM, исключаем из dns-prefetch
const TRACKERS = [
'google-analytics.com', 'googletagmanager.com', 'doubleclick.net',
'facebook.com', 'connect.facebook.net', 'clarity.ms',
'hotjar.com', 'mc.yandex.ru', 'top-fwz1.mail.ru',
// 'stat.livejournal.net', // счётчик LiveJournal
'redirect.appmetrica.yandex.com', // аналитика Яндекс AppMetrica
'counter.yadro.ru' // счётчик Liveinternet/Yadro
];
// Домены-исключения — НЕ удаляем их элементы из 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);
// Базовый CSS: мгновенный скролл + принудительная видимость контента.
// scroll-behavior:auto — отменяет smooth scroll сайта, скролл реагирует 1-в-1.
// visibility/opacity — некоторые сайты прячут body до загрузки рекламы/попапов,
// это принудительно раскрывает страницу сразу. (идея из PureRender)
// ---------- CSS: принудительная видимость и опционально отключение плавного скролла ----------
// Список сайтов (доменов), где НЕ нужно отключать плавный скролл
const noScrollBehaviorSites = [
'chat.deepseek.com',
'chatgpt.com',
'grok.com',
'claude.ai',
'claude.site'
];
const currentHost = window.location.hostname;
const shouldSkipScroll = noScrollBehaviorSites.some(site => currentHost.includes(site));
// Базовый стиль: принудительная видимость (идея из PureRender)
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(); return; }
// prefetch-ссылки — удаляем (не тратим ресурсы впрок),
// кроме исключений (Cloudflare Challenge использует prefetch для своей проверки)
if (tag === 'LINK' && node.rel === 'prefetch' && !isException(src)) { node.remove(); return; }
// Внешние шрифты — откладываем на idle, не блокируем рендер текста
if (tag === 'LINK' && /fonts\.g(oogle|static)apis\.com/.test(src)) {
node.media = 'print';
setTimeout(() => { if (node.parentNode) node.media = 'all'; }, 2000); // setTimeout гарантирует задержку 2с (requestIdleCallback игнорирует числовой аргумент)
return;
}
// Изображения — ленивая загрузка + async decoding
if (tag === 'IMG') {
if (!node.loading) node.loading = 'lazy';
node.decoding = 'async';
}
};
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 его не находит
const runRenderOpts = () => {
const vh = window.innerHeight;
// srcset у картинок вне вьюпорта — убираем, браузер не грузит тяжёлую версию впрок
document.querySelectorAll('img[srcset]').forEach(img => {
if (img.getBoundingClientRect().top > vh) img.removeAttribute('srcset');
});
// content-visibility:auto на блоках ниже 2 экранов — пропускаем рендер того
// что пользователь ещё не видит; плееры исключаем чтобы не обнулить их размер
document.querySelectorAll('article,section,.post,.content,.entry,main>div')
.forEach((el, i) => {
if (i > 1 && el.getBoundingClientRect().top > vh * 2
&& !el.querySelector('video,iframe,[class*="player"],[id*="player"]')) {
el.style.contentVisibility = 'auto';
el.style.containIntrinsicSize = '0 500px';
}
});
};
/* ── Page: "Video Content" ──────────────────────────── */
// Цель: найти главное видео (Detect & Play: Main Video on Page)
// и запустить его как можно быстрее — это главный приоритет
if (PAGE === 'Video Content') {
// Заглушка внутренней аналитики 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) заглушена');
}
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.preload = 'auto';
main.fetchPriority = 'high';
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));
} else {
log('[IHW] Главное видео: ожидаем canplay (readyState=' + main.readyState + ')');
main.addEventListener('canplay', () => {
log('[IHW] canplay сработал, запускаем воспроизведение');
main.play().catch(e => log('[IHW] play() заблокирован —', e.message));
}, { once: true });
}
return true;
}
// --- Стратегия 2: плеер в <iframe> (Rutube, Vimeo, VK и др.) ---
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.loading = 'eager';
main.fetchPriority = 'high';
return true;
}
// --- Стратегия 3: кастомный плеер (div с data-атрибутами) ---
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`);
return true;
}
if (videos.length) {
log(`[IHW] Главное видео: найдено ${videos.length} <video>, но все нулевого размера (плеер ещё не отрисован)`);
} else {
log('[IHW] Главное видео: не найдено ни <video>, ни iframe-плеера, ни кастомного плеера');
}
return false;
};
// Повторяем поиск с нарастающим интервалом — плеер в SPA может появиться через 3–8с
const RETRY_DELAYS = [1500, 3000, 5000, 8000];
let retryIdx = 0;
const tryBoost = () => {
if (boostMainVideo()) return;
if (retryIdx >= RETRY_DELAYS.length) {
log('[IHW] Главное видео: все попытки исчерпаны');
return;
}
const delay = RETRY_DELAYS[retryIdx++];
log(`[IHW] Главное видео: повтор через ${delay / 1000}с (попытка ${retryIdx}/${RETRY_DELAYS.length})`);
setTimeout(tryBoost, delay);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryBoost, { once: true });
} else {
tryBoost();
}
}
/* ── 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';
}
});
document.querySelectorAll('iframe').forEach(fr => {
if (fr.getBoundingClientRect().top > vh * 2 && fr.src) {
fr.dataset.lazySrc = fr.src;
fr.removeAttribute('src');
lazyIframeObserver.observe(fr);
}
});
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}}';
(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;
}
}, true);
}
/* ── КНОПКА УПРАВЛЕНИЯ ──────────────────────────────── */
function _renderBtn(isOff) {
const btn = document.createElement('button');
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',
isOff ? 'background:#888;color:#ddd' : 'background:#5a9fd4;color:#fff'
].join(';');
btn.textContent = isOff ? 'OFF' : 'ON';
btn.addEventListener('mouseenter', () => {
btn.textContent = isOff ? 'Включить ускорение на этом сайте?' : 'Отключить ускорение на этом сайте?';
btn.style.opacity = '0.95';
});
btn.addEventListener('mouseleave', () => {
btn.textContent = isOff ? 'OFF' : 'ON';
btn.style.opacity = '0.5';
});
btn.addEventListener('click', () => {
isOff ? localStorage.removeItem(SITE_KEY) : localStorage.setItem(SITE_KEY, '1');
location.reload();
});
document.documentElement.appendChild(btn);
}
_renderBtn(false);
/* ── 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;
const sentinel = document.createElement('div');
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 — не тормозим скролл (предложение Qwen)
(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-оптимизации там не нужны (нет статейных блоков)
if (PAGE === 'Mixed Content') {
(window.requestIdleCallback || setTimeout).bind(window)(runRenderOpts, 500);
setTimeout(createSecondScreenSentinel, 1200);
}
setTimeout(() => mo.disconnect(), 4000);
};
// Надёжный запуск — три варианта на случай разного поведения 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) {
createSecondScreenSentinel();
}
}, 7000);
})();