chunk-008-lazyload.js

I’ll build this as a safe, advanced lazy-load utility chunk for RX Theme: images, iframes, videos, backgrounds, scripts, styles, prefetch, idle tasks, and fallback support. It will be standalone so you can place it directly in assets/static-js/static-chunks-js/chunk-008-lazyload.js.

My dear friend, create this file:

rx-theme/assets/static-js/static-chunks-js/chunk-008-lazyload.js

Then paste this full advanced lazy-load code inside it.

/*!
 * RX Theme - Advanced Lazy Load Chunk
 * File: assets/static-js/static-chunks-js/chunk-008-lazyload.js
 * Purpose:
 * - Lazy load images, iframes, videos, background images
 * - Lazy load scripts and styles
 * - Support responsive images: srcset, sizes, picture/source
 * - Support native loading="lazy"
 * - Support IntersectionObserver
 * - Fallback for old browsers
 * - Idle loading, priority hints, prefetch/preload helper
 * - MutationObserver for Ajax/dynamic content
 *
 * Author: RX Theme
 */

(function () {
  "use strict";

  /**
   * Prevent duplicate loading if file is included more than once.
   */
  if (window.RXThemeLazyLoadInitialized) {
    return;
  }

  window.RXThemeLazyLoadInitialized = true;

  /**
   * Main global object.
   */
  var RXLazy = {
    version: "1.0.0",

    config: {
      root: null,
      rootMargin: "300px 0px",
      threshold: 0.01,

      imageSelector:
        "img[data-src], img[data-srcset], img.rx-lazy, img[loading='lazy']",

      pictureSourceSelector:
        "picture source[data-srcset], source[data-srcset]",

      iframeSelector:
        "iframe[data-src], iframe.rx-lazy-frame",

      videoSelector:
        "video[data-src], video.rx-lazy-video, video[data-poster], video source[data-src]",

      backgroundSelector:
        "[data-bg], [data-bg-image], [data-background], .rx-lazy-bg",

      scriptSelector:
        "script[type='text/rx-lazy-script'][data-src], script[data-rx-lazy-script]",

      styleSelector:
        "link[data-rx-lazy-style], link[data-href]",

      moduleSelector:
        "[data-rx-module]",

      lazyClass: "rx-lazy",
      loadingClass: "rx-lazy-loading",
      loadedClass: "rx-lazy-loaded",
      errorClass: "rx-lazy-error",

      useNativeLazy: true,
      observeDynamicContent: true,
      enableIdleLoad: true,
      idleTimeout: 2500,
      retryCount: 2,
      debug: false
    },

    observers: {
      image: null,
      iframe: null,
      video: null,
      background: null,
      module: null
    },

    loadedAssets: {
      scripts: {},
      styles: {},
      images: {}
    }
  };

  /**
   * Small helper: debug log.
   */
  function debugLog() {
    if (!RXLazy.config.debug) {
      return;
    }

    if (window.console && typeof window.console.log === "function") {
      window.console.log.apply(window.console, ["[RXLazy]"].concat([].slice.call(arguments)));
    }
  }

  /**
   * Check if browser supports IntersectionObserver.
   */
  function supportsIntersectionObserver() {
    return "IntersectionObserver" in window;
  }

  /**
   * Check if browser supports native image lazy loading.
   */
  function supportsNativeLazyLoading() {
    return "loading" in HTMLImageElement.prototype;
  }

  /**
   * requestIdleCallback fallback.
   */
  function runWhenIdle(callback, timeout) {
    if (typeof callback !== "function") {
      return;
    }

    if ("requestIdleCallback" in window) {
      window.requestIdleCallback(callback, {
        timeout: timeout || RXLazy.config.idleTimeout
      });
    } else {
      window.setTimeout(callback, 1);
    }
  }

  /**
   * requestAnimationFrame fallback.
   */
  function runNextFrame(callback) {
    if (typeof callback !== "function") {
      return;
    }

    if ("requestAnimationFrame" in window) {
      window.requestAnimationFrame(callback);
    } else {
      window.setTimeout(callback, 16);
    }
  }

  /**
   * Convert NodeList to Array safely.
   */
  function toArray(nodes) {
    return Array.prototype.slice.call(nodes || []);
  }

  /**
   * Safe query selector.
   */
  function queryAll(selector, context) {
    try {
      return toArray((context || document).querySelectorAll(selector));
    } catch (error) {
      debugLog("Invalid selector:", selector, error);
      return [];
    }
  }

  /**
   * Add class safely.
   */
  function addClass(element, className) {
    if (!element || !className) {
      return;
    }

    if (element.classList) {
      element.classList.add(className);
    } else if ((" " + element.className + " ").indexOf(" " + className + " ") === -1) {
      element.className += " " + className;
    }
  }

  /**
   * Remove class safely.
   */
  function removeClass(element, className) {
    if (!element || !className) {
      return;
    }

    if (element.classList) {
      element.classList.remove(className);
    } else {
      element.className = (" " + element.className + " ")
        .replace(" " + className + " ", " ")
        .trim();
    }
  }

  /**
   * Mark loading state.
   */
  function markLoading(element) {
    addClass(element, RXLazy.config.loadingClass);
    removeClass(element, RXLazy.config.loadedClass);
    removeClass(element, RXLazy.config.errorClass);
  }

  /**
   * Mark loaded state.
   */
  function markLoaded(element) {
    removeClass(element, RXLazy.config.loadingClass);
    removeClass(element, RXLazy.config.errorClass);
    addClass(element, RXLazy.config.loadedClass);

    element.setAttribute("data-rx-lazy-status", "loaded");
  }

  /**
   * Mark error state.
   */
  function markError(element) {
    removeClass(element, RXLazy.config.loadingClass);
    addClass(element, RXLazy.config.errorClass);

    element.setAttribute("data-rx-lazy-status", "error");
  }

  /**
   * Dispatch custom event.
   */
  function dispatchLazyEvent(element, eventName, detail) {
    if (!element) {
      return;
    }

    var event;

    detail = detail || {};

    try {
      event = new CustomEvent(eventName, {
        bubbles: true,
        cancelable: true,
        detail: detail
      });
    } catch (error) {
      event = document.createEvent("CustomEvent");
      event.initCustomEvent(eventName, true, true, detail);
    }

    element.dispatchEvent(event);
  }

  /**
   * Check whether element was already processed.
   */
  function isProcessed(element) {
    return element && element.getAttribute("data-rx-lazy-status") === "loaded";
  }

  /**
   * Set fetchpriority safely.
   */
  function applyFetchPriority(element) {
    if (!element) {
      return;
    }

    var priority = element.getAttribute("data-fetchpriority") || element.getAttribute("data-priority");

    if (priority) {
      try {
        element.setAttribute("fetchpriority", priority);
      } catch (error) {
        debugLog("fetchpriority not applied", error);
      }
    }
  }

  /**
   * Load normal image.
   */
  function loadImage(img) {
    if (!img || isProcessed(img)) {
      return;
    }

    markLoading(img);
    applyFetchPriority(img);

    var src = img.getAttribute("data-src");
    var srcset = img.getAttribute("data-srcset");
    var sizes = img.getAttribute("data-sizes");
    var decoding = img.getAttribute("data-decoding") || "async";

    try {
      img.decoding = decoding;
    } catch (error) {
      debugLog("Image decoding not supported", error);
    }

    img.onload = function () {
      markLoaded(img);
      dispatchLazyEvent(img, "rxlazy:image:loaded", {
        src: img.currentSrc || img.src
      });
    };

    img.onerror = function () {
      retryImage(img);
    };

    /**
     * Load source tags inside picture first.
     */
    var picture = img.parentNode && img.parentNode.nodeName
      ? img.parentNode.nodeName.toLowerCase() === "picture"
        ? img.parentNode
        : null
      : null;

    if (picture) {
      var sources = queryAll("source[data-srcset]", picture);

      sources.forEach(function (source) {
        var sourceSrcset = source.getAttribute("data-srcset");
        var sourceSizes = source.getAttribute("data-sizes");

        if (sourceSrcset) {
          source.setAttribute("srcset", sourceSrcset);
          source.removeAttribute("data-srcset");
        }

        if (sourceSizes) {
          source.setAttribute("sizes", sourceSizes);
          source.removeAttribute("data-sizes");
        }
      });
    }

    if (sizes) {
      img.setAttribute("sizes", sizes);
      img.removeAttribute("data-sizes");
    }

    if (srcset) {
      img.setAttribute("srcset", srcset);
      img.removeAttribute("data-srcset");
    }

    if (src) {
      img.setAttribute("src", src);
      img.removeAttribute("data-src");
    }

    /**
     * If image is already complete from cache.
     */
    if (img.complete && img.naturalWidth > 0) {
      markLoaded(img);
    }
  }

  /**
   * Retry image loading.
   */
  function retryImage(img) {
    if (!img) {
      return;
    }

    var currentRetry = parseInt(img.getAttribute("data-rx-retry") || "0", 10);

    if (currentRetry < RXLazy.config.retryCount) {
      img.setAttribute("data-rx-retry", String(currentRetry + 1));

      var originalSrc = img.getAttribute("src");

      if (originalSrc) {
        var separator = originalSrc.indexOf("?") === -1 ? "?" : "&";
        img.setAttribute("src", originalSrc + separator + "rx_retry=" + Date.now());
      }

      return;
    }

    markError(img);

    dispatchLazyEvent(img, "rxlazy:image:error", {
      src: img.getAttribute("src") || img.getAttribute("data-src")
    });
  }

  /**
   * Load picture source directly when needed.
   */
  function loadPictureSource(source) {
    if (!source || isProcessed(source)) {
      return;
    }

    var srcset = source.getAttribute("data-srcset");
    var sizes = source.getAttribute("data-sizes");

    if (srcset) {
      source.setAttribute("srcset", srcset);
      source.removeAttribute("data-srcset");
    }

    if (sizes) {
      source.setAttribute("sizes", sizes);
      source.removeAttribute("data-sizes");
    }

    markLoaded(source);
  }

  /**
   * Load iframe.
   */
  function loadIframe(iframe) {
    if (!iframe || isProcessed(iframe)) {
      return;
    }

    var src = iframe.getAttribute("data-src");

    if (!src) {
      return;
    }

    markLoading(iframe);

    iframe.onload = function () {
      markLoaded(iframe);
      dispatchLazyEvent(iframe, "rxlazy:iframe:loaded", {
        src: iframe.src
      });
    };

    iframe.onerror = function () {
      markError(iframe);
      dispatchLazyEvent(iframe, "rxlazy:iframe:error", {
        src: src
      });
    };

    iframe.setAttribute("src", src);
    iframe.removeAttribute("data-src");
  }

  /**
   * Load video.
   */
  function loadVideo(video) {
    if (!video || isProcessed(video)) {
      return;
    }

    markLoading(video);

    var poster = video.getAttribute("data-poster");
    var src = video.getAttribute("data-src");

    if (poster) {
      video.setAttribute("poster", poster);
      video.removeAttribute("data-poster");
    }

    if (src) {
      video.setAttribute("src", src);
      video.removeAttribute("data-src");
    }

    var sources = queryAll("source[data-src]", video);

    sources.forEach(function (source) {
      var sourceSrc = source.getAttribute("data-src");

      if (sourceSrc) {
        source.setAttribute("src", sourceSrc);
        source.removeAttribute("data-src");
      }
    });

    try {
      video.load();
    } catch (error) {
      debugLog("Video load error", error);
    }

    video.onloadeddata = function () {
      markLoaded(video);
      dispatchLazyEvent(video, "rxlazy:video:loaded", {});
    };

    video.onerror = function () {
      markError(video);
      dispatchLazyEvent(video, "rxlazy:video:error", {});
    };

    /**
     * Some videos may not fire loadeddata quickly.
     */
    window.setTimeout(function () {
      if (!isProcessed(video)) {
        markLoaded(video);
      }
    }, 2000);
  }

  /**
   * Load background image.
   */
  function loadBackground(element) {
    if (!element || isProcessed(element)) {
      return;
    }

    var bg =
      element.getAttribute("data-bg") ||
      element.getAttribute("data-bg-image") ||
      element.getAttribute("data-background");

    if (!bg) {
      return;
    }

    markLoading(element);

    var tempImg = new Image();

    tempImg.onload = function () {
      element.style.backgroundImage = "url('" + bg.replace(/'/g, "\\'") + "')";

      element.removeAttribute("data-bg");
      element.removeAttribute("data-bg-image");
      element.removeAttribute("data-background");

      markLoaded(element);

      dispatchLazyEvent(element, "rxlazy:background:loaded", {
        src: bg
      });
    };

    tempImg.onerror = function () {
      markError(element);

      dispatchLazyEvent(element, "rxlazy:background:error", {
        src: bg
      });
    };

    tempImg.src = bg;
  }

  /**
   * Load CSS file dynamically.
   */
  function loadStyleElement(link) {
    if (!link || isProcessed(link)) {
      return;
    }

    var href = link.getAttribute("data-href") || link.getAttribute("data-rx-lazy-style");

    if (!href) {
      return;
    }

    if (RXLazy.loadedAssets.styles[href]) {
      markLoaded(link);
      return;
    }

    markLoading(link);

    link.onload = function () {
      RXLazy.loadedAssets.styles[href] = true;
      markLoaded(link);

      dispatchLazyEvent(link, "rxlazy:style:loaded", {
        href: href
      });
    };

    link.onerror = function () {
      markError(link);

      dispatchLazyEvent(link, "rxlazy:style:error", {
        href: href
      });
    };

    link.setAttribute("rel", "stylesheet");
    link.setAttribute("href", href);
    link.removeAttribute("data-href");
    link.removeAttribute("data-rx-lazy-style");
  }

  /**
   * Load external script by URL.
   */
  function loadScriptByUrl(src, options) {
    options = options || {};

    return new Promise(function (resolve, reject) {
      if (!src) {
        reject(new Error("Missing script src"));
        return;
      }

      if (RXLazy.loadedAssets.scripts[src]) {
        resolve(src);
        return;
      }

      var script = document.createElement("script");

      script.src = src;
      script.async = options.async !== false;
      script.defer = options.defer === true;
      script.crossOrigin = options.crossorigin || options.crossOrigin || null;

      if (options.type) {
        script.type = options.type;
      }

      if (options.integrity) {
        script.integrity = options.integrity;
      }

      if (options.referrerpolicy) {
        script.referrerPolicy = options.referrerpolicy;
      }

      script.onload = function () {
        RXLazy.loadedAssets.scripts[src] = true;
        resolve(src);
      };

      script.onerror = function () {
        reject(new Error("Failed to load script: " + src));
      };

      document.head.appendChild(script);
    });
  }

  /**
   * Load lazy script tag.
   */
  function loadScriptElement(scriptElement) {
    if (!scriptElement || isProcessed(scriptElement)) {
      return;
    }

    var src =
      scriptElement.getAttribute("data-src") ||
      scriptElement.getAttribute("data-rx-lazy-script");

    if (!src) {
      return;
    }

    markLoading(scriptElement);

    loadScriptByUrl(src, {
      async: scriptElement.getAttribute("data-async") !== "false",
      defer: scriptElement.getAttribute("data-defer") === "true",
      type: scriptElement.getAttribute("data-script-type"),
      integrity: scriptElement.getAttribute("data-integrity"),
      crossorigin: scriptElement.getAttribute("data-crossorigin"),
      referrerpolicy: scriptElement.getAttribute("data-referrerpolicy")
    })
      .then(function () {
        markLoaded(scriptElement);

        dispatchLazyEvent(scriptElement, "rxlazy:script:loaded", {
          src: src
        });
      })
      .catch(function () {
        markError(scriptElement);

        dispatchLazyEvent(scriptElement, "rxlazy:script:error", {
          src: src
        });
      });
  }

  /**
   * Load module by data attribute.
   * Example:
   * <div data-rx-module="/assets/js/comments.js"></div>
   */
  function loadModuleElement(element) {
    if (!element || isProcessed(element)) {
      return;
    }

    var moduleSrc = element.getAttribute("data-rx-module");

    if (!moduleSrc) {
      return;
    }

    markLoading(element);

    loadScriptByUrl(moduleSrc, {
      async: true,
      defer: true,
      type: element.getAttribute("data-module-type") || "module"
    })
      .then(function () {
        markLoaded(element);

        dispatchLazyEvent(element, "rxlazy:module:loaded", {
          src: moduleSrc
        });
      })
      .catch(function () {
        markError(element);

        dispatchLazyEvent(element, "rxlazy:module:error", {
          src: moduleSrc
        });
      });
  }

  /**
   * Preload an asset.
   */
  function preloadAsset(url, asType, options) {
    if (!url) {
      return;
    }

    options = options || {};

    var selector = "link[rel='preload'][href='" + cssEscape(url) + "']";

    if (document.querySelector(selector)) {
      return;
    }

    var link = document.createElement("link");

    link.rel = "preload";
    link.href = url;
    link.as = asType || "script";

    if (options.crossorigin) {
      link.crossOrigin = options.crossorigin;
    }

    if (options.type) {
      link.type = options.type;
    }

    if (options.fetchpriority) {
      link.setAttribute("fetchpriority", options.fetchpriority);
    }

    document.head.appendChild(link);
  }

  /**
   * Prefetch an asset.
   */
  function prefetchAsset(url, options) {
    if (!url) {
      return;
    }

    options = options || {};

    var selector = "link[rel='prefetch'][href='" + cssEscape(url) + "']";

    if (document.querySelector(selector)) {
      return;
    }

    var link = document.createElement("link");

    link.rel = "prefetch";
    link.href = url;

    if (options.crossorigin) {
      link.crossOrigin = options.crossorigin;
    }

    document.head.appendChild(link);
  }

  /**
   * CSS.escape fallback.
   */
  function cssEscape(value) {
    if (window.CSS && typeof window.CSS.escape === "function") {
      return window.CSS.escape(value);
    }

    return String(value).replace(/'/g, "\\'");
  }

  /**
   * Observe element with IntersectionObserver.
   */
  function observeElement(observer, element, fallbackLoader) {
    if (!element) {
      return;
    }

    if (supportsIntersectionObserver() && observer) {
      observer.observe(element);
    } else if (typeof fallbackLoader === "function") {
      fallbackLoader(element);
    }
  }

  /**
   * Create observer.
   */
  function createObserver(loader) {
    if (!supportsIntersectionObserver()) {
      return null;
    }

    return new IntersectionObserver(
      function (entries, observer) {
        entries.forEach(function (entry) {
          if (entry.isIntersecting || entry.intersectionRatio > 0) {
            observer.unobserve(entry.target);
            loader(entry.target);
          }
        });
      },
      {
        root: RXLazy.config.root,
        rootMargin: RXLazy.config.rootMargin,
        threshold: RXLazy.config.threshold
      }
    );
  }

  /**
   * Native lazy loading enhancement.
   */
  function prepareNativeLazyImages(context) {
    if (!RXLazy.config.useNativeLazy || !supportsNativeLazyLoading()) {
      return;
    }

    var images = queryAll("img[data-src], img[data-srcset]", context);

    images.forEach(function (img) {
      if (!img.getAttribute("loading")) {
        img.setAttribute("loading", "lazy");
      }

      if (!img.getAttribute("decoding")) {
        img.setAttribute("decoding", "async");
      }
    });

    var iframes = queryAll("iframe[data-src]", context);

    iframes.forEach(function (iframe) {
      if (!iframe.getAttribute("loading")) {
        iframe.setAttribute("loading", "lazy");
      }
    });
  }

  /**
   * Scan DOM and register lazy elements.
   */
  function scan(context) {
    context = context || document;

    prepareNativeLazyImages(context);

    var images = queryAll(RXLazy.config.imageSelector, context);
    var sources = queryAll(RXLazy.config.pictureSourceSelector, context);
    var iframes = queryAll(RXLazy.config.iframeSelector, context);
    var videos = queryAll(RXLazy.config.videoSelector, context);
    var backgrounds = queryAll(RXLazy.config.backgroundSelector, context);
    var scripts = queryAll(RXLazy.config.scriptSelector, context);
    var styles = queryAll(RXLazy.config.styleSelector, context);
    var modules = queryAll(RXLazy.config.moduleSelector, context);

    images.forEach(function (img) {
      observeElement(RXLazy.observers.image, img, loadImage);
    });

    sources.forEach(function (source) {
      observeElement(RXLazy.observers.image, source, loadPictureSource);
    });

    iframes.forEach(function (iframe) {
      observeElement(RXLazy.observers.iframe, iframe, loadIframe);
    });

    videos.forEach(function (video) {
      observeElement(RXLazy.observers.video, video, loadVideo);
    });

    backgrounds.forEach(function (element) {
      observeElement(RXLazy.observers.background, element, loadBackground);
    });

    scripts.forEach(function (scriptElement) {
      if (RXLazy.config.enableIdleLoad) {
        runWhenIdle(function () {
          loadScriptElement(scriptElement);
        });
      } else {
        loadScriptElement(scriptElement);
      }
    });

    styles.forEach(function (link) {
      if (RXLazy.config.enableIdleLoad) {
        runWhenIdle(function () {
          loadStyleElement(link);
        });
      } else {
        loadStyleElement(link);
      }
    });

    modules.forEach(function (moduleElement) {
      observeElement(RXLazy.observers.module, moduleElement, loadModuleElement);
    });
  }

  /**
   * Setup MutationObserver for dynamic/Ajax content.
   */
  function setupMutationObserver() {
    if (!RXLazy.config.observeDynamicContent || !("MutationObserver" in window)) {
      return;
    }

    var mutationTimer = null;

    var observer = new MutationObserver(function (mutations) {
      var shouldScan = false;

      mutations.forEach(function (mutation) {
        if (mutation.addedNodes && mutation.addedNodes.length) {
          shouldScan = true;
        }
      });

      if (!shouldScan) {
        return;
      }

      window.clearTimeout(mutationTimer);

      mutationTimer = window.setTimeout(function () {
        scan(document);
      }, 120);
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  /**
   * Load assets after user interaction.
   */
  function setupInteractionLoader() {
    var loaded = false;

    var events = [
      "mousemove",
      "mousedown",
      "keydown",
      "touchstart",
      "scroll"
    ];

    function onFirstInteraction() {
      if (loaded) {
        return;
      }

      loaded = true;

      events.forEach(function (eventName) {
        window.removeEventListener(eventName, onFirstInteraction, passiveEventOptions());
      });

      dispatchLazyEvent(document, "rxlazy:user:interaction", {});

      runWhenIdle(function () {
        loadInteractionAssets();
      });
    }

    events.forEach(function (eventName) {
      window.addEventListener(eventName, onFirstInteraction, passiveEventOptions());
    });
  }

  /**
   * Passive event option helper.
   */
  function passiveEventOptions() {
    var supportsPassive = false;

    try {
      var options = Object.defineProperty({}, "passive", {
        get: function () {
          supportsPassive = true;
          return true;
        }
      });

      window.addEventListener("rx-passive-test", null, options);
      window.removeEventListener("rx-passive-test", null, options);
    } catch (error) {
      supportsPassive = false;
    }

    return supportsPassive ? { passive: true } : false;
  }

  /**
   * Load assets marked for user interaction.
   *
   * Examples:
   * <script type="text/rx-lazy-script" data-src="/chat.js" data-rx-load="interaction"></script>
   * <link data-href="/extra.css" data-rx-load="interaction">
   */
  function loadInteractionAssets() {
    var interactionScripts = queryAll(
      "script[type='text/rx-lazy-script'][data-src][data-rx-load='interaction']"
    );

    var interactionStyles = queryAll(
      "link[data-href][data-rx-load='interaction'], link[data-rx-lazy-style][data-rx-load='interaction']"
    );

    interactionScripts.forEach(loadScriptElement);
    interactionStyles.forEach(loadStyleElement);
  }

  /**
   * Load assets after full window load.
   */
  function setupWindowLoadAssets() {
    window.addEventListener("load", function () {
      runWhenIdle(function () {
        var scripts = queryAll(
          "script[type='text/rx-lazy-script'][data-src][data-rx-load='windowload']"
        );

        var styles = queryAll(
          "link[data-href][data-rx-load='windowload'], link[data-rx-lazy-style][data-rx-load='windowload']"
        );

        scripts.forEach(loadScriptElement);
        styles.forEach(loadStyleElement);
      });
    });
  }

  /**
   * Preload assets marked by data-rx-preload.
   *
   * Example:
   * <a href="/next-page/" data-rx-preload="hover">Next</a>
   */
  function setupHoverPreload() {
    document.addEventListener(
      "mouseover",
      function (event) {
        var target = event.target;

        while (target && target !== document) {
          if (target.getAttribute && target.getAttribute("data-rx-preload") === "hover") {
            var href = target.getAttribute("href") || target.getAttribute("data-href");

            if (href) {
              prefetchAsset(href);
            }

            break;
          }

          target = target.parentNode;
        }
      },
      passiveEventOptions()
    );
  }

  /**
   * Lazy load CSS background from responsive data attributes.
   *
   * Example:
   * data-bg-mobile=""
   * data-bg-tablet=""
   * data-bg-desktop=""
   */
  function applyResponsiveBackgrounds(context) {
    var elements = queryAll("[data-bg-mobile], [data-bg-tablet], [data-bg-desktop]", context);

    elements.forEach(function (element) {
      var width = window.innerWidth || document.documentElement.clientWidth;
      var bg = "";

      if (width <= 767) {
        bg = element.getAttribute("data-bg-mobile");
      } else if (width <= 1024) {
        bg = element.getAttribute("data-bg-tablet");
      } else {
        bg = element.getAttribute("data-bg-desktop");
      }

      if (bg) {
        element.setAttribute("data-bg", bg);
      }
    });
  }

  /**
   * Resize handler for responsive backgrounds.
   */
  function setupResponsiveBackgroundResize() {
    var resizeTimer = null;

    window.addEventListener(
      "resize",
      function () {
        window.clearTimeout(resizeTimer);

        resizeTimer = window.setTimeout(function () {
          applyResponsiveBackgrounds(document);
          scan(document);
        }, 250);
      },
      passiveEventOptions()
    );
  }

  /**
   * Lazy load Google Maps iframe only when clicked.
   *
   * Example:
   * <div class="rx-map-placeholder" data-rx-map-src="https://www.google.com/maps/embed?...">
   *   Click to load map
   * </div>
   */
  function setupClickToLoadMaps() {
    document.addEventListener("click", function (event) {
      var target = event.target;

      while (target && target !== document) {
        if (target.getAttribute && target.getAttribute("data-rx-map-src")) {
          var src = target.getAttribute("data-rx-map-src");

          if (!src) {
            return;
          }

          var iframe = document.createElement("iframe");

          iframe.src = src;
          iframe.loading = "lazy";
          iframe.width = target.getAttribute("data-width") || "100%";
          iframe.height = target.getAttribute("data-height") || "400";
          iframe.style.border = "0";
          iframe.setAttribute("allowfullscreen", "");
          iframe.setAttribute("referrerpolicy", "no-referrer-when-downgrade");

          target.innerHTML = "";
          target.appendChild(iframe);
          target.removeAttribute("data-rx-map-src");
          addClass(target, RXLazy.config.loadedClass);

          dispatchLazyEvent(target, "rxlazy:map:loaded", {
            src: src
          });

          break;
        }

        target = target.parentNode;
      }
    });
  }

  /**
   * Lazy load YouTube/Vimeo iframe on click.
   *
   * Example:
   * <div class="rx-video-placeholder" data-rx-embed-src="https://www.youtube.com/embed/VIDEO_ID">
   *   Play
   * </div>
   */
  function setupClickToLoadEmbeds() {
    document.addEventListener("click", function (event) {
      var target = event.target;

      while (target && target !== document) {
        if (target.getAttribute && target.getAttribute("data-rx-embed-src")) {
          var src = target.getAttribute("data-rx-embed-src");

          if (!src) {
            return;
          }

          var iframe = document.createElement("iframe");

          iframe.src = src;
          iframe.loading = "lazy";
          iframe.allow =
            "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share";
          iframe.allowFullscreen = true;
          iframe.width = target.getAttribute("data-width") || "100%";
          iframe.height = target.getAttribute("data-height") || "400";
          iframe.style.border = "0";

          target.innerHTML = "";
          target.appendChild(iframe);
          target.removeAttribute("data-rx-embed-src");
          addClass(target, RXLazy.config.loadedClass);

          dispatchLazyEvent(target, "rxlazy:embed:loaded", {
            src: src
          });

          break;
        }

        target = target.parentNode;
      }
    });
  }

  /**
   * Lazy load comments area.
   *
   * Example:
   * <div id="comments" data-rx-comments-module="/assets/js/comments.js"></div>
   */
  function setupCommentsLazyLoad() {
    var comments = queryAll("[data-rx-comments-module]");

    if (!comments.length) {
      return;
    }

    var commentsObserver = createObserver(function (element) {
      var src = element.getAttribute("data-rx-comments-module");

      if (!src) {
        return;
      }

      loadScriptByUrl(src, {
        async: true,
        defer: true
      })
        .then(function () {
          markLoaded(element);

          dispatchLazyEvent(element, "rxlazy:comments:loaded", {
            src: src
          });
        })
        .catch(function () {
          markError(element);
        });
    });

    comments.forEach(function (element) {
      observeElement(commentsObserver, element, function () {
        var src = element.getAttribute("data-rx-comments-module");

        if (src) {
          loadScriptByUrl(src);
        }
      });
    });
  }

  /**
   * Lazy load ads placeholder.
   *
   * Example:
   * <div data-rx-ad-module="/assets/js/ads.js"></div>
   */
  function setupAdsLazyLoad() {
    var ads = queryAll("[data-rx-ad-module]");

    if (!ads.length) {
      return;
    }

    var adObserver = createObserver(function (element) {
      var src = element.getAttribute("data-rx-ad-module");

      if (!src) {
        return;
      }

      loadScriptByUrl(src, {
        async: true,
        defer: true
      })
        .then(function () {
          markLoaded(element);

          dispatchLazyEvent(element, "rxlazy:ad:loaded", {
            src: src
          });
        })
        .catch(function () {
          markError(element);
        });
    });

    ads.forEach(function (element) {
      observeElement(adObserver, element, function () {
        var src = element.getAttribute("data-rx-ad-module");

        if (src) {
          loadScriptByUrl(src);
        }
      });
    });
  }

  /**
   * Lazy hydrate components.
   *
   * Example:
   * <div data-rx-hydrate="/assets/js/component.js"></div>
   */
  function setupLazyHydration() {
    var components = queryAll("[data-rx-hydrate]");

    if (!components.length) {
      return;
    }

    var hydrationObserver = createObserver(function (element) {
      var src = element.getAttribute("data-rx-hydrate");

      if (!src) {
        return;
      }

      loadScriptByUrl(src, {
        async: true,
        defer: true,
        type: element.getAttribute("data-hydrate-type") || null
      })
        .then(function () {
          markLoaded(element);

          dispatchLazyEvent(element, "rxlazy:hydrate:loaded", {
            src: src
          });
        })
        .catch(function () {
          markError(element);
        });
    });

    components.forEach(function (element) {
      observeElement(hydrationObserver, element, function () {
        var src = element.getAttribute("data-rx-hydrate");

        if (src) {
          loadScriptByUrl(src);
        }
      });
    });
  }

  /**
   * Data API:
   * <div data-rx-prefetch="/page-2/"></div>
   * <div data-rx-preload="/assets/app.js" data-as="script"></div>
   */
  function setupDataAssetHints(context) {
    var preloads = queryAll("[data-rx-preload-url]", context);
    var prefetches = queryAll("[data-rx-prefetch-url]", context);

    preloads.forEach(function (element) {
      var url = element.getAttribute("data-rx-preload-url");
      var asType = element.getAttribute("data-as") || "script";

      preloadAsset(url, asType, {
        crossorigin: element.getAttribute("data-crossorigin"),
        type: element.getAttribute("data-type"),
        fetchpriority: element.getAttribute("data-fetchpriority")
      });
    });

    prefetches.forEach(function (element) {
      var url = element.getAttribute("data-rx-prefetch-url");

      prefetchAsset(url, {
        crossorigin: element.getAttribute("data-crossorigin")
      });
    });
  }

  /**
   * Public API.
   */
  RXLazy.scan = scan;
  RXLazy.loadImage = loadImage;
  RXLazy.loadIframe = loadIframe;
  RXLazy.loadVideo = loadVideo;
  RXLazy.loadBackground = loadBackground;
  RXLazy.loadScriptByUrl = loadScriptByUrl;
  RXLazy.preloadAsset = preloadAsset;
  RXLazy.prefetchAsset = prefetchAsset;
  RXLazy.runWhenIdle = runWhenIdle;

  /**
   * Initialize observers.
   */
  function setupObservers() {
    RXLazy.observers.image = createObserver(function (element) {
      if (element.nodeName && element.nodeName.toLowerCase() === "source") {
        loadPictureSource(element);
      } else {
        loadImage(element);
      }
    });

    RXLazy.observers.iframe = createObserver(loadIframe);
    RXLazy.observers.video = createObserver(loadVideo);
    RXLazy.observers.background = createObserver(loadBackground);
    RXLazy.observers.module = createObserver(loadModuleElement);
  }

  /**
   * Initialize everything.
   */
  function init() {
    setupObservers();

    applyResponsiveBackgrounds(document);
    setupDataAssetHints(document);

    scan(document);

    setupMutationObserver();
    setupInteractionLoader();
    setupWindowLoadAssets();
    setupHoverPreload();
    setupResponsiveBackgroundResize();
    setupClickToLoadMaps();
    setupClickToLoadEmbeds();
    setupCommentsLazyLoad();
    setupAdsLazyLoad();
    setupLazyHydration();

    dispatchLazyEvent(document, "rxlazy:ready", {
      version: RXLazy.version
    });

    debugLog("RX LazyLoad initialized");
  }

  /**
   * Start safely.
   */
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    runNextFrame(init);
  }

  /**
   * Expose globally.
   */
  window.RXLazy = RXLazy;
})();

Example HTML usage

Lazy image:

<img
  class="rx-lazy"
  src="small-placeholder.jpg"
  data-src="real-image.jpg"
  data-srcset="real-image-480.jpg 480w, real-image-800.jpg 800w"
  data-sizes="(max-width: 768px) 100vw, 800px"
  alt="RX Theme image"
  loading="lazy"
  decoding="async">

Lazy background:

<div class="rx-lazy-bg" data-bg="hero-image.jpg"></div>

Responsive lazy background:

<div
  class="rx-lazy-bg"
  data-bg-mobile="hero-mobile.jpg"
  data-bg-tablet="hero-tablet.jpg"
  data-bg-desktop="hero-desktop.jpg">
</div>

Lazy iframe:

<iframe
  class="rx-lazy-frame"
  data-src="https://www.youtube.com/embed/example"
  loading="lazy"
  width="100%"
  height="400">
</iframe>

Click-to-load video embed:

<div
  class="rx-video-placeholder"
  data-rx-embed-src="https://www.youtube.com/embed/example"
  data-width="100%"
  data-height="400">
  Click to load video
</div>

Lazy script:

<script
  type="text/rx-lazy-script"
  data-src="/wp-content/themes/rx-theme/assets/static-js/app.js"
  data-rx-load="windowload">
</script>

Lazy CSS:

<link
  data-href="/wp-content/themes/rx-theme/assets/static-css/extra.css"
  data-rx-load="windowload">

Lazy module:

<div data-rx-module="/wp-content/themes/rx-theme/assets/static-js/comments.js"></div>

Best enqueue name for WordPress

In your functions.php or enqueue file, use:

wp_enqueue_script(
    'rx-theme-lazyload',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-008-lazyload.js',
    array(),
    '1.0.0',
    true
);

For performance, this file should load in the footer with true.

Leave a Reply

Your email address will not be published. Required fields are marked *