Intelligently preload images in carousels for faster navigation
// ==UserScript==
// @name Carousel Image Preloader
// @namespace Q2Fyb3VzZWwgSW1hZ2UgUHJlbG9hZGVy
// @version 1.0
// @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 https://*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const DEBUG = false; // Set to true to enable console logs
const PRELOAD_BUFFER = 3; // Number of images to preload ahead/behind
const PRELOAD_DELAY = 500; // ms delay before preloading adjacent images
function log(msg) {
if (DEBUG) console.log('[Carousel Preloader]', msg);
}
// Detect if element is likely part of a carousel
function isCarouselContainer(el) {
const classes = el.className.toLowerCase();
const id = el.id.toLowerCase();
// Common carousel indicators
const carouselPatterns = [
'carousel', 'slider', 'swiper', 'glide', 'slick',
'splide', 'owl', 'gallery', 'slideshow', 'lightbox'
];
return carouselPatterns.some(pattern =>
classes.includes(pattern) || id.includes(pattern)
);
}
// Find all images in a container
function getCarouselImages(container) {
const images = Array.from(container.querySelectorAll('img'));
const bgImages = Array.from(container.querySelectorAll('[style*="background-image"]'));
return [...images, ...bgImages].filter(el => {
// Filter out tiny images (likely icons, buttons)
if (el.tagName === 'IMG') {
return el.naturalWidth === 0 || el.naturalWidth > 50;
}
return true;
});
}
// Get the currently visible image index
function getVisibleImageIndex(container, images) {
for (let i = 0; i < images.length; i++) {
const img = images[i];
const style = window.getComputedStyle(img);
// Check various visibility conditions
if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
// For parent containers with transform/positioning
const parent = img.parentElement;
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.display !== 'none') {
return i;
}
}
}
return 0;
}
// Preload image by creating new Image object
function preloadImage(img) {
if (img.tagName === 'IMG') {
const src = img.src || img.dataset.src;
if (src && !src.includes('data:')) {
const newImg = new Image();
newImg.src = src;
log(`Preloading image: ${src}`);
}
} else if (img.style.backgroundImage) {
const bgImage = img.style.backgroundImage.match(/url\(['"]?([^'")]+)['"]?\)/);
if (bgImage && bgImage[1]) {
const newImg = new Image();
newImg.src = bgImage[1];
log(`Preloading background image: ${bgImage[1]}`);
}
}
}
// Preload adjacent images in carousel
function preloadAdjacentImages(container, 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]);
}
log(`Preloaded images ${start} to ${end-1} (current: ${currentIndex})`);
}
// Monitor carousel for navigation and preload accordingly
function monitorCarousel(container) {
const images = getCarouselImages(container);
if (images.length < 2) return; // Not a real carousel
log(`Found carousel with ${images.length} images`);
let lastIndex = getVisibleImageIndex(container, images);
preloadAdjacentImages(container, images, lastIndex);
// Listen for clicks on navigation buttons
const navButtons = container.querySelectorAll('[class*="arrow"], [class*="next"], [class*="prev"], [class*="button"]');
navButtons.forEach(btn => {
btn.addEventListener('click', () => {
setTimeout(() => {
const currentIndex = getVisibleImageIndex(container, images);
if (currentIndex !== lastIndex) {
log(`Navigation detected: ${lastIndex} -> ${currentIndex}`);
lastIndex = currentIndex;
preloadAdjacentImages(container, images, currentIndex);
}
}, PRELOAD_DELAY);
});
});
// Monitor for swipe/touch events (for mobile)
let touchStartX = 0;
container.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
}, false);
container.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (Math.abs(touchEndX - touchStartX) > 50) { // Minimum swipe distance
setTimeout(() => {
const currentIndex = getVisibleImageIndex(container, images);
if (currentIndex !== lastIndex) {
log(`Swipe detected: ${lastIndex} -> ${currentIndex}`);
lastIndex = currentIndex;
preloadAdjacentImages(container, images, currentIndex);
}
}, PRELOAD_DELAY);
}
}, false);
// Monitor keyboard navigation (arrow keys)
const handleKeydown = (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
setTimeout(() => {
const currentIndex = getVisibleImageIndex(container, images);
if (currentIndex !== lastIndex) {
log(`Keyboard navigation: ${lastIndex} -> ${currentIndex}`);
lastIndex = currentIndex;
preloadAdjacentImages(container, images, currentIndex);
}
}, PRELOAD_DELAY);
}
};
document.addEventListener('keydown', handleKeydown);
// Monitor DOM mutations for dynamically revealed images
const observer = new MutationObserver(() => {
const currentIndex = getVisibleImageIndex(container, images);
if (currentIndex !== lastIndex) {
lastIndex = currentIndex;
preloadAdjacentImages(container, images, currentIndex);
}
});
observer.observe(container, {
attributes: true,
attributeFilter: ['style', 'class'],
subtree: true
});
}
// Scan page for carousels
function scanForCarousels() {
const allContainers = document.querySelectorAll('div, section, article, figure');
const carousels = [];
allContainers.forEach(container => {
if (isCarouselContainer(container)) {
// Check if it's not a child of another carousel
const isChild = Array.from(carousels).some(carousel =>
carousel.contains(container)
);
if (!isChild && getCarouselImages(container).length >= 2) {
carousels.push(container);
monitorCarousel(container);
}
}
});
log(`Detected and monitoring ${carousels.length} carousels`);
}
// Run on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scanForCarousels);
} else {
scanForCarousels();
}
// Also scan after a delay to catch dynamically loaded content
setTimeout(scanForCarousels, 2000);
// Rescan periodically for lazy-loaded pages
setInterval(scanForCarousels, 5000);
})();