Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();