Intelligently preload images in carousels for faster navigation
// ==UserScript==
// @name Carousel Image Preloader
// @namespace Q2Fyb3VzZWwgSW1hZ2UgUHJlbG9hZGVy
// @version 1.1
// @description Intelligently preload images in carousels for faster navigation
// @author smed79
// @license GPLv3
// @icon https://i25.servimg.com/u/f25/11/94/21/24/imgloa10.png
// @match *://*/*
// @run-at document-end
// @grant none
// ==/UserScript==
(function() {
'use strict';
const PRELOAD_BUFFER = 5;
const PRELOAD_DELAY = 150; // Reduced delay for snappier preloading
// Track what we've already done to save CPU and Network
const processedCarousels = new WeakSet();
const preloadedUrls = new Set();
const carouselPatterns = /carousel|slider|swiper|glide|slick|splide|owl|gallery|slideshow|lightbox/i;
function isCarouselContainer(el) {
if (!el.className && !el.id) return false;
return carouselPatterns.test(el.className) || carouselPatterns.test(el.id);
}
function getCarouselImages(container) {
const images = Array.from(container.querySelectorAll('img'));
const bgImages = Array.from(container.querySelectorAll('[style*="background-image"]'));
return [...images, ...bgImages];
}
// Lightweight heuristic for visibility instead of getComputedStyle
function getVisibleImageIndex(images) {
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
for (let i = 0; i < images.length; i++) {
const rect = images[i].getBoundingClientRect();
// If it has width/height and is within the horizontal viewport bounds
if (rect.width > 0 && rect.right > 0 && rect.left < viewportWidth) {
return i;
}
}
return 0; // Default to first if none found
}
function preloadImage(img) {
let src = '';
if (img.tagName === 'IMG') {
src = img.dataset.src || img.src;
// Override native lazy loading so it actually loads when we want it to
if (img.hasAttribute('loading')) {
img.setAttribute('loading', 'eager');
}
} else if (img.style && img.style.backgroundImage) {
const match = img.style.backgroundImage.match(/url\(['"]?([^'")]+)['"]?\)/);
if (match) src = match[1];
}
if (src && !src.includes('data:') && !preloadedUrls.has(src)) {
preloadedUrls.add(src);
// Tell the browser to prioritize this image
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = src;
document.head.appendChild(link);
// Memory fetch fallback
const newImg = new Image();
newImg.src = src;
}
}
function preloadAdjacentImages(images, currentIndex) {
const start = Math.max(0, currentIndex - PRELOAD_BUFFER);
const end = Math.min(images.length, currentIndex + PRELOAD_BUFFER + 1);
for (let i = start; i < end; i++) {
preloadImage(images[i]);
}
}
function monitorCarousel(container) {
if (processedCarousels.has(container)) return;
const images = getCarouselImages(container);
if (images.length < 2) return;
processedCarousels.add(container);
let lastIndex = getVisibleImageIndex(images);
preloadAdjacentImages(images, lastIndex);
// Efficient event delegation - only trigger preload after interaction ends
let debounceTimer;
const handleInteraction = () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const currentIndex = getVisibleImageIndex(images);
if (currentIndex !== lastIndex) {
lastIndex = currentIndex;
preloadAdjacentImages(images, currentIndex);
}
}, PRELOAD_DELAY);
};
// Click & Touch events (Passive for better scroll performance)
container.addEventListener('click', handleInteraction, { passive: true });
container.addEventListener('touchend', handleInteraction, { passive: true });
// Keyboard events
container.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') handleInteraction();
}, { passive: true });
}
function scanForCarousels() {
// Only search common wrapper elements to save CPU
const containers = document.querySelectorAll('div[class], section[class], div[id], section[id]');
for (const container of containers) {
if (isCarouselContainer(container)) {
monitorCarousel(container);
}
}
}
// Initial Scan
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scanForCarousels);
} else {
scanForCarousels();
}
// Efficient observer for dynamically loaded websites (Replaces the setInterval)
let domTimeout;
const observer = new MutationObserver((mutations) => {
const hasNewNodes = mutations.some(m => m.addedNodes.length > 0);
if (hasNewNodes) {
clearTimeout(domTimeout);
domTimeout = setTimeout(scanForCarousels, 1000); // Debounce massive DOM changes
}
});
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true
});
})();