There’s been plenty written about basic lazy loading, so we won’t go too deep into it. We’ll touch on the key points of the main techniques, their pros and cons, and then look at more advanced techniques that can save your animated website.
It’s universal and accessible – just add it to everything outside the hero section. The browser decides on its own when and how to load, based on speed and current load.
Pros
Native
Many CMS platforms add it automatically; there are plenty of plugins that handle this
Minimal setup required
Cons
The browser might easily go overboard and start loading images 2–3 screens early, or start way too late, leaving the user staring at blinking empty boxes. Chromium starts loading at roughly ~1250px on a fast connection and ~2500px on a slow one (historically, these thresholds were as high as 3000px and 8000px).
It doesn’t handle horizontally scrollable sections well – it often only starts showing content after the element enters the viewport. Same issue with position: sticky.
Completely uncontrollable.
Data attributes and placeholders
Here things become much more controllable since JavaScript does everything on our terms.
We can split this into two main approaches:
With a placeholder.
Without a placeholder.
Both work almost identically – we swap src, srcset, and sizes for data-src, data-srcset, and data-sizes. Once the element is 1-2 screens away, we swap them back to their original values, and there’s your lazily loaded image. In the placeholder version, src isn’t left empty – it gets a 1-2 KB SVG or JPEG image.
Which one to pick – that’s up to you. Google claims that Googlebot renders JavaScript and sees the final DOM state, including the swapped-in src. Some in the community play it safe: if rendering doesn’t finish in time, the bot might index the placeholder. In our own projects, we leave src empty – and we haven’t seen any indexing issues.
Lazy loading for video
Video is a different story. preload is a browser hint, and it’s important to understand which direction it can be ignored in.
preload=”none” is respected by browsers.
Modern browsers won’t start loading video data if preload="none" is set – there are no confirmed cases of the opposite in current Chrome, Firefox, or Safari. A browser may load less than requested (on mobile, preload="auto" often falls back to metadata to save bandwidth).
But there’s a catch. Background videos on animated sites almost always have autoplay muted playsinline. The autoplay attribute completely overrides preload – the browser must load data to start playback. In that case, preload="none" is useless.
The solution:
remove src from <source> and put it in data-src. Without src, the browser has nothing to load – regardless of whether autoplay is present. When the IntersectionObserver fires, we swap in the real path and call video.load(). By the time the video enters the viewport, it’s already buffering.
Lazy for heavily animated sites
On sites with lots of animations – horizontal sections, custom scroll, 3D transforms – native loading="lazy" just doesn’t cut it. Watching a flashy site where loading images pop in mid-transition is a pretty dubious experience.
IntersectionObserver is what’s left. But applying the same rootMargin to every element runs into problems:
Horizontal sections. If they’re built with overflow-x: hidden + transform: translateX() (like in GSAP ScrollTrigger), elements physically sit outside the viewport horizontally. IO with root=null can see their bounding rect and correctly detect intersection, but rootMargin only sets vertical offsets. A horizontal margin can be set, but it’s fixed and doesn’t account for 5+ screens of content within a section.
Container scroll. When body has overflow: hidden and a nested div is what scrolls (common for preventing address bar jumps on mobile browsers that break 3D animations), IO with root=null stops receiving events: from the viewport’s perspective, nothing is moving. You need to explicitly set the scroll container as the Observer’s root – and standard plugins don’t do this.
Our solution
A dual-mode system: self and parent.
Self – the Observer watches the image itself. Good for regular vertical sections. Works like classic lazy loading but with a controllable threshold and custom root support.
Parent – the Observer watches the parent container (by default .e-parent – Elementor’s container class). When the container enters the zone, all nested lazy media load at once. This solves the horizontal section problem: we don’t wait for each image to scroll into view – we load everything as the section approaches.
So, we created a custom WordPress + Elementor plugin: the PHP side moves src, srcset, and sizes into data attributes. In the admin panel, you pick the mode and default thresholds; in the Elementor editor, you configure settings per widget.
Three threshold tiers: tier 1 (~100% viewport height), tier 2 (~150%), tier 3 (~200%). If a section is tall and static – use self with tier 2. If a section is horizontal or has heavy animations – use parent with tier 3, so everything loads well in advance.
Supporting stuff
Separate desktop/mobile cache. The hero section on desktop might contain images that end up below the fold on mobile. With server-side caching, you need branching: separate lazy settings for each device type.
Custom root for IO. If scrolling is handled by a nested container, you specify its selector in the settings, and the script uses that element as the Observer’s root. Without this, rootMargin won’t work.
Optimizing sizes/srcset. On mobile, we can programmatically trim unnecessary variants from srcset, reducing the chance of loading unnecessarily heavy images.
Implementation example
The server (WordPress + Elementor) prepares the markup: either native loading="lazy" (soft mode) or data-src + placeholder (hard mode). Elements get data-lazy-* attributes with tier and scope settings.
The client spins up an IntersectionObserver with the configured root and rootMargin. On intersection – it swaps in real URLs, removes native lazy, and unblocks video. The script is not loaded in the Elementor editor.
Soft (global): the wp_get_attachment_image_attributes filter adds loading="lazy" to all images except those with fetchpriority="high", the brs-no-lazy class, or elements that already have data-src set.
On the front end: in soft mode, we remove loading="lazy" when the threshold is crossed so loading follows our `rootMargin` instead of browser defaults. In hard mode – we move data-src → src.
function activateHardImage(el) {
// data-src → src, data-srcset → srcset, data-sizes → sizes
// Handle inside
}
function activateSoftImage(el) {
if (el.getAttribute('loading') === 'lazy') {
el.removeAttribute('loading');
}
stripLazyMetaAttrs(el);
}
Video
Elements with the brs-lz-video class contain a <source> with data-src instead of src. The autoplay attribute stays – but without src, there’s nothing for the browser to play. On activation: we swap in the real src and call video.load() – the video starts buffering and playing.
Exclusions and integrations
Exclusions: CSS selectors in the admin panel + the brs-no-lazy class / data-brs-no-lazy attribute.
Perfmatters and similar tools: the lazy script needs to be excluded from Delay / Defer JS, otherwise the Observer kicks in after elements have already appeared on screen.
So...
The right approach depends on the complexity of the project. For static sites, the native attribute is enough. For animated sites, you need controllable JavaScript that understands the context of each element.
Principles we’ve taken away: lazy loading should be predictable (we know the trigger threshold), contextual (self for vertical, parent for horizontal), and adaptive (different settings for different devices and sections).
P.S. If you need some assistance with this, just drop us a line.
Alex is a seasoned expert with 9 years of experience in WebGL, WordPress, VR/AR, and ecommerce, turning complex (and sometimes crazy) ideas into seamless, user-friendly solutions with passion and attention to detail.
Subscribe for exclusive access to my tailored knowledge base, inspiring content, the latest in tech, and practical tutorials that empower your growth.
By submitting, you agree to receive email updates. Unsubscribe anytime.
Manage Consent
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts.
Functional
Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts.
Functional
Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.