The image carousel at the very top of the home page is often seen as a compromise: every department gets its slide, and the page looks lively. For load time it is rarely a compromise and usually the most expensive component above the fold. The carousel carries the largest image on the page, it is frequently assembled by script, it moves without any action from the visitor and it brings a library that works on the main thread. A single component therefore touches all three Core Web Vitals at once. This article treats the slider as an audit object of its own: which slide the browser counts as the Largest Contentful Paint, why the first slide belongs in the HTML, why autoplay transitions are counted differently from click transitions, what accessibility requires and how a static hero image or Scroll Snap reaches the same goal with less effort.
Key takeaways
- On 76 percent (HTTP Archive Web Almanac 2025) of mobile pages the LCP element is an image. If a carousel sits at the top, it is usually its first slide - and that slide decides the value.
- The first slide belongs in the HTML as an
<img>, withoutloading="lazy"and withfetchpriority="high". web.dev calls loading carousel content via script probably the single biggest carousel performance mistake. - All slides need the same intrinsic size and fixed dimensions. Otherwise a later, larger slide becomes the LCP element during autoplay and the measuring point moves backwards.
- Animate transitions only with
transform, not withleftorwidth. After a click, shifts fall into the grace period of 500 milliseconds (web.dev); with autoplay they count in full. - Under success criterion 2.2.2, auto-advancing slides need a visible pause button as soon as the motion lasts longer than five seconds (W3C WCAG 2.2). Pausing on hover alone is not enough.
- A single hero image is often enough. If you need several slides, build them with CSS Scroll Snap without a library and then check LCP, CLS and INP in the field at the 75th percentile (web.dev).
Why the carousel is so often the LCP element
Largest Contentful Paint measures when the largest visible text or image element in the initial viewport has been painted. A value of 2.5 seconds (web.dev) or less counts as good, measured at the 75th percentile (web.dev) of page loads, segmented across mobile and desktop. A hero carousel is an obvious candidate for this measurement: it usually spans the full width, carries a large-format photo and sits at the very top. Accordingly, images dominate the field. According to the Web Almanac, an image is the LCP element on 85.3 percent (HTTP Archive Web Almanac 2025) of desktop pages and on 76 percent (HTTP Archive Web Almanac 2025) of mobile pages. At the same time, only 62 percent (HTTP Archive Web Almanac 2025) of websites reach a good LCP on smartphones. Anyone who wants to improve their Core Web Vitals on a home page with a slider will therefore usually end up at the carousel.
Which slide is counted depends on how the carousel behaves. While the page loads, the browser keeps reporting new candidates whenever a larger element is painted, and it stops doing so at the first input - a tap, a scroll or a keypress. For a carousel that stands still until someone clicks, only the first slide is therefore a possible candidate. With autoplay, by contrast, any slide can become the LCP element if it appears before the first input and is larger than its predecessors; with equally sized images it stays with the one painted first. Since Chrome 88 (web.dev), images that are later removed from the DOM also count. A carousel that swaps out the old slide when it advances therefore no longer loses its early candidate - before that, every removal triggered a new LCP entry.
Visible or intrinsic size?
The first slide belongs in the HTML
Many carousel libraries build their slides from a data list or from placeholders with data-src once their script has been loaded, parsed and executed. For LCP this is the least favourable order: the image cannot be requested until the script has run. web.dev calls using JavaScript to initiate the loading of carousel content probably the single biggest performance mistake to avoid with carousels. The reason lies in the browser's preload scanner: it searches the HTML for resources before the document has been fully processed, but it only finds what is written there as the src or srcset of an image. An image behind data-src, a background image from a stylesheet or a slide injected by a script remain invisible to it.
The pattern is still widespread. Around 16 to 17 percent (HTTP Archive Web Almanac 2025) of pages lazy-load their LCP image, and on 5.9 percent (HTTP Archive Web Almanac 2025) of mobile pages this happens through a custom approach such as a data-src attribute. Where the delay arises is shown by breaking LCP into four phases: it falls into the resource load delay, before a single byte of the image is on its way. The structure in the following snippet avoids this. The first slide sits completely in the HTML, carries fixed dimensions and a high priority; the following slides have the same dimensions but a low priority. The text sits as HTML on top of the image, not inside it.
<!-- First slide: in the HTML, no lazy loading, high priority -->
<section class="carousel" aria-roledescription="carousel" aria-label="Our services">
<button class="carousel-pause" type="button">Pause carousel</button>
<div class="carousel-track" data-slide="1">
<div class="slide" role="group" aria-roledescription="slide" aria-label="1 of 3">
<img src="/images/hero-1-1600.avif"
srcset="/images/hero-1-800.avif 800w, /images/hero-1-1600.avif 1600w"
sizes="100vw" width="1600" height="640"
fetchpriority="high" alt="Workshop with assembly bench">
<h2 class="slide-text">Assembly from a single source</h2>
</div>
<!-- Following slides: same intrinsic size, low priority -->
<div class="slide" role="group" aria-roledescription="slide" aria-label="2 of 3">
<img src="/images/hero-2-1600.avif"
srcset="/images/hero-2-800.avif 800w, /images/hero-2-1600.avif 1600w"
sizes="100vw" width="1600" height="640"
loading="lazy" fetchpriority="low" alt="Warehouse with shelving system">
<h2 class="slide-text">Delivery from stock</h2>
</div>
<div class="slide" role="group" aria-roledescription="slide" aria-label="3 of 3">
<img src="/images/hero-3-1600.avif"
srcset="/images/hero-3-800.avif 800w, /images/hero-3-1600.avif 1600w"
sizes="100vw" width="1600" height="640"
loading="lazy" fetchpriority="low" alt="Consultation at the counter">
<h2 class="slide-text">Advice on site</h2>
</div>
</div>
</section>Priority matters because of the default behaviour: images start at a low priority and are only boosted once the browser realises after layout that they lie in the viewport. fetchpriority="high" on the first slide skips this wait. In a test by the Chrome team on a flight search page, LCP improved from 2.6 to 1.9 seconds (web.dev). The opposite direction is just as important: following slides often sit just outside the viewport, are considered close enough and would otherwise be boosted as well, even with loading="lazy". web.dev therefore calls fetchpriority="low" the correct solution for them. The attribute is not yet widespread: fetchpriority="high" appears on 17 percent (HTTP Archive Web Almanac 2025) of mobile pages with an LCP image. When boosting pays off and when it takes bandwidth away from other resources is covered in the article on fetchpriority and Priority Hints.
Two traps when loading the slides
loading="lazy" are therefore loaded before they become visible - good against empty slides during a change, but at the cost of extra downloads. The second trap is the background image: if the first slide exists only as a background-image in a stylesheet, the preload scanner does not find it, and the request only starts once the CSS has been loaded and evaluated. The LCP image itself does not belong behind lazy loading; web.dev states this as an explicit warning.The same size for every slide
The second stumbling block lies in the image files. Many carousels are filled over years: the first slide comes from a high-resolution photo shoot, the third from a campaign in a smaller format, the fifth is a crop from a product image. On screen the carousel scales them all to the same area, but for LCP the smaller value of visible and intrinsic size counts. If the files are smaller than the display area, the slide with the largest file wins during autoplay - and it often only comes up second or third. The measuring point thus moves back by one or two slide changes, even though the first slide had loaded quickly long before. web.dev therefore explicitly recommends giving all slides of an autoplaying carousel the same intrinsic size.
Fixed dimensions belong to this as well. If width and height are missing on the image, or there is no reserved area on the container, the layout jumps as soon as the first slide arrives and the content below slides down. According to the Web Almanac, 65 percent (HTTP Archive Web Almanac 2025) of desktop pages set no fixed dimensions on at least one image. For the carousel this means: a uniform image template with a fixed aspect ratio, variants in several widths via srcset and a modern format. How the choice of format reduces file size at the same quality is covered in the article on WebP and AVIF; converting entire image libraries belongs in image optimization.
| Situation in the carousel | Which slide counts as LCP | Effect on the metric |
|---|---|---|
| No autoplay, all slides the same size | the first slide | LCP depends only on the loading path of the first slide |
| Autoplay, all slides the same size | the first slide, as long as no larger one follows | stable if the first slide loads early |
| Autoplay, later slide with a larger file | the larger slide shown later | measuring point moves back by one or more changes |
| First slide as a CSS background | image is discovered late | load delay before the first byte |
| First slide via script or with loading=lazy | the first slide, but requested late | delay by the script's run time or until after layout |
Transitions: transform instead of left and width
How a slide enters the frame decides whether the layout jumps. Many older sliders move a track via left or margin-left, others change the width of the active slide or fade in and out via its height. Each of these properties forces the browser to recalculate the layout, and every change in position of a visible element that results from it counts as a layout shift. web.dev therefore advises using transform: translate() instead of top, right, bottom or left, and transform: scale() instead of width and height. Such animations do not change the layout and do not count as a shift. The Web Almanac shows how common the opposite is: non-composited animations appear on 40 percent (HTTP Archive Web Almanac 2025) of mobile pages and 44 percent (HTTP Archive Web Almanac 2025) of desktop pages.
/* Moves the slide track without touching the layout */
.carousel {
position: relative;
aspect-ratio: 5 / 2; /* space reserved before the image arrives */
overflow: hidden;
}
.carousel-track {
display: flex;
height: 100%;
transition: transform 400ms ease;
}
.carousel-track[data-slide="2"] { transform: translateX(-100%); }
.carousel-track[data-slide="3"] { transform: translateX(-200%); }
.slide { position: relative; flex: 0 0 100%; }
.slide img { width: 100%; height: 100%; object-fit: cover; }
.slide-text { position: absolute; left: 2rem; bottom: 2rem; }
/* Controls occupy their space from the start */
.carousel-pause { position: absolute; z-index: 1; right: 1rem; bottom: 1rem; }
/* Avoid: each of these rules produces layout shifts
.carousel-track { position: relative; left: -100%; transition: left 400ms; }
.slide.active { width: 100%; transition: width 400ms; } */
/* Visitors who asked for less motion get a hard cut */
@media (prefers-reduced-motion: reduce) {
.carousel-track { transition: none; }
}The snippet reserves the area via aspect-ratio before the first image has loaded and afterwards only moves the track. The rule for reduced motion switches off the sliding transition for everyone who has asked their operating system for less motion; MDN describes the media query as the way to detect exactly this setting. Which properties the browser can animate without recalculation and how to track down jank in a performance profile is covered in depth in the article on smooth animations without jank. A second, often overlooked trigger is controls that only appear on mouse hover and take up space when they do. Arrows, dots and the pause button should occupy their space from the start or sit as a layer above the image.
Autoplay: why the 500-millisecond exception does not apply
Cumulative Layout Shift has a grace period for expected movement. Shifts that occur within 500 milliseconds (web.dev) of user input are flagged and excluded from the calculation - anyone who clicks an arrow expects something to move. For carousels this means: in a carousel without autoplay, the shifts of a slide change usually fall within exactly these 500 milliseconds (web.dev) and do not count towards CLS. With autoplay the input is missing. Every change triggered by the timer is unexpected from the metric's point of view, and every shift that an animation via left or width produces along the way goes into the value. A CLS of 0.1 (web.dev) or less counts as good - a limit that a full-width slider with poorly animated transitions can exceed quickly.
web.dev warns about this case in drastic terms: pages with an autoplaying carousel have the potential to cause infinite CLS, and because these shifts are hardly visible to the human eye, they are easy to overlook. Since the metric switched to session windows, this needs some qualification. What counts is no longer the sum of all shifts over the entire lifetime of the page, but the worst session window: a burst of shifts with less than 1 second (web.dev) between each, lasting at most 5 seconds (web.dev) in total. An autoplay carousel therefore no longer adds up towards infinity. Each individual window can still exceed the limit, however, and a sliding animation via left that produces shifts over many frames lands entirely in one window. If your shop is struggling with layout jumps anyway, you will find the other causes in the article on CLS in online shops.
The difference in one sentence
INP: what the slider's script costs
For responsiveness, a carousel is actually a small component. web.dev notes that carousels have minimal JavaScript requirements and therefore should not impact page responsiveness, and recommends replacing the tooling if your own carousel has long-running scripts. In practice it often looks different. Older libraries pull in dependencies, initialise all slides on load, measure widths in loops and attach observers to every image. Any task that takes longer than 50 milliseconds (web.dev) is a long task and holds up every input during that time. If a tap on the arrow falls into such a task, the response waits. An INP of 200 milliseconds (web.dev) or less counts as good.
This becomes especially visible on secondary pages. According to the Web Almanac, only 69 percent (HTTP Archive Web Almanac 2025) of mobile pages reach a good INP there; as a possible cause the analysis names more complex interactions such as filters, carousels and form validation. How input delay, processing time and presentation delay can be separated is described in the article on optimizing INP. How to cap the amount of script permanently is shown in the article on JavaScript budgets, and why many slides present at the same time lengthen every style calculation in the one on DOM size. Three rules have proven themselves for the slider.
Put the library to the test
A slider library that brings another library along as a dependency should be replaced. According to web.dev, newer tools are less likely to need such dependencies, and plain CSS is enough for many carousels.
Build only visible slides
Every slide with image, text and button adds nodes to the DOM. Following slides stay lean; extra content such as videos or forms is only added when the slide is actually navigated to.
Stop the timer when nobody is looking
A timer that keeps running in a background tab or outside the viewport creates work without benefit. visibilitychange and an IntersectionObserver pause the autoplay.
Pausing is mandatory: WCAG 2.2.2
Beyond the metrics there is one requirement that is not an optimisation goal but an obligation. Success criterion 2.2.2 of WCAG requires a mechanism to pause, stop or hide moving content that starts automatically, lasts more than five seconds (W3C WCAG 2.2) and is presented in parallel with other content. The criterion belongs to level A, the baseline level. The W3C's accompanying document explains the threshold: five seconds (W3C WAI) is long enough to get a user's attention, but short enough that a user can wait out the distraction if necessary. An autoplay carousel on an endless loop therefore generally falls under the criterion. One detail matters: motion that starts through an indirect interaction, such as focus, mouse hover or scrolling into view, also counts as starting automatically.
For the implementation, the W3C WAI carousel tutorial describes three building blocks. First, a visible button to pause and resume, whose label changes with its state. Second, a pause while the mouse pointer rests on the carousel or an element inside it has keyboard focus - so keyboard users do not lose their position and links in the slide are easy to click. Third, hiding the slide that is currently changing from assistive technology, so that a screen reader does not offer two slides at once. The following snippet implements this and uses inert for it, which also removes hidden slides from the keyboard order. How such requirements fit into the rest of the frontend belongs in every review of accessibility.
// Autoplay with a pause button, pause on pointer and focus,
// no start with reduced motion, a hidden page or a slider scrolled out of view
const carousel = document.querySelector('.carousel');
const track = carousel.querySelector('.carousel-track');
const button = carousel.querySelector('.carousel-pause');
const slides = [...track.children];
const lessMotion = matchMedia('(prefers-reduced-motion: reduce)');
const state = {
paused: lessMotion.matches, // user preference: do not start at all
inView: false,
pointerOrFocus: false,
};
let current = 1;
let timer = null;
function show(number) {
current = number;
track.dataset.slide = current; // change only via transform (see CSS)
slides.forEach((slide, i) => { slide.inert = i + 1 !== current; });
}
function sync() {
const run = !state.paused && state.inView
&& !state.pointerOrFocus && !document.hidden;
if (run && !timer) {
timer = setInterval(() => show(current % slides.length + 1), 7000);
}
if (!run && timer) {
clearInterval(timer);
timer = null;
}
}
function label() {
button.textContent = state.paused ? 'Play carousel' : 'Pause carousel';
}
button.addEventListener('click', () => {
state.paused = !state.paused;
label();
sync();
});
carousel.addEventListener('mouseenter', () => { state.pointerOrFocus = true; sync(); });
carousel.addEventListener('mouseleave', () => { state.pointerOrFocus = false; sync(); });
carousel.addEventListener('focusin', () => { state.pointerOrFocus = true; sync(); });
carousel.addEventListener('focusout', (e) => {
if (!carousel.contains(e.relatedTarget)) { state.pointerOrFocus = false; sync(); }
});
document.addEventListener('visibilitychange', sync);
new IntersectionObserver(([entry]) => {
state.inView = entry.isIntersecting;
sync();
}).observe(carousel);
show(1);
label();A hover pause does not replace a button
Alternatives: hero image or Scroll Snap
The most effective optimisation is often not to let the carousel run automatically at all. web.dev describes autoplaying carousels as difficult to read because people read at different speeds, and notes that on home pages, where users may only stick around for a second or two, a single image may be similarly effective. A static hero image with one clear message and a row of tiles for the remaining topics below it serves the same purpose: every department gets its place, just not one after another in the same frame. For online shops as well as corporate websites this is often the shortest route to a stable LCP.
If you need several slides, you can do without a library. With CSS Scroll Snap, a horizontally scrollable track becomes a carousel: the browser snaps each slide cleanly into place, swipe gestures work without extra code, and because position changes that stem purely from scrolling do not count as layout shifts, paging through produces no CLS. Since Chrome 135 (Chrome for Developers), ::scroll-button() and ::scroll-marker() from CSS Overflow 5 even add arrows and dots without JavaScript. These pseudo-elements are new and not available in all browsers; they therefore belong as an addition on top of a Scroll Snap foundation that remains fully usable without them.
/* Carousel without a library: Scroll Snap as the foundation */
.carousel-track {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
aspect-ratio: 5 / 2;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.slide {
scroll-snap-align: start;
scroll-snap-stop: always; /* one swipe, one slide */
}
.slide img { width: 100%; height: 100%; object-fit: cover; }
@media (prefers-reduced-motion: reduce) {
.carousel-track { scroll-behavior: auto; }
}
/* Addition for browsers with CSS Overflow 5: arrows and dots without script */
@supports (scroll-marker-group: after) {
.carousel-track { scroll-marker-group: after; }
.carousel-track::scroll-button(left) { content: "<" / "Previous slide"; }
.carousel-track::scroll-button(right) { content: ">" / "Next slide"; }
.slide::scroll-marker { content: "" / attr(data-title); }
}| Variant | LCP | CLS | INP | Pause per WCAG |
|---|---|---|---|---|
| Autoplay slider from a library, slides via script | late, slide only after the script | every change counts if animated via left | library burdens the main thread | button has to be retrofitted |
| Autoplay slider, first slide in the HTML, transform | early, provided all slides are the same size | stable if only transform is animated | low with lean control code | visible button required |
| Carousel without autoplay using Scroll Snap | first slide is the LCP element | paging produces no shift | hardly any script | not needed, nothing moves on its own |
| Static hero image with tiles | one image, discoverable early | stable with fixed dimensions | no script needed | not needed |
A hero carousel is usually a compromise between departments, not a performance feature. Anyone who measures it as a component quickly sees that the first slide does all the work and the others mainly add weight.
Checklist for your own slider
Every rebuild decision starts with measurement. In the field, the LCP entry shows which element was counted; in the lab you can check when the first slide is requested and whether shifts occur during a slide change. It is important to keep both perspectives apart: a lab run without input reflects the autoplay, whereas real visitors tap sooner or later, and from then on no new LCP candidates count. A structured technical analysis combines both views. The following list covers the most common weak spots.
- The first slide is an
<img>withsrcorsrcsetin the delivered HTML, not behinddata-srcand not a CSS background. - The first slide has no
loading="lazy"butfetchpriority="high"; all further slides havefetchpriority="low". - All slides have the same intrinsic size and the same aspect ratio; variants come via
srcset. - Image and container have fixed dimensions or an
aspect-ratio, and the area is reserved before loading. - Transitions run only via
transformandopacity, not vialeft,margin-left,widthorheight. - Arrows, dots and the pause button occupy their space from the start and push nothing aside when they appear.
- Autoplay is off - or there is a visible pause button, a pause on pointer and focus and no start with reduced motion.
- The slide text sits as HTML on top of the image and not inside the image file.
- The slider library brings no further library as a dependency and creates no long tasks at startup.
- The effect has been re-measured in the field: LCP element, LCP, CLS and INP before and after the rebuild, separately for mobile and desktop.
If you would rather not take the slider apart yourself: in frontend optimization we treat the carousel as a component of its own, measure which slide is counted as LCP in the field, and rebuild loading path, transitions and pause control so that the values generally stay stable. The page on Core Web Vitals places this among the other metrics, and image optimization covers the work on the image files themselves. For an initial assessment, a message via the contact page is enough.
Sources and Studies
loading="lazy" is only requested once the layout is settled and loses time as a result. Even so, around 16 to 17 percent (HTTP Archive Web Almanac 2025) of pages load their LCP image this way. The first slide belongs in the HTML as an <img> with fetchpriority="high"; the other slides get fetchpriority="low".transform instead of left or width, you produce no shifts at all and do not depend on the exception.::scroll-button() and ::scroll-marker() add arrows and dots in CSS. Script is mainly still needed for autoplay - and therefore also for its pause button.Related Articles
Video Performance: Keeping Load Times Under Control
Video as an underrated load-time driver: poster as LCP candidate, lean preload, modern codecs and facades for embedded players - so no video slows the page.
A/B Tests Without Flicker: Keeping LCP Under Control
Why anti-flicker layers push out Largest Contentful Paint, how to measure their share of the first paint and which setups carry a test without hiding the page.
Break Down LCP into Four Phases: the Subpart Analysis
Stop optimizing Largest Contentful Paint blindly: measure TTFB, load delay, load time and render delay individually and target the biggest lever deliberately.