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

})();