Carousel Image Preloader

Intelligently preload images in carousels for faster navigation

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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.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
  });

})();