Swiper Lazy Loading: Built-in Module vs. Custom IntersectionObserver
A decision guide for deferring image loads in Swiper: compare the built-in Lazy module against a custom IntersectionObserver, with implementation and validation steps.
07 Sept 2026, 10:36 UTC

Image-heavy carousels can delay the first meaningful paint and waste bandwidth on slides the user may never see. When building with Swiper, the engineering decision is whether to use Swiper's built-in Lazy module or a custom IntersectionObserver to defer image loading. The built-in module integrates directly with Swiper's slide lifecycle and is faster to adopt, while a custom observer gives you control over non-image media and avoids Swiper's internal image cache. This guide compares the two approaches, shows how to implement each, and explains how to verify the result in the browser.
Constraints and version assumptions
These notes assume Swiper 8 or later, where the Lazy module is not bundled by default. You must import and register it explicitly. If you choose the custom route, IntersectionObserver is available in all modern browsers, but you should confirm support for your target matrix or include a polyfill. In both cases, lazy images use a data-src attribute instead of src so the browser does not fetch them eagerly.
Option comparison
| Aspect | Built-in Lazy module | Custom IntersectionObserver |
|---|---|---|
| Integration effort | Import and register the module; add lazy class and data-src | Write observer logic and assign src manually |
| Supported media | Primarily images | Images, videos, iframes, or background images |
| Preload control | preloadImages, lazyPreloadPrevNext, lazyPreloadAll | Fully manual via rootMargin and thresholds |
| Lifecycle integration | Fires with Swiper slide events | Independent; must sync with Swiper if needed |
| Memory and caching | Swiper caches loaded images internally | No Swiper cache; browser cache only |
| Best for | Standard image carousels | Mixed media, custom batching, or avoiding Swiper cache overhead |
Trade-offs in practice
Built-in Lazy module
The built-in module is the path of least resistance. After importing Lazy and passing it in the modules array, Swiper automatically swaps data-src into src when a slide enters the viewport. You can tune neighbor preloading with lazyPreloadPrevNext (for example, 1 loads the previous and next slides) or force all images to load with lazyPreloadAll. Setting preloadImages: false prevents Swiper from eagerly loading every image on initialization.
A known risk is interference from external scripts or CSS that manipulate src attributes directly. If another library rewrites an image's src before Swiper's lazy logic runs, you may see duplicate requests or broken images. Keep image manipulation inside Swiper's lifecycle or use the custom approach when third-party code touches the DOM.
Custom IntersectionObserver
A custom observer decouples lazy loading from Swiper internals. Because Swiper translates slides with CSS transforms and clips overflow on its container, you should set the observer's root to the Swiper wrapper element. IntersectionObserver computes visibility against the root's clip rectangle, so off-screen slides that are merely translated will not trigger loads. You can also adjust rootMargin to start loading slightly before a slide becomes fully visible, which smooths transitions.
The cost is manual bookkeeping. You must unobserve images after they load, handle dynamically added slides, and decide how to react to Swiper's slideChange event if you need to force-load a specific slide. For typical carousels the performance difference versus the built-in module is small, but a custom observer can reduce memory pressure when you want to avoid Swiper's internal image cache entirely.
Concrete implementation
Using Swiper's built-in Lazy module
In your project's entry JavaScript file, import Swiper and the Lazy module, then initialize the carousel. In your HTML, mark each lazy image with the lazy class and a data-src attribute.
import Swiper from 'swiper';
import { Lazy } from 'swiper/modules';
const swiper = new Swiper('.swiper', {
modules: [Lazy],
preloadImages: false,
lazyPreloadPrevNext: 1,
});
<div class='swiper'>
<div class='swiper-wrapper'>
<div class='swiper-slide'>
<img class='lazy' data-src='/img/slide-1.jpg' alt='First slide' />
</div>
<div class='swiper-slide'>
<img class='lazy' data-src='/img/slide-2.jpg' alt='Second slide' />
</div>
</div>
</div>
Replace .swiper with your container selector and /img/slide-1.jpg with real paths. After initialization, open your browser's DevTools Network panel and filter for images. You should see requests only for the active slide and the neighbors defined by lazyPreloadPrevNext.
Using a custom IntersectionObserver
Initialize Swiper without the Lazy module, then create an observer rooted to the Swiper container. The following example runs in your entry JavaScript file after the DOM is ready.
import Swiper from 'swiper';
const swiper = new Swiper('.swiper', {
// standard options, no Lazy module
});
const swiperEl = document.querySelector('.swiper');
const lazyImages = swiperEl.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
});
}, {
root: swiperEl,
rootMargin: '0px',
threshold: 0.1,
});
lazyImages.forEach((img) => imageObserver.observe(img));
The threshold: 0.1 value means the callback fires when at least 10% of the image is visible within the Swiper container. Increase rootMargin (for example, '200px') if you want to start loading earlier. After adding or removing slides dynamically, re-query img[data-src] and observe the new elements.
Validation and practical checks
- Network panel: Open DevTools in your browser, select the Network tab, filter by Img, and reload the page. Confirm that only the active slide and any explicitly preloaded neighbors issue requests.
- Performance tab: Record a page load before and after enabling lazy loading. Compare the initial image payload to verify the expected reduction.
- Custom observer root: Inspect a slide that is translated but outside the visible area. If its image loads immediately, the observer's
rootis likely set to the document viewport instead of the Swiper container. - Dynamic content: Add a slide via Swiper's API and confirm the new image does not load until it enters the viewport.
Limitations and risks
The built-in Lazy module is not enabled by default in Swiper 8 and later, so forgetting the modules: [Lazy] registration will leave images with data-src unloaded. External scripts that rewrite src can interfere with Swiper's lazy logic and cause duplicate loads. The custom IntersectionObserver approach avoids Swiper's cache but requires you to manage observer lifecycles, especially when slides are added or removed. For most standard image carousels, the performance difference between the two strategies is small; choose the built-in module for speed of integration and the custom observer when you need control over media types or want to bypass Swiper's internal caching.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.