ElementorWordPressJavaScriptPerformanceMobile Optimization

Fixing Mobile Crashes in Elementor Carousels with Multiple Autoplay Videos

Learn how to use Intersection Observer to lazy-load videos and prevent mobile browser crashes in Elementor carousels with multiple autoplaying videos.

Milan PavlákMilan Pavlák
7 min read
Fixing Mobile Crashes in Elementor Carousels with Multiple Autoplay Videos

When you place several autoplaying videos inside an animated Elementor carousel (especially layouts like two-column vertical carousels with opposite scroll directions), mobile and tablet devices often struggle to handle the load.

Symptoms usually include:

  • mobile browser freezing or crashing
  • severe FPS drops
  • delayed scroll or touch response
  • videos loading simultaneously and overwhelming memory/CPU
  • inconsistent autoplay behavior

This happens because each <video> element inside the carousel tries to:

  • load its full source
  • decode the file
  • autoplay as soon as it appears in the DOM

On a page with 6 + 6 videos moving constantly, this becomes a performance bottleneck—especially on iOS and mid‑range Android devices.

To solve this, we need a smarter system.


A Smarter Approach: Lazy‑Load + Auto‑Pause Using Intersection Observer

The script below solves all major issues by:

  • Only loading videos when they are actually visible (50% in viewport)
  • Pausing & unloading videos when they leave the viewport
  • Reducing RAM usage by removing src when not needed
  • Preventing simultaneous decoding of multiple videos
  • Running only on mobile & tablet (≤ 1024px)
  • Working globally for all Elementor videos (video.elementor-video)

This results in:

  • smooth scrolling
  • zero browser crashes
  • stable performance on mobile
  • videos autoplay only when appropriate
  • carousels running fluidly even with many videos

The Script

Below is the exact optimized script you can use:

<script>
document.addEventListener("DOMContentLoaded", function() {
  // Run only on Elementor mobile + tablet (<= 1024px)
  if (window.innerWidth > 1024) {
    return;
  }

  /* ---------------------------------------
     Lazy-load + pause videos outside viewport
     (GLOBAL – applies to all Elementor videos)
     --------------------------------------- */
  var allVideos = document.querySelectorAll("video.elementor-video");

  allVideos.forEach(function(video) {
    if (!video.getAttribute("data-src")) {
      video.setAttribute("data-src", video.getAttribute("src"));
    }
    video.removeAttribute("src");
    video.setAttribute("preload", "none");
    video.isIntersecting = false;
  });

  var loadTimeouts = new Map();
  var unloadTimeouts = new Map();
  var observerOptions = { root: null, threshold: 0.5 };

  var observer = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
      var video = entry.target;
      video.isIntersecting = entry.isIntersecting;

      if (entry.isIntersecting) {
        // Cancel unload
        if (unloadTimeouts.has(video)) {
          clearTimeout(unloadTimeouts.get(video));
          unloadTimeouts.delete(video);
        }

        // Schedule load and play
        if (!loadTimeouts.has(video)) {
          var timer = setTimeout(function() {
            if (video.isIntersecting) {
              if (!video.getAttribute("src")) {
                video.setAttribute("src", video.getAttribute("data-src"));
                video.load();
              }
              video.play().catch(function(error) {
                console.error("Error auto-playing video:", error);
              });
            }
            loadTimeouts.delete(video);
          }, 300);

          loadTimeouts.set(video, timer);
        }
      } else {
        // Cancel load
        if (loadTimeouts.has(video)) {
          clearTimeout(loadTimeouts.get(video));
          loadTimeouts.delete(video);
        }

        // Schedule pause + unload src
        if (!unloadTimeouts.has(video)) {
          var timer2 = setTimeout(function() {
            if (!video.isIntersecting) {
              video.pause();
              video.removeAttribute("src");
            }
            unloadTimeouts.delete(video);
          }, 50);

          unloadTimeouts.set(video, timer2);
        }
      }
    });
  }, observerOptions);

  // Observe all videos
  allVideos.forEach(function(video) {
    observer.observe(video);
  });
});
</script>

Code Breakdown: How the Script Works

Let's walk through the key parts of the script so you can confidently modify or reuse it in other projects.

1. Early exit on desktop

if (window.innerWidth > 1024) {
  return;
}

We only need this optimization on mobile and tablet. Desktop devices usually handle multiple videos better, and you might want them to autoplay there. Anything wider than 1024px simply skips the logic.


2. Preparing all Elementor videos for lazy-loading

var allVideos = document.querySelectorAll("video.elementor-video");

allVideos.forEach(function(video) {
  if (!video.getAttribute("data-src")) {
    video.setAttribute("data-src", video.getAttribute("src"));
  }
  video.removeAttribute("src");
  video.setAttribute("preload", "none");
  video.isIntersecting = false;
});

Here we:

  • select every video.elementor-video on the page
  • move the original src into data-src
  • clear the src so the browser doesn't load the file yet
  • set preload="none" so the browser doesn't pre-buffer

From now on, videos are effectively placeholders until they enter the viewport.


3. Tracking load/unload timers per video

var loadTimeouts = new Map();
var unloadTimeouts = new Map();
var observerOptions = { root: null, threshold: 0.5 };

We use two Map objects to store setTimeout IDs for each video:

  • loadTimeouts → delayed load & play when the video becomes visible
  • unloadTimeouts → delayed pause & unload when it leaves the viewport

threshold: 0.5 means the callback fires once 50% of the video is visible.


4. IntersectionObserver: deciding when to load or unload

var observer = new IntersectionObserver(function(entries) {
  entries.forEach(function(entry) {
    var video = entry.target;
    video.isIntersecting = entry.isIntersecting;

    if (entry.isIntersecting) {
      // ... handle load logic ...
    } else {
      // ... handle unload logic ...
    }
  });
}, observerOptions);

The IntersectionObserver watches each video and tells us when it:

  • enters the viewport (≥ 50% visible)
  • leaves the viewport

Instead of constantly checking scroll position manually, the browser notifies us efficiently.


5. When a video becomes visible

if (entry.isIntersecting) {
  // Cancel unload
  if (unloadTimeouts.has(video)) {
    clearTimeout(unloadTimeouts.get(video));
    unloadTimeouts.delete(video);
  }

  // Schedule load and play
  if (!loadTimeouts.has(video)) {
    var timer = setTimeout(function() {
      if (video.isIntersecting) {
        if (!video.getAttribute("src")) {
          video.setAttribute("src", video.getAttribute("data-src"));
          video.load();
        }
        video.play().catch(function(error) {
          console.error("Error auto-playing video:", error);
        });
      }
      loadTimeouts.delete(video);
    }, 300);

    loadTimeouts.set(video, timer);
  }
}

What happens here:

  • we cancel any pending unload (in case the user scrolls back quickly)
  • we schedule a delayed load & play (300ms)
  • we check again if the video is still in view before loading (avoid unnecessary work)
  • we restore src from data-src, call .load(), and then .play()

The delay prevents too many videos from loading at once during fast scrolling.


6. When a video leaves the viewport

// Cancel load
if (loadTimeouts.has(video)) {
  clearTimeout(loadTimeouts.get(video));
  loadTimeouts.delete(video);
}

// Schedule pause + unload src
if (!unloadTimeouts.has(video)) {
  var timer2 = setTimeout(function() {
    if (!video.isIntersecting) {
      video.pause();
      video.removeAttribute("src");
    }
    unloadTimeouts.delete(video);
  }, 50);

  unloadTimeouts.set(video, timer2);
}

When a video moves out of view:

  • any pending load is cancelled (no need to load it anymore)
  • we schedule a very quick timeout (50ms) to:
    • pause() the video
    • remove its src again → freeing memory and stopping buffering

That's where the real RAM and CPU savings come from.


7. Start observing all videos

allVideos.forEach(function(video) {
  observer.observe(video);
});

Finally, every Elementor video is attached to the IntersectionObserver, making the whole system fully automatic.

No widget changes, no extra attributes – the script just works on top of your existing design.


Why This Script Works so Well

1. Intersection Observer controls visibility-based loading

The browser only loads and plays videos when at least 50% of the element is visible.

Perfect for moving carousels, where videos scroll in and out of view.

2. Removing the video src drastically reduces memory usage

When videos aren't needed, the script removes their src attribute.

That means:

  • no buffering
  • no decoding
  • no RAM consumption
  • no GPU usage

3. Delayed loading avoids CPU spikes

The setTimeout(..., 300) ensures the browser doesn't try to load too many videos at once.

4. Immediate unloading avoids lag

When a video leaves the viewport, we pause & unload it after 50ms.

5. Lightweight, vanilla JavaScript

No dependencies. No performance cost.

6. Works for ALL Elementor videos automatically

No need to edit widgets—this is plug‑and‑play.


Ideal Use Cases

This script is designed specifically for high‑motion layouts like:

  • Vertical autoplaying carousels
  • Multi‑column video grids with scroll animation
  • Video background sections with half‑visibility
  • Looping promo videos in sliders
  • Long pages with many Elementor video widgets

If your layout contains:

  • moving videos,
  • autoplay loops, and
  • more than 3 videos,

…this script is essential for mobile users.


Final Thoughts

Elementor's default video widgets do not include lazy-loading, visibility-based control, or RAM optimization. On desktop, it's manageable—but mobile devices can quickly become overwhelmed.

This script adds the missing logic:

  • smart loading
  • smart unloading
  • automatic pause
  • viewport awareness

Your visitors get:

  • smooth performance
  • zero freezes
  • full interaction

And your videos continue to autoplay only when they should.

Let's Connect

Ready to discuss your project? Reach out through any of these channels.

Based in Bratislava, Slovakia. Available for projects worldwide.

Fixing Mobile Crashes in Elementor Carousels with Multiple Autoplay Videos