// ==UserScript== // @name vichan CPU Saver // @namespace vichan-perf-tools // @version 1.0 // @description Reduce CPU/resource usage on vichan-based imageboards: throttles polling/timers, kills mousemove-heavy image-hover previews, lazy-loads thumbnails, freezes animated GIFs until clicked, and strips CSS animations. // @author you // @match https://yourboard.example/* // @match https://yourboard.example/*/* // @run-at document-start // @grant none // ==/UserScript== /* SETUP ----- 1. Edit the @match lines above to your actual board's domain(s), e.g.: // @match https://myboard.net/* You can add as many @match lines as you need for different boards. (Left broad with a placeholder domain on purpose — this script patches window.setInterval, which you generally don't want running on every random site you visit.) 2. The script also runs a runtime check (isVichanPage) as a second safety net, so if a @match domain isn't actually vichan, most of the heavier logic just no-ops. 3. Toggle features in CONFIG below. */ (function () { 'use strict'; const CONFIG = { minIntervalMs: 15000, // clamp any setInterval faster than this (auto-update polling, etc.) pauseWhenHidden: true, // skip interval callbacks while the tab isn't visible killImageHover: true, // throttle mousemove so floating hover-image previews stop hogging CPU lazyLoadImages: true, // only decode thumbnails once they scroll into view disableAnimations: true, // strip CSS transitions/animations/backdrop-filter freezeGifs: true, // render animated GIFs as a static frame until clicked }; // ---- Detection: bail out fast on non-vichan pages ---- function isVichanPage() { return !!( document.querySelector('#board_navigation') || document.querySelector('.postarea') || document.querySelector('script[src*="inline-expanding"]') || document.querySelector('script[src*="expand.js"]') || document.querySelector('body.is_index, body.is_thread, body.is_catalog') || document.querySelector('.thread .post') || document.querySelector('meta[name="generator"][content*="vichan" i]') ); } // ---- 1. Throttle setInterval-based polling (auto-updater, clocks, etc.) ---- // Patched immediately at document-start, before any page script runs. const nativeSetInterval = window.setInterval; function patchTimers() { window.setInterval = function (fn, delay, ...args) { const clamped = Math.max(delay || 0, CONFIG.minIntervalMs); const wrapped = function () { if (CONFIG.pauseWhenHidden && document.hidden) return; fn.apply(this, args); }; return nativeSetInterval(wrapped, clamped); }; } function unpatchTimers() { window.setInterval = nativeSetInterval; } // ---- 2. Throttle mousemove globally (kills floating image-hover-preview CPU drain) ---- function throttleMousemove() { let last = 0; document.addEventListener( 'mousemove', function (e) { const now = performance.now(); if (now - last < 50) { // ~20 events/sec instead of 100+/sec e.stopImmediatePropagation(); } last = now; }, true ); } // ---- 3. Lazy-load thumbnails/images ---- function lazyLoadImages() { if (!('IntersectionObserver' in window)) return; const imgs = document.querySelectorAll('.post img[src], .thread img[src]'); const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const img = entry.target; if (img.dataset.lazySrc) img.src = img.dataset.lazySrc; img.removeAttribute('data-lazy-src'); io.unobserve(img); }); }, { rootMargin: '200px' } ); imgs.forEach((img) => { if (img.dataset.lazyDone) return; img.dataset.lazyDone = '1'; img.dataset.lazySrc = img.src; img.removeAttribute('src'); // stop immediate decode io.observe(img); }); } // ---- 4. Freeze animated GIFs to a static frame until clicked ---- function freezeGif(img) { return new Promise((resolve) => { const draw = () => { try { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; canvas.getContext('2d').drawImage(img, 0, 0); resolve(canvas.toDataURL()); } catch (e) { resolve(null); // cross-origin canvas taint — skip freezing this one } }; if (img.complete && img.naturalWidth) draw(); else img.addEventListener('load', draw, { once: true }); }); } function freezeGifs() { document.querySelectorAll('img[src$=".gif"]').forEach((img) => { if (img.dataset.gifGuard) return; img.dataset.gifGuard = '1'; const originalSrc = img.dataset.lazySrc || img.src; if (!originalSrc) return; freezeGif(img).then((frozenSrc) => { if (!frozenSrc) return; img.src = frozenSrc; img.style.cursor = 'pointer'; img.title = (img.title ? img.title + ' — ' : '') + 'Click to play'; img.addEventListener( 'click', function playOnce() { img.src = originalSrc; }, { once: true } ); }); }); } // ---- 5. Strip CSS animation/transition overhead ---- function disableAnimations() { const style = document.createElement('style'); style.textContent = ` *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; backdrop-filter: none !important; } `; document.head.appendChild(style); } // ---- Wire it up ---- patchTimers(); // cheap, safe to run before we know if this is vichan document.addEventListener('DOMContentLoaded', () => { if (!isVichanPage()) { unpatchTimers(); return; } if (CONFIG.killImageHover) throttleMousemove(); if (CONFIG.disableAnimations) disableAnimations(); const runDomWork = () => { if (CONFIG.lazyLoadImages) lazyLoadImages(); if (CONFIG.freezeGifs) freezeGifs(); }; runDomWork(); // Re-run for posts injected by the thread auto-updater const observer = new MutationObserver(runDomWork); observer.observe(document.body, { childList: true, subtree: true }); }); })();