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
srcwhen 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-videoon the page - move the original
srcintodata-src - clear the
srcso 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 visibleunloadTimeouts→ 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
srcfromdata-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
srcagain → 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.
