Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);

})();