// ==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
// ==/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="<!doctype html><title>yt-cpu-tamer-timer-provider</title>";
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" || delay<MIN_DELAY_THRESHOLD || !timersAreThrottled)
return mainTimers.setInterval(()=>cb.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));
})();