Hi!
I’ve refactored the script to reduce idle CPU usage and added a configurable frame-rate:
Replaced the previous double loop (setTimeout + requestAnimationFrame) with an event-driven requestAnimationFrame loop that runs only while the video element is playing and stops on pause/ended or when the tab is hidden.
Added options.TARGET_FPS (default 15 is smooth in most case). If set to 0, the progress bar updates every animation frame; otherwise it throttles to the specified FPS.
Cached chapter widths to avoid recalculating offsetWidth each frame.
Skip updates while controls are visible (.ytp-autohide absent) so YouTube’s own code handles them.
No functional regressions observed; CPU usage (single core) drops from > 80 % to ~20 % in my tests.
(function() {
"use strict";
var style = document.createElement('style');
var to = { createHTML: s => s },
tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to,
html = s => tp.createHTML(s)
style.type = 'text/css';
style.innerHTML = html(
'.ytp-autohide .ytp-chrome-bottom{opacity:1!important;display:block!important}' +
'.ytp-autohide .ytp-chrome-bottom .ytp-progress-bar-container{bottom:-1px!important}' +
'.ytp-autohide .ytp-chrome-bottom .ytp-chrome-controls{opacity:0!important}' +
'.ytp-progress-bar-container:not(:hover) .ytp-scrubber-container{display:none!important}'
);
document.getElementsByTagName('head')[0].appendChild(style);
var permanentProgressBar = {
options: {
// To change bar update rate, go to line 161.
UPDATE_VIDEO_TIMER: true,
UPDATE_PROGRESSBAR: true,
UPDATE_BUFFERBAR: true,
TARGET_FPS: 15, // frame per second. Set 0 to update on each rAF.
},
// cache chapter metrics
chapterCache: {len:0,widths:[],totalWidth:0,ratio:1},
// Converts current video time to a human-readable format.
prettifyVideoTime: function(video) {
let seconds = "" + Math.floor(video.currentTime % 60);
let minutes = "" + Math.floor((video.currentTime % 3600) / 60);
let hours = "" + Math.floor(video.currentTime / 3600);
if (video.currentTime / 60 > 60) {
return `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`;
} else {
return `${minutes}:${seconds.padStart(2, '0')}`;
}
},
// For progress mode, return current time; for buffer mode, return the end of the buffered range that contains currentTime.
getDuration: function(video, type) {
if (type === "PROGRESSBAR") {
return video.currentTime;
} else if (type === "BUFFERBAR") {
if (video.buffered.length > 0) {
for (let i = 0; i < video.buffered.length; i++) {
if (video.currentTime >= video.buffered.start(i) && video.currentTime <= video.buffered.end(i)) {
return video.buffered.end(i);
}
}
}
return 0;
}
},
// Updates the current time display on the player.
updateCurrentTimeField: function(player) {
const video = player.querySelector("video");
const currentTimeEl = player.querySelector(".ytp-time-current");
if (!video || !currentTimeEl) return;
currentTimeEl.innerText = permanentProgressBar.prettifyVideoTime(video);
},
// For non-chaptered videos, update the progress and buffer bars directly.
updateOverallProgressBar: function(player) {
const video = player.querySelector("video");
const progressBar = player.querySelector(".ytp-play-progress");
const bufferBar = player.querySelector(".ytp-load-progress");
if (!video || !progressBar || !bufferBar) return;
if (!video.duration) return;
let progressRatio = video.currentTime / video.duration;
let bufferRatio = this.getDuration(video, "BUFFERBAR") / video.duration;
progressBar.style.transform = `scaleX(${Math.min(1, progressRatio.toFixed(5))})`;
bufferBar.style.transform = `scaleX(${Math.min(1, bufferRatio.toFixed(5))})`;
},
// For chaptered videos, update each chapter element directly based on current time.
updateProgressBarWithChapters: function(player, type) {
const video = player.querySelector("video");
if (!video || isNaN(video.duration)) return;
// Get the chapter elements and corresponding progress elements.
const chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
let chapterProgressEls;
if (type === "PROGRESSBAR") {
chapterProgressEls = player.getElementsByClassName("ytp-play-progress");
} else if (type === "BUFFERBAR") {
chapterProgressEls = player.getElementsByClassName("ytp-load-progress");
}
if (!chapterElements || !chapterProgressEls) return;
// Compute chapter metrics only if cache invalid
if (this.chapterCache.len !== chapterElements.length || !this.chapterCache.totalWidth) {
this.chapterCache.len = chapterElements.length;
this.chapterCache.widths = Array.from(chapterElements).map(el => el.offsetWidth);
this.chapterCache.totalWidth = this.chapterCache.widths.reduce((a,b)=>a+b,0);
this.chapterCache.ratio = video.duration / this.chapterCache.totalWidth;
}
const widths = this.chapterCache.widths;
const durationWidthRatio = this.chapterCache.ratio;
let accumulatedWidth = 0;
for (let i = 0; i < chapterElements.length; i++) {
const chapterWidth = widths[i];
const chapterEndTime = durationWidthRatio * (accumulatedWidth + chapterWidth);
const chapterStartTime = durationWidthRatio * accumulatedWidth;
let currentTimeForType = this.getDuration(video, type);
let ratio;
if (currentTimeForType >= chapterEndTime) {
ratio = 1;
} else if (currentTimeForType < chapterStartTime) {
ratio = 0;
} else {
ratio = (currentTimeForType - chapterStartTime) / (chapterEndTime - chapterStartTime);
}
chapterProgressEls[i].style.transform = `scaleX(${Math.min(1, ratio.toFixed(5))})`;
accumulatedWidth += chapterWidth;
}
},
// The main update function which selects chapter-mode or overall mode.
update: function() {
const player = document.querySelector(".html5-video-player");
if (!player) return;
// Skip updates while controls are visible
if (!player.classList.contains('ytp-autohide')) return;
if (this.options.UPDATE_VIDEO_TIMER) {
this.updateCurrentTimeField(player);
}
// If chapter elements exist, update chapter-mode; otherwise use overall mode.
let chapterElements = player.getElementsByClassName("ytp-progress-bar-padding");
if (chapterElements.length > 0) {
if (this.options.UPDATE_PROGRESSBAR) {
this.updateProgressBarWithChapters(player, "PROGRESSBAR");
}
if (this.options.UPDATE_BUFFERBAR) {
this.updateProgressBarWithChapters(player, "BUFFERBAR");
}
} else {
this.updateOverallProgressBar(player);
}
},
/* ------------------ Performance-friendly update loop ------------------ */
rafId: 0,
/* Starts a requestAnimationFrame loop only while the <video> is playing. */
attachLoop: function(video) {
let last = 0;
const loop = (ts) => {
if (video.paused || document.hidden) {
this.rafId = requestAnimationFrame(loop);
return;
}
if (!this.options.TARGET_FPS || ts - last >= 1000 / this.options.TARGET_FPS) {
this.update();
last = ts;
}
this.rafId = requestAnimationFrame(loop);
};
this.rafId = requestAnimationFrame(loop);
},
/* Locate the player & video element and wire play/pause events. */
waitForVideo: function() {
const tryAttach = () => {
const player = document.querySelector('.html5-video-player');
const video = player?.querySelector('video');
if (!video) {
setTimeout(tryAttach, 1000);
return;
}
const startLoop = () => {
cancelAnimationFrame(this.rafId);
this.attachLoop(video);
};
const stopLoop = () => cancelAnimationFrame(this.rafId);
video.addEventListener('play', startLoop);
video.addEventListener('playing',startLoop);
video.addEventListener('pause', stopLoop);
video.addEventListener('ended', stopLoop);
// Auto-start if video already playing
if (!video.paused) startLoop();
};
tryAttach();
},
start: function() {
this.waitForVideo();
}
};
permanentProgressBar.start();
})();
Hi! I’ve refactored the script to reduce idle CPU usage and added a configurable frame-rate: