// ==UserScript== // @name YouTube CPU Tamer – Hybrid Edition // @name:ja YouTube CPU負荷軽減スクリプト – ハイブリッド方式 // @name:en YouTube CPU Tamer – Hybrid Edition // @name:zh-CN YouTube CPU减负脚本 – 混合策略 // @name:zh-TW YouTube CPU負載減輕工具 – 混合策略 // @name:ko YouTube CPU 부하 감소 스크립트 – 하이브리드 방식 // @name:fr Réducteur de charge CPU YouTube – Édition Hybride // @name:es Reductor de carga de CPU para YouTube – Edición Híbrida // @name:de YouTube CPU-Last-Reduzierer – Hybrid-Edition // @name:pt-BR Redutor de uso da CPU no YouTube – Edição Híbrida // @name:ru Снижение нагрузки на CPU в YouTube – Гибридная версия // @version 5.0.0 // @description Reduces YouTube CPU usage by intelligently throttling timers and animation frames, while preserving critical player functions to help avoid freezes and infinite loading. // @description:ja タイマーとアニメーションフレームを賢く間引いて YouTube の CPU 負荷を低減。プレイヤーの重要機能は保護し、フリーズや無限読み込みの発生を抑制します。 // @description:en Reduces YouTube CPU usage by intelligently throttling timers and animation frames, while preserving critical player functions to help avoid freezes and infinite loading. // @description:zh-CN 通过智能节流计时器和动画帧,降低 YouTube 的 CPU 占用,同时保护关键播放器功能,帮助避免卡死和无限加载。 // @description:zh-TW 透過智慧節流計時器與動畫幀,降低 YouTube 的 CPU 使用,同時保護關鍵播放器功能,協助避免當機與無限載入。 // @description:ko 타이머와 애니메이션 프레임을 지능적으로 간소화해 YouTube의 CPU 사용을 낮추고, 중요한 플레이어 기능을 보존하여 프리징·무한 로딩을 방지하는 데 도움을 줍니다. // @description:fr Réduit l’utilisation CPU de YouTube en régulant intelligemment les temporisateurs et les images d’animation, tout en préservant les fonctions critiques du lecteur pour aider à éviter les blocages et les chargements infinis. // @description:es Reduce el uso de CPU en YouTube al regular inteligentemente temporizadores y fotogramas de animación, preservando funciones críticas del reproductor para ayudar a evitar congelamientos y cargas infinitas. // @description:de Senkt die CPU-Auslastung auf YouTube durch intelligentes Drosseln von Timern und Animations-Frames, wobei kritische Player-Funktionen erhalten bleiben und Freezes sowie endloses Laden vermieden werden können. // @description:pt-BR Reduz o uso de CPU no YouTube ao limitar inteligentemente temporizadores e quadros de animação, preservando funções críticas do player para ajudar a evitar travamentos e carregamentos infinitos. // @description:ru Снижает нагрузку CPU на YouTube за счёт интеллектуального ограничения таймеров и кадров анимации, сохраняя критически важные функции плеера и помогая избегать зависаний и бесконечной загрузки. // @namespace https://github.com/koyasi777/youtube-cpu-tamer-hybrid // @author koyasi777 // @match https://www.youtube.com/* // @match https://www.youtube.com/embed/* // @match https://www.youtube-nocookie.com/embed/* // @match https://music.youtube.com/* // @run-at document-start // @grant none // @inject-into page // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @homepageURL https://github.com/koyasi777/youtube-cpu-tamer-hybrid // @supportURL https://github.com/koyasi777/youtube-cpu-tamer-hybrid/issues // @downloadURL none // ==/UserScript== (() => { "use strict"; const FLAG = "__yt_cpu_tamer_hybrid_running__"; if (window[FLAG]) return; window[FLAG] = true; // ========= Tunables ========= const THROTTLE_WHEN_HIDDEN = false; const PATCH_INTERVALS = false; const ENABLE_THROTTLE_EVENTS = true; const ENABLE_MUTATION_BATCH = true; const ENABLE_LIGHT_CSS = true; const ENABLE_RAF_DECIMATOR = true; // ← Idle時だけ有効化するのでON const ADAPTIVE_MIN_DELAY_THRESHOLD = true; // Idle Boost thresholds const QUIET_MS = 6000; // 入力無しでIdleへ const IDLE_MIN_DELAY_FLOOR = 220; // Idle時の setTimeout 閾値の下限 const INTERACTIVE_MIN_DELAY_BASE = 150; // 通常時のベース(適応で上下) // rAF const RAF_VISIBLE_FPS_IDLE = 24; const RAF_HIDDEN_FPS_IDLE = 5; const PLAYER_READY_SELECTOR = "ytd-player,#movie_player,video.video-stream,ytmusic-player-bar"; const REPATCH_TIMEOUT = 10000; const CSS_TOGGLE_ATTR = "data-yt-cpu-tamer-cv-off"; // content-visibility一時解除用 const IDLE_ATTR = "data-yt-cpu-tamer-idle"; const DEBUG = false; const dlog = (...a)=>{ if (DEBUG) console.debug("[YouTube CPU Tamer]", ...a); }; // ========= Global knobs (dynamic) ========= let baseMinDelay = INTERACTIVE_MIN_DELAY_BASE; // Adaptive が更新 let MIN_DELAY_THRESHOLD = baseMinDelay; // 実際に使われる値(Idleで上げる) let MO_FLUSH_MS = 50; // Idleで80msに let useDecimator = false; // Idleでtrue const getMOFlushMs = () => MO_FLUSH_MS; // ========= Utilities ========= const ORIG_RAF = window.requestAnimationFrame.bind(window); const ORIG_CAF = window.cancelAnimationFrame.bind(window); const isGlobalTarget = (t)=> t===window||t===document||t===document.documentElement||t===document.body; const nextAnimationFrame = ()=> new Promise(r=>requestAnimationFrame(r)); const waitForDocReady = async()=>{ while(!document.documentElement||!document.head){ await nextAnimationFrame(); } }; // ========= Event throttler (stability edition) ========= if (ENABLE_THROTTLE_EVENTS) { try { const ORIG_ADD = EventTarget.prototype.addEventListener; const ORIG_REMOVE = EventTarget.prototype.removeEventListener; const wrapMap = new WeakMap(); const RAF_EVENTS = new Set(["mousemove","pointermove","touchmove"]); const THROTTLED = new Map([["scroll",50],["wheel",50],["resize",100]]); const isPlayerCritical = (t)=>{ if (t instanceof HTMLVideoElement) return true; if (typeof t.closest === "function") { if (t.closest(".ytp-chrome-bottom,.ytp-volume-panel,.ytp-progress-bar,.ytp-ad-progress,.ytp-settings-menu")) return true; } return false; }; const rafThrottle = (fn, ctx)=>{ let scheduled=false, lastArgs=null; return function(...args){ lastArgs=args; if(!scheduled){ scheduled=true; requestAnimationFrame(()=>{ scheduled=false; try{fn.apply(ctx,lastArgs);}catch(e){console.error(e);} }); } }; }; const timerThrottle = (fn, ctx, delay)=>{ let busy=false, lastArgs=null; return function(...args){ lastArgs=args; if(!busy){ busy=true; setTimeout(()=>{ busy=false; try{fn.apply(ctx,lastArgs);}catch(e){console.error(e);} }, delay); } }; }; const leadingTrailing = (fn, ctx, delay)=>{ let leadingDone=false, tid=null, lastArgs=null; return function(...args){ lastArgs=args; if(!leadingDone){ leadingDone=true; try{fn.apply(ctx,args);}catch(e){console.error(e);} } clearTimeout(tid); tid=setTimeout(()=>{ leadingDone=false; try{fn.apply(ctx,lastArgs);}catch(e){console.error(e);} }, delay); }; }; EventTarget.prototype.addEventListener = function(type, listener, options){ if (typeof listener!=="function") return ORIG_ADD.call(this,type,listener,options); if (isPlayerCritical(this)) return ORIG_ADD.call(this,type,listener,options); if (isGlobalTarget(this) && (type==="wheel"||type==="scroll"||type==="resize")) return ORIG_ADD.call(this,type,listener,options); let wrapped = listener; if (RAF_EVENTS.has(type)) wrapped = rafThrottle(listener,this); else if (THROTTLED.has(type)) { if (type==="resize") { if (!isGlobalTarget(this)) wrapped = leadingTrailing(listener,this,THROTTLED.get(type)); } else if (type==="wheel"||type==="scroll") { wrapped = timerThrottle(listener,this,THROTTLED.get(type)); } } if (wrapped!==listener) wrapMap.set(listener,wrapped); return ORIG_ADD.call(this,type,wrapped,options); }; EventTarget.prototype.removeEventListener = function(type, listener, options){ const wrapped = wrapMap.get(listener)||listener; return ORIG_REMOVE.call(this,type,wrapped,options); }; dlog("Event throttler installed."); } catch(e){ console.error("[YouTube CPU Tamer] Event throttler failed:", e); } } // ========= MutationObserver batcher (dynamic flush) ========= if (ENABLE_MUTATION_BATCH) { try { const NativeMO = window.MutationObserver; window.MutationObserver = class extends NativeMO { constructor(cb){ let queue=[], scheduled=false, lastObserver=null; const flush=()=>{ scheduled=false; const records=queue; queue=[]; try{cb(records,lastObserver);}catch(e){console.error(e);} }; const proxy=(records,obs)=>{ lastObserver=obs; queue.push(...records); if(!scheduled){ scheduled=true; setTimeout(flush, getMOFlushMs()); } }; super(proxy); } }; dlog("MO batcher installed."); } catch(e){ console.error("[YouTube CPU Tamer] MO batcher failed:", e); } } // ========= CSS reductions (idle-aware) ========= if (ENABLE_LIGHT_CSS) { try { const styleId="yt-cpu-tamer-css"; if(!document.getElementById(styleId)){ const style=document.createElement("style"); style.id=styleId; style.textContent = ` /* 常時アニメ(スケルトン/継続ローディング)の抑制 */ .ytd-ghost-grid-renderer *, .ytd-continuation-item-renderer * { animation: none !important; } /* 画面外の大領域は可視時のみ描画(一時解除は [${CSS_TOGGLE_ATTR}]) */ html:not([${CSS_TOGGLE_ATTR}]) #comments, html:not([${CSS_TOGGLE_ATTR}]) #related, html:not([${CSS_TOGGLE_ATTR}]) ytd-watch-next-secondary-results-renderer { content-visibility: auto !important; contain-intrinsic-size: 800px 600px !important; } /* スムーススクロールは無効(安定) */ html { scroll-behavior: auto !important; } /* === Idle Boost 中だけ追加の軽量化(操作復帰で即解除) === */ html[${IDLE_ATTR}] ytd-thumbnail *, html[${IDLE_ATTR}] .ytp-storyboard, html[${IDLE_ATTR}] .ytd-reel-shelf-renderer * { animation: none !important; transition-property: none !important; } `; (document.head||document.documentElement).appendChild(style); } } catch(e){ console.error("[YouTube CPU Tamer] CSS reductions failed:", e); } } // ========= rAF decimator (Idle時のみ動作) ========= if (ENABLE_RAF_DECIMATOR) { try { const DECIM_ID_BASE = 1e9; let seq = 1; const queued = new Map(); // id -> cb let ticking=false, nextDue=performance.now(); const budget = ()=> (document.visibilityState==="visible" ? 1000/RAF_VISIBLE_FPS_IDLE : 1000/RAF_HIDDEN_FPS_IDLE); const loop = ()=>{ if (!useDecimator) { ticking=false; return; } // 停止 const now = performance.now(); if (now >= nextDue) { nextDue = now + budget(); if (queued.size) { ORIG_RAF((ts)=>{ const cbs = Array.from(queued.values()); queued.clear(); for (const cb of cbs) { try{ cb(ts); } catch(e){ console.error(e);} } }); } } ORIG_RAF(loop); }; window.requestAnimationFrame = (cb)=>{ if (!useDecimator) return ORIG_RAF(cb); const id = DECIM_ID_BASE + (seq++); queued.set(id, cb); if (!ticking) { ticking=true; nextDue=performance.now(); ORIG_RAF(loop); } return id; }; window.cancelAnimationFrame = (id)=>{ if (typeof id==="number" && id>=DECIM_ID_BASE) queued.delete(id); else ORIG_CAF(id); }; document.addEventListener("visibilitychange", ()=>{ nextDue = performance.now(); }); dlog("rAF decimator ready (idle-controlled)."); } catch(e){ console.error("[YouTube CPU Tamer] rAF decimator failed:", e); } } // ========= Adaptive timer threshold (updates baseMinDelay) ========= if (ADAPTIVE_MIN_DELAY_THRESHOLD) { try { let busy = 0; if (window.PerformanceObserver) { const po = new PerformanceObserver((list)=>{ for (const e of list.getEntries()) busy += e.duration; }); try { po.observe({ entryTypes: ["longtask"] }); } catch {} } setInterval(()=>{ const slice = busy; busy=0; const ratio = Math.max(0, Math.min(1, slice/1000)); baseMinDelay = Math.round(80 + (200-80) * ratio); // Idleかどうかで実値を更新 if (!document.documentElement.hasAttribute(IDLE_ATTR)) { MIN_DELAY_THRESHOLD = baseMinDelay; } else { MIN_DELAY_THRESHOLD = Math.max(baseMinDelay, IDLE_MIN_DELAY_FLOOR); } dlog("Adaptive baseMinDelay=", baseMinDelay, " MIN_DELAY_THRESHOLD=", MIN_DELAY_THRESHOLD); }, 1000); } catch(e){ console.error("[YouTube CPU Tamer] Adaptive threshold failed:", e); } } // ========= Layout kick (restore/minimize/BFCache) ========= const kickLayout = ()=>{ try{ document.documentElement.setAttribute(CSS_TOGGLE_ATTR,"1"); void document.documentElement.offsetHeight; const fire=()=> window.dispatchEvent(new Event("resize")); fire(); setTimeout(fire,50); requestAnimationFrame(fire); requestAnimationFrame(()=> document.documentElement.removeAttribute(CSS_TOGGLE_ATTR)); }catch(_){} }; // ========= Idle detector ========= let lastActive = performance.now(); const markActive = ()=>{ lastActive = performance.now(); if (document.documentElement.hasAttribute(IDLE_ATTR)) exitIdle(); }; ["mousemove","mousedown","keydown","wheel","touchstart","pointerdown","focusin"].forEach(t=>{ window.addEventListener(t, markActive, {capture:true, passive:true}); }); const isPlaying = ()=>{ const v = document.querySelector("video.video-stream"); return v && !v.paused && !v.ended; }; const enterIdle = ()=>{ if (document.documentElement.hasAttribute(IDLE_ATTR)) return; document.documentElement.setAttribute(IDLE_ATTR,"1"); useDecimator = true; MO_FLUSH_MS = 80; MIN_DELAY_THRESHOLD = Math.max(baseMinDelay, IDLE_MIN_DELAY_FLOOR); dlog("Idle Boost ON"); }; const exitIdle = ()=>{ if (!document.documentElement.hasAttribute(IDLE_ATTR)) return; document.documentElement.removeAttribute(IDLE_ATTR); useDecimator = false; MO_FLUSH_MS = 50; MIN_DELAY_THRESHOLD = baseMinDelay; dlog("Idle Boost OFF"); }; setInterval(()=>{ const now = performance.now(); if (document.visibilityState==="visible" && isPlaying() && (now - lastActive) >= QUIET_MS) enterIdle(); else exitIdle(); }, 1000); // ========= Core: hybrid timer patching ========= const PromiseExt = (()=>{ let _r,_j; const shim=(r,j)=>{_r=r;_j=j;}; return class extends Promise{ constructor(cb=shim){ super(cb); if(cb===shim){ this.resolve=_r; this.reject=_j; } } }; })(); const setup = async ()=>{ await waitForDocReady(); const mainTimers = { setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), setInterval: window.setInterval.bind(window), clearInterval: window.clearInterval.bind(window), }; // Clean timers via sandboxed iframe const FRAME_ID="yt-cpu-tamer-timer-frame"; let frame=document.getElementById(FRAME_ID); if(frame && (!frame.contentWindow||!frame.contentWindow.setTimeout)){ frame.remove(); frame=null; } if(!frame){ frame=document.createElement("iframe"); frame.id=FRAME_ID; frame.style.display="none"; frame.sandbox="allow-same-origin allow-scripts"; const html="yt-cpu-tamer-timer-provider"; if (window.trustedTypes && window.trustedTypes.createPolicy) { try { const pol=trustedTypes.createPolicy("yt-cpu-tamer-policy",{createHTML:(s)=>s}); frame.srcdoc=pol.createHTML(html); } catch{ frame.srcdoc=html; } } else frame.srcdoc=html; document.documentElement.appendChild(frame); } while(!frame.contentWindow||!frame.contentWindow.setTimeout){ await nextAnimationFrame(); } const nativeTimers = { setTimeout: frame.contentWindow.setTimeout.bind(frame.contentWindow), setInterval: frame.contentWindow.setInterval.bind(frame.contentWindow), clearTimeout: frame.contentWindow.clearTimeout.bind(frame.contentWindow), clearInterval: frame.contentWindow.clearInterval.bind(frame.contentWindow), }; const DUMMY_ID="yt-cpu-tamer-trigger-node"; let dummy=document.getElementById(DUMMY_ID); if(!dummy){ dummy=document.createElement("div"); dummy.id=DUMMY_ID; dummy.style.display="none"; document.documentElement.appendChild(dummy); } let timersAreThrottled = document.visibilityState==="visible"; const makeHybridTrigger = ()=>{ if (document.visibilityState==="visible" || THROTTLE_WHEN_HIDDEN) { return (cb)=>{ const p=new PromiseExt(); requestAnimationFrame(p.resolve); return p.then(cb); }; } else { return (cb)=>{ const p=new PromiseExt(); const mo=new MutationObserver(()=>{ mo.disconnect(); p.resolve(); }); mo.observe(dummy,{attributes:true}); dummy.setAttribute("data-yt-cpu-tamer-trigger", Math.random().toString(36)); return p.then(cb); }; } }; let currentTrigger = makeHybridTrigger(); const VC_FLAG="__yt_cpu_tamer_visibility_listener__"; if(!window[VC_FLAG]){ document.addEventListener("visibilitychange", ()=>{ timersAreThrottled = document.visibilityState==="visible"; currentTrigger = makeHybridTrigger(); if (document.visibilityState==="visible") kickLayout(); dlog("Visibility:", document.visibilityState, " timers=", timersAreThrottled); }); window[VC_FLAG]=true; } const activeTimeouts = new Set(); const mirrorToString=(patched,native)=>{ try{ patched.toString = native.toString.bind(native); }catch{} }; const makeTimeoutPatcher = (cleanTimeout, pool)=>function patchedSetTimeout(cb, delay=0, ...args){ const isFn = (typeof cb==="function"); const runInMain = isFn ? ()=>cb.apply(window,args) : ()=>{ try{ (0,eval)(String(cb)); }catch(e){ console.error("[YT Tamer] eval error:",e);} }; if (!timersAreThrottled || delay < MIN_DELAY_THRESHOLD) return mainTimers.setTimeout(runInMain, delay); let id = cleanTimeout(()=>{ if(pool.has(id)) pool.delete(id); currentTrigger(runInMain); }, delay); pool.add(id); return id; }; const makeClearTimeout = (pool)=>(id)=>{ if(pool.has(id)){ pool.delete(id); nativeTimers.clearTimeout(id);} else { mainTimers.clearTimeout(id);} }; const makeIntervalPatcher = (cleanInterval)=>function patchedSetInterval(cb, delay=0, ...args){ if (!PATCH_INTERVALS || typeof cb!=="function" || delaycb.apply(window,args), delay); return cleanInterval(()=>{ currentTrigger(()=>cb.apply(window,args)); }, delay); }; const installPatches = ()=>{ window.setTimeout = makeTimeoutPatcher(nativeTimers.setTimeout, activeTimeouts); window.clearTimeout = makeClearTimeout(activeTimeouts); window.setInterval = PATCH_INTERVALS ? makeIntervalPatcher(nativeTimers.setInterval) : mainTimers.setInterval; window.clearInterval = PATCH_INTERVALS ? nativeTimers.clearInterval : mainTimers.clearInterval; mirrorToString(window.setTimeout, mainTimers.setTimeout); mirrorToString(window.clearTimeout, mainTimers.clearTimeout); mirrorToString(window.setInterval, mainTimers.setInterval); mirrorToString(window.clearInterval, mainTimers.clearInterval); dlog("Timer patches installed."); }; const uninstallPatches = ()=>{ window.setTimeout=mainTimers.setTimeout; window.clearTimeout=mainTimers.clearTimeout; window.setInterval=mainTimers.setInterval; window.clearInterval=mainTimers.clearInterval; dlog("Timer patches uninstalled."); }; installPatches(); window.addEventListener("yt-navigate-start", ()=>{ try{ uninstallPatches(); }catch{} }); let navigationHandler=null; window.addEventListener("yt-navigate-finish", ()=>{ if (navigationHandler){ navigationHandler.abort(); } navigationHandler = (function(){ let aborted=false, observer=null, tid=null; const cleanup=()=>{ if(observer) observer.disconnect(); if(tid) nativeTimers.clearTimeout(tid); observer=null; tid=null; navigationHandler=null; }; const handleRepatch=(reason)=>{ if (aborted) return; dlog(reason, "-> re-install timer patches"); installPatches(); timersAreThrottled = document.visibilityState==="visible"; kickLayout(); cleanup(); }; dlog("navigate-finish: temporary native timers during rebuild"); uninstallPatches(); const tryRepatch=()=>{ if (document.querySelector(PLAYER_READY_SELECTOR)) handleRepatch("Player detected"); }; observer = new MutationObserver(tryRepatch); if (document.body) observer.observe(document.body,{childList:true,subtree:true}); tid = nativeTimers.setTimeout(()=>handleRepatch("Repatch timeout"), REPATCH_TIMEOUT); if (document.querySelector(PLAYER_READY_SELECTOR)) nativeTimers.setTimeout(()=>handleRepatch("Player already exists"),0); return { abort: ()=>{ if (aborted) return; aborted=true; cleanup(); } }; })(); }); window.addEventListener("pageshow",(e)=>{ if (e.persisted) { dlog("pageshow BFCache -> layout kick"); kickLayout(); } }); }; setup().catch(err=>console.error("[YouTube CPU Tamer] Setup error:", err)); })();