YouTube Live CPU Tamer

It reduces the high CPU usage on Super Chats with nothing to lose.

  1. // ==UserScript==
  2. // @name YouTube Live CPU Tamer
  3. // @name:ja YouTube Live CPU Tamer
  4. // @name:zh-CN YouTube Live CPU Tamer
  5. // @description It reduces the high CPU usage on Super Chats with nothing to lose.
  6. // @description:ja スーパーチャットによる高いCPU使用率を削減します。見た目は何も変わりません。
  7. // @description:zh-CN 降低超级聊天的高CPU利用率。外观完全没有变化。
  8. // @namespace knoa.jp
  9. // @include https://www.youtube.com/live_chat*
  10. // @include https://www.youtube.com/live_chat_replay*
  11. // @version 2.0.6
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function(){
  16. const SCRIPTID = 'YouTubeLiveCpuTamer';
  17. const SCRIPTNAME = 'YouTube Live CPU Tamer';
  18. const DEBUG = false;/*
  19. [update] 2.0.6
  20. No updates on code. Just confirmed to work.
  21.  
  22. [bug]
  23.  
  24. [todo]
  25.  
  26. [possible]
  27.  
  28. [research]
  29. Proxyを使うとbackgroundトリック不要?CPU使用に対する効果はある?
  30. 放送開始前の待機画面でもHelper(GPU)が食ってる件
  31. リアルタイム視聴時のほうがHelper(GPU)が消費する件は新着確認js処理のせいか
  32.  
  33. [memo]
  34. none:80+30=110 => tame:50+20=70 => remove:30+15=45
  35. */
  36. if(console.time) console.time(SCRIPTID);
  37. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  38. const THROTTLE = 1000*MS;
  39. const site = {
  40. targets: {
  41. itemsNode: () => $('yt-live-chat-ticker-renderer #items'),
  42. },
  43. get: {
  44. tickerItemInsideContainers: (items) => items.querySelectorAll('.yt-live-chat-ticker-renderer[role="button"] #container'),/* existing items */
  45. tickerItemInsideContainer: (node) => node.querySelector('.yt-live-chat-ticker-renderer[role="button"] #container'),/* for observer */
  46. },
  47. };
  48. let elements = {};
  49. const core = {
  50. initialize: function(){
  51. elements.html = document.documentElement;
  52. elements.html.classList.add(SCRIPTID);
  53. text.setup(texts, top.document.documentElement.lang);
  54. core.ready();
  55. core.addStyle('style');
  56. },
  57. ready: function(){
  58. core.getTargets(site.targets).then(() => {
  59. log("I'm ready.");
  60. core.observeTickerItems();
  61. core.prepareRemoveTickersButton();
  62. });
  63. },
  64. observeTickerItems: function(){
  65. let containers = site.get.tickerItemInsideContainers(elements.itemsNode);
  66. Array.from(containers).forEach(container => {
  67. core.observeTickerItemInsideContainer(container);
  68. });
  69. observe(elements.itemsNode, function(records){
  70. records.forEach(r => r.addedNodes.forEach(node => {
  71. let container = site.get.tickerItemInsideContainer(node);
  72. if(container) core.observeTickerItemInsideContainer(container);
  73. }));
  74. });
  75. },
  76. observeTickerItemInsideContainer: function(container){
  77. container.parentNode.style.background = container.style.background;
  78. let lastUpdated = Date.now();
  79. observe(container, function(records){
  80. let now = Date.now();
  81. if(now - lastUpdated < THROTTLE) return;
  82. lastUpdated = now;
  83. container.parentNode.style.background = container.style.background;
  84. }, {attributes: true, attributeFilter: ['style']});
  85. },
  86. prepareRemoveTickersButton: function(){
  87. let button = createElement(html.removeTickersButton());
  88. button.addEventListener('click', function(e){
  89. elements.itemsNode.parentNode.removeChild(elements.itemsNode);
  90. });
  91. elements.itemsNode.parentNode.appendChild(button);
  92. },
  93. getTarget: function(selector, retry = 10){
  94. const key = selector.name;
  95. const get = function(resolve, reject, retry){
  96. let selected = selector();
  97. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  98. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  99. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, 1000, resolve, reject, retry);
  100. else return reject(selector);
  101. elements[key] = selected;
  102. resolve(selected);
  103. };
  104. return new Promise(function(resolve, reject){
  105. get(resolve, reject, retry);
  106. }).catch(selector => {
  107. log(`Not found: ${key}, I give up.`);
  108. });
  109. },
  110. getTargets: function(selectors, retry = 10){
  111. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry)));
  112. },
  113. addStyle: function(name = 'style'){
  114. if(html[name] === undefined) return;
  115. let style = createElement(html[name]());
  116. document.head.appendChild(style);
  117. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  118. elements[name] = style;
  119. },
  120. };
  121. const texts = {
  122. 'remove tickers by ${SCRIPTNAME}': {
  123. en: () => `remove tickers by ${SCRIPTNAME}`,
  124. ja: () => `履歴欄を削除 by ${SCRIPTNAME}`,
  125. zh: () => `删除历史记录栏 by ${SCRIPTNAME}`,
  126. },
  127. };
  128. const html = {
  129. removeTickersButton: () => `<button id="${SCRIPTID}-removeTickers" title="${text('remove tickers by ${SCRIPTNAME}')}">╳</button>`,
  130. style: () => `
  131. <style type="text/css">
  132. yt-live-chat-ticker-renderer #items > *{
  133. border-radius: 999px;
  134. }
  135. yt-live-chat-ticker-renderer #items > * > #container{
  136. background: none !important;
  137. }
  138. yt-live-chat-ticker-renderer #${SCRIPTID}-removeTickers{
  139. cursor: pointer;
  140. position: absolute;
  141. top: 50%;
  142. left: 5px;
  143. transform: translateY(-50%);
  144. border-radius: 100vmax;
  145. border: none;
  146. background: white;
  147. filter: drop-shadow(0px 0px 1px rgba(0,0,0,.25));
  148. height: 20px;
  149. width: 20px;
  150. padding: 0 !important;
  151. opacity: 0;
  152. transition: opacity 250ms;
  153. pointer-events: none;
  154. }
  155. yt-live-chat-ticker-renderer:hover #left-arrow-container[hidden] ~ #${SCRIPTID}-removeTickers{
  156. opacity: 1;
  157. pointer-events: auto;
  158. }
  159. yt-live-chat-ticker-renderer #items > *{
  160. transition: transform 250ms;
  161. }
  162. yt-live-chat-ticker-renderer:hover #items > *{
  163. transform: translateX(5px);
  164. }
  165. </style>
  166. `,
  167. };
  168. const text = function(key, ...args){
  169. if(text.texts[key] === undefined){
  170. log('Not found text key:', key);
  171. return key;
  172. }else return text.texts[key](args);
  173. };
  174. text.setup = function(texts, language){
  175. let languages = [...window.navigator.languages];
  176. if(language) languages.unshift(...String(language).split('-').map((p,i,a) => a.slice(0,1+i).join('-')).reverse());
  177. if(!languages.includes('en')) languages.push('en');
  178. languages = languages.map(l => l.toLowerCase());
  179. Object.keys(texts).forEach(key => {
  180. Object.keys(texts[key]).forEach(l => texts[key][l.toLowerCase()] = texts[key][l]);
  181. texts[key] = texts[key][languages.find(l => texts[key][l] !== undefined)] || (() => key);
  182. });
  183. text.texts = texts;
  184. };
  185. const $ = function(s, f){
  186. let target = document.querySelector(s);
  187. if(target === null) return null;
  188. return f ? f(target) : target;
  189. };
  190. const $$ = function(s, f){
  191. let targets = document.querySelectorAll(s);
  192. return f ? Array.from(targets).map(t => f(t)) : targets;
  193. };
  194. const createElement = function(html = '<span></span>'){
  195. let outer = document.createElement('div');
  196. outer.innerHTML = html;
  197. return outer.firstElementChild;
  198. };
  199. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  200. let observer = new MutationObserver(callback.bind(element));
  201. observer.observe(element, options);
  202. return observer;
  203. };
  204. const log = function(){
  205. if(!DEBUG) return;
  206. let l = log.last = log.now || new Date(), n = log.now = new Date();
  207. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  208. //console.log(error.stack);
  209. console.log(
  210. SCRIPTID + ':',
  211. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  212. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  213. /* :00 */ ':' + line,
  214. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  215. /* caller */ (callers[1] || '') + '()',
  216. ...arguments
  217. );
  218. };
  219. log.formats = [{
  220. name: 'Firefox Scratchpad',
  221. detector: /MARKER@Scratchpad/,
  222. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  223. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  224. }, {
  225. name: 'Firefox Console',
  226. detector: /MARKER@debugger/,
  227. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  228. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  229. }, {
  230. name: 'Firefox Greasemonkey 3',
  231. detector: /\/gm_scripts\//,
  232. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  233. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  234. }, {
  235. name: 'Firefox Greasemonkey 4+',
  236. detector: /MARKER@user-script:/,
  237. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  238. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  239. }, {
  240. name: 'Firefox Tampermonkey',
  241. detector: /MARKER@moz-extension:/,
  242. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  243. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  244. }, {
  245. name: 'Chrome Console',
  246. detector: /at MARKER \(<anonymous>/,
  247. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  248. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  249. }, {
  250. name: 'Chrome Tampermonkey',
  251. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  252. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 5,
  253. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  254. }, {
  255. name: 'Chrome Extension',
  256. detector: /at MARKER \(chrome-extension:/,
  257. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  258. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  259. }, {
  260. name: 'Edge Console',
  261. detector: /at MARKER \(eval/,
  262. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  263. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  264. }, {
  265. name: 'Edge Tampermonkey',
  266. detector: /at MARKER \(Function/,
  267. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  268. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  269. }, {
  270. name: 'Safari',
  271. detector: /^MARKER$/m,
  272. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  273. getCallers: (e) => e.stack.split('\n'),
  274. }, {
  275. name: 'Default',
  276. detector: /./,
  277. getLine: (e) => 0,
  278. getCallers: (e) => [],
  279. }];
  280. log.format = log.formats.find(function MARKER(f){
  281. if(!f.detector.test(new Error().stack)) return false;
  282. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  283. return true;
  284. });
  285. core.initialize();
  286. if(console.timeEnd) console.timeEnd(SCRIPTID);
  287. })();