How to Lazy-Load Images in Carousels

This guide extends Lazy-Loading Images Without Hurting LCP, part of Image & Media Optimization. Carousels — hero sliders, product galleries, testimonial rotators — are one of the most common causes of poor LCP. They typically go wrong in one of two ways. Either every slide loads eagerly, so five or ten large images compete for bandwidth with the one image the user actually sees, or every slide is lazy-loaded by a JavaScript library, including the first, so the LCP image waits for the script to download, run and decide what to load.

The fix has two halves: make the first visible slide an ordinary, high-priority <img> in the server-rendered HTML, and defer the rest so they load just before the user can reach them.

Hero carousel: all eager vs first-slide priority Timeline comparing a carousel loading all slides at once with one that prioritises the first slide and defers the rest. Hero carousel: all eager vs first-slide priority All slides eager 6 slides share bandwidth JS library lazy-load carousel JS slide 1 First slide priority slide 1 high slides 2-3 idle 0ms 400ms 800ms 1200ms 1600ms 2000ms 2400ms 2800ms 3200ms LCP

Rapid Diagnosis

  • Count carousel image requests before any interaction. More than one or two large images means hidden slides are loading.
  • Check the first slide's markup in View Source. If it is a <div data-src> or has no src, it depends on JavaScript.
  • Check loading and fetchpriority on the first slide image. It must not be lazy.
  • Check for layout shifts when the carousel initialises; libraries often change slide dimensions on mount.

Root Cause Analysis

1. Hidden slides are still rendered. Slides positioned off-screen with transforms are rendered elements, so their images load eagerly.

2. Library lazy loading applies to every slide. Generic settings mark the first slide as lazy too.

3. Client-rendered carousels. When slides are built from JSON in JavaScript, nothing loads until the bundle runs.

4. Autoplay preloading. Some libraries preload all slides for smooth autoplay.

Step-by-Step Resolution

1. Server-render the first slide as a priority image

html
<div class="carousel" aria-roledescription="carousel" aria-label="Featured collections">
  <div class="slide" aria-roledescription="slide" aria-label="1 of 5">
    <img src="/img/s1-1200.avif" srcset="/img/s1-800.avif 800w, /img/s1-1200.avif 1200w, /img/s1-1600.avif 1600w"
         sizes="100vw" width="1600" height="700" alt="Autumn collection" fetchpriority="high">
  </div>
  <div class="slide" aria-roledescription="slide" aria-label="2 of 5">
    <img src="/img/s2-1200.avif" srcset="/img/s2-800.avif 800w, /img/s2-1200.avif 1200w" sizes="100vw"
         width="1600" height="700" alt="Winter coats" loading="lazy">
  </div>
  <!-- ... -->
</div>
<!-- trade-off: native lazy loading may still load horizontally adjacent slides
     early because they are within the distance threshold; that is often fine. -->

2. Defer remaining slides explicitly

For carousels where hidden slides are horizontally inside the viewport box (common with transform-based sliders), native lazy loading can still fetch them. Use data-src for slides 3+ and swap them in when the slide before them becomes active.

javascript
carousel.on('change', (index) => {
  for (const i of [index + 1, index + 2]) {
    const img = slides[i]?.querySelector('img[data-src]');
    if (img) { img.srcset = img.dataset.srcset; img.src = img.dataset.src; img.removeAttribute('data-src'); }
  }
});
// trade-off: loading two slides ahead hides latency for swipes but costs bytes for
// users who never swipe; one ahead suits autoplay-free product galleries.

3. Reserve dimensions to avoid shifts

Give the carousel container an aspect-ratio matching the slides, and make sure the first slide is visible before the library initialises, so mounting does not change its size.

4. Delay autoplay and its preloading

Start autoplay only after the page has loaded (or not at all — autoplaying carousels have usability and accessibility issues), so preloading for autoplay never competes with the LCP.

Loading plan for a five-slide hero carousel Layered loading plan showing which slides load at which stage. Loading plan for a five-slide hero carousel Slide 1 In HTML, fetchpriority high, eager Slide 2 loading=lazy or after load event Slides 3-5 data-src, loaded one or two ahead of the active slide Autoplay Starts after load or disabled

Initial image bytes on a six-slide hero Bar chart of image bytes transferred before interaction for three carousel loading strategies. Initial image bytes on a six-slide hero All slides eager 1540KB Library lazy-load (JS) 1540KB Slide 1 priority + defer rest 290KB The library variant transfers the same bytes, only later.

Verification

With cache disabled, reload and confirm only the first slide (and possibly the second) downloads before interaction. The first slide's request should start from the HTML (initiator: document) with High priority. Swipe through and check no blank slides appear. CLS during carousel initialisation should be zero. In RUM, compare LCP and image bytes per page view before and after.

Worked Example: A Fashion Homepage

A fashion retailer's homepage carousel had six 1600px slides, all lazy-loaded by a slider library with JavaScript. LCP p75 on mobile was 4.1s: the slider script loaded at 1.2s, initialised at 1.6s, then requested all six images at once. The team server-rendered slide 1 as an <img fetchpriority="high">, moved slides 3–6 to data-src loaded one ahead, and disabled autoplay. LCP p75 dropped to 2.0s, initial image transfer fell by 1.3MB, and slide engagement did not change — analytics showed fewer than 8% of visitors ever advanced past the first slide.

Data from many sites shows that interaction with slides beyond the first is low. If a carousel exists mainly to give several teams homepage space, a static hero plus a row of cards often performs better on every metric: faster LCP, no layout shifts, no autoplay accessibility issues, and higher click-through on secondary content that is now visible without waiting. When a carousel stays, treat the first slide as the page's hero image and optimise it like one.

Testing Carousels Under Real Conditions

Carousels behave differently on throttled networks and touch devices than on a developer laptop. Test on a mid-range Android phone (or with 4x CPU and Fast 4G throttling) and check three things: the first slide appears without a blank frame, swiping to slide two shows an image rather than a placeholder, and initialisation does not shift the page. Repeat with JavaScript disabled to confirm that the first slide still renders from HTML alone. Add a synthetic check that asserts the LCP element is the first slide image and that it loads with High priority, so a future library upgrade cannot quietly reintroduce lazy loading on slide one.

Common Mistakes

  • Lazy-loading the first slide. The most common carousel LCP regression.
  • Building slides only in JavaScript. Nothing loads until the bundle runs.
  • Same priority for every slide. Hidden slides share bandwidth with the visible one.
  • No dimensions. The carousel collapses and then expands when images load.

Edge Cases

Randomised first slide. If the server picks a random slide, render it server-side; client-side randomisation discards the preload scanner's head start.

Product galleries with thumbnails. Thumbnails are small and can load eagerly; defer only the large images.

Swipe on mobile. Load at least one slide ahead, so a quick swipe does not show an empty frame.

Infinite loop carousels. Cloned slides can duplicate image requests; ensure clones reuse the same URLs (cache hits) and do not use loading="eager" with high priority.

FAQ

Should the first carousel slide use fetchpriority="high"?

Yes, if it is the LCP element. Make sure it is in the HTML, not injected by JavaScript, so the browser can discover it early.

Does native lazy loading work for horizontal carousels?

Partly. Browsers measure distance from the viewport, and slides placed horizontally may fall inside the threshold and load anyway. Explicit deferral gives more control.

Should I preload the first slide?

Usually unnecessary if the <img> is in the server-rendered HTML. Preload helps when the image is set via CSS or chosen by script.

How do I avoid CLS when the carousel library initialises?

Reserve space with aspect-ratio, and make the initial server-rendered markup match the initialised layout, so the library does not change dimensions on mount.

Is autoplay bad for performance?

It encourages loading every slide early and keeps the main thread busy with transitions. It also creates accessibility issues; if used, it needs a pause control.

What about carousels below the fold?

Lazy-load the whole carousel, including the first slide, and consider deferring the carousel script until the component nears the viewport.