I Hate Waiting

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);

})();