nolimits4web / swiper

Most modern mobile touch slider with hardware accelerated transitions
https://swiperjs.com
MIT License
39.55k stars 9.75k forks source link

Swiper CLS (Cumulative Layout Shift) on PageSpeed Insights #4076

Open maxymczech opened 3 years ago

maxymczech commented 3 years ago

This is a (multiple allowed):

What you did

Swiper is working perfectly, thank you very much for working on this project. I am trying to increase PageSpeed performance index, and the Lighthouse testing tool in Chrome reports large CLS (Cumulative Layout Shift) due to Swiper.

Expected Behavior

It would be great to have 0 CLS due to Swiper

Actual Behavior

Reported CLS is 0.874: https://developers.google.com/speed/pagespeed/insights/?url=https%3A%2F%2Fwannacat.org%2F&tab=desktop I have tested it and it is unfortunately due to Swiper. During the initialization, the Swiper container first disappears and then reappears, causing large layout shift. Is there any way to avoid this? Thank you.

norbertorok92 commented 3 years ago

I have the same issue and seems like the problem is with the loop (at least in my case). The layout shift is caused by the slides added before and after your slides on initialisation. As per the Swiper API docs: Also, because of nature of how the loop mode works, it will add duplicated slides.

Falcikas commented 3 years ago

I can also confirm that CLS is caused by loop. Maybe as a solution for a future releases would be adding an option to manually add placeholders for duplicated slides? Before initialization the element would have the width/height set? So it won't cause this problem? Just thinking out loud

maxymczech commented 3 years ago

I can also confirm that CLS is caused by loop

It actually did not occur to me to try turning loop off. Yes, CLS is 0 with option loop: false.

dpw1 commented 3 years ago

If I am not mistaken the issue CLS is when you attempt to click on an item and it moves away. Drastic height changes can probably trigger it, which is what happens with Swiper.

So, with Swiper, before it loads usually all images are stacked on top of each other.

Once it's loaded they are all stacked side by side, causing a sudden abrupt change on layout.

Setting a max height with the size of the first image should fix it, but i haven't tried it yet. If anyone can confirm please let me know, I'll give it a try this weekend.

On Thu, Jan 14, 2021, 09:34 Maksym Shcherban notifications@github.com wrote:

I can also confirm that CLS is caused by loop

It actually did not occur to me to try turning loop off. Yes, CLS is 0 with option loop: false.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/nolimits4web/swiper/issues/4076#issuecomment-760168226, or unsubscribe https://github.com/notifications/unsubscribe-auth/AC6UQSJZ57Q5VNDGTM7QS23SZ3QDPANCNFSM4VHANYSA .

norbertorok92 commented 3 years ago

@dpw1 I checked my carousel and it has max height, so the issue is not caused by that. Also CLS is not only when you try to click on the item and it moves away (ref: https://web.dev/cls/)

Here is an example: https://swiperjs.com/demos/#loop_mode_infinite_loop

I think that the CLS happens when Swiper adds an extra slide in the swiper-wrapper(see example above). Initially swiper-wrapper has 10 slides. Once Swiper initialises it adds one before the first child and one after the last child.

Adding one after the last child is not an issue. The problem is that it adds one before the first one. So the first slide shifts, basically the first becomes the second.

dpw1 commented 3 years ago

When it adds an extra slide it doesn't actually cause a visual disruption, does it?

quoting from the link you have referred to:

"A layout shift occurs any time a visible element changes its position from one rendered frame to the next."

If the extra slides are off screen I'm unsure whether it could be the cause

On Thu, Jan 14, 2021, 10:05 Norbert Torok notifications@github.com wrote:

@dpw1 https://github.com/dpw1 I checked my carousel and it has max height, so the issue is not caused by that. Also CLS is not only when you try to click on the item and it moves away (ref: https://web.dev/cls/)

Here is an example: https://swiperjs.com/demos/#loop_mode_infinite_loop

I think that the CLS happens when Swiper adds an extra slide in the swiper-wrapper(see example above). Initially swiper-wrapper has 10 slides. Once Swiper initialises it adds one before the first child and one after the last child.

Adding one after the last child is not an issue. The problem is that it adds one before the first one. So the first slide shifts, basically the first becomes the second.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nolimits4web/swiper/issues/4076#issuecomment-760184669, or unsubscribe https://github.com/notifications/unsubscribe-auth/AC6UQSJYNTKY6LLFHLW35MLSZ3T2VANCNFSM4VHANYSA .

norbertorok92 commented 3 years ago

Yes that is right, it does not causes a visual disruption... But if I run the performance profiling, under the experience summary I can see this:

image

I just try to find an answer and fix the problem. 🤷‍♂️

Falcikas commented 3 years ago

Its because of the dynamic DOM element. I think any cloned element added to tree causes CLS if there is no place reserved for it.

dpw1 commented 3 years ago

I see. Do you have any potential solutions in mind for it?

Em qui., 14 de jan. de 2021 às 10:31, Ernest F. notifications@github.com escreveu:

Its because of the dynamic DOM element. I think any cloned element added to tree causes CLS if there is no place reserved for it.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nolimits4web/swiper/issues/4076#issuecomment-760197890, or unsubscribe https://github.com/notifications/unsubscribe-auth/AC6UQSJQH7TT3DNZFMV425LSZ3WZLANCNFSM4VHANYSA .

lytesaber commented 3 years ago

I'm experiencing the same issue with Swiper and loop mode enabled. With loop mode disabled my CLS score is 0.0006 and with loop mode enabled the CLS score is 0.298. Any site that has a CLS score of above 0.1 is classified as "needs improvement" and any above 0.25 as "poor" https://web.dev/cls/. Google has outlined that they will start using CLS which is part of Core Web Vitals https://web.dev/vitals/ for search engine rankings so it's quite an important issue to solve.

maxymczech commented 3 years ago

As a temporary solution, turning off loop worked fine for me.

lytesaber commented 3 years ago

Yes, this works as a temp solution however turning off loop isn't really the ideal option. I'm using swiper for promotional banners and product display carousels for e-commerce based sites where having the carousel loop is better UX in my opinion. Once again though it's Google creating additional head aches for web devs to keep up with their "ideal" metrics with the underlying threat of getting penalized in search rankings....

K-ETFreeman commented 3 years ago

Seems like i found a solution

1) set duplicates position to absolute 2) fill their places with ::before/::after pseudo-elems. Use min-width to set width 3) replace duplicate:last-child to the end of wrapper using 'left'

for example, my slides have full-screen width, and i got 4 slides in a loop and this reduced CLS back to 0, although swiper is still working normal: image

whitersun commented 3 years ago

I try and yes, that also get the problem about CLS when using loop, but not only there have CLS when I combine with slidePerView by 3 or more together, that increase my CLS too.

dpw1 commented 3 years ago

@K-ETFreeman Unfortunately this solution is breaking the design on my end.

norbertorok92 commented 3 years ago

The solution doesn't work for me neither because I can have any number of slides from the back-end, I cannot hardcode it as left: 500%, that won't work for 2 slides or 3 slides, only for 4... 🤔

mcometa commented 3 years ago

I can confirm that turning off loop fixes my CLS score. It's not a deal breaker for my site so it's fine.

ciromattia commented 3 years ago

I'm experiencing the same issue and Core Vitals are needed on my end. I noticed that without loop the CLS turns back to 0, but with Autoplay the slider keeps displaying the first slide when it comes to the end, I'm wondering what loop actually does, though.

maxymczech commented 3 years ago

@ciromattia loop allows you to manually loop through slides in a "circular" manner (1 - 2 - 3 - 4 - 5 - 1 - ...)

adekkpl commented 3 years ago

got the same problem, in my situation helped loop: false, and had to change one element in renderBullet function

renderBullet: function (i, c) {
                return '<span class="' + c + '"><span class="swiper-pagination-txt">' + $(this.slides[i+1]).find('.banners-name').html() + '</span></span>';                
            }

changed only one element from i+1 to only i $(this.slides[i])
becouse like was said on this forum, it was generating one more layer which produces CLS. Strange thing is that even when loop is false, my slider is still working properly and looping all images... and no CLS at all :)

AleCiotto commented 3 years ago

I used a similiar @K-ETFreeman solution, in my case I have only one slide per view and full page width. This code works for me, don't increment LCP and content is visible also with javascript disabled (if you managed it before)

#{$slides-wrapper-selector}:not([style*='translate3d']) {
    max-width: 100%;

    #{$duplicated-slide-selector} {
        position: absolute !important;
        left: -9999px;
    }

    &::before {
        content: '';
        min-width: 100%;
        position: relative;
        left: -9999px;
    }

    #{$slide-selector} {
        transform: translateX(-100%);
    }
}

I hope that this workaround can help someone.

gilbertococchi commented 3 years ago

Hi all, I think it may help to know this useful resource where you can find CLS metric change log https://chromium.googlesource.com/chromium/src/+/master/docs/speed/metrics_changelog/cls.md

I am not sure about your examples but there are more fixes coming with Chrome 89 and 90 soon you can already test in Chrome Canary.

kristofferdamborg commented 3 years ago

I'm also seeing this issue and unfortunately turning off loop isn't an option for me. Haven't been able to find any solution that works yet 😞

gilbertococchi commented 3 years ago

@kristofferdamborg did you try your site on Chrome Canary 90 to check whether the issue is still happening with loop on?

nolimits4web commented 3 years ago

Guys, I think it is not really the Swiper issue.

Yes, in loop mode it duplicates and adds slides to DOM, it is necessary and must. But! it doesn't create any visual issues and there is no actual layout shift, and what is more important, there is no UI/UX issues for users.

All mentioned workarounds above can do the trick, but all of them doesn't fix anything except "fix this metric", they don't fix the issue, because there is no issue. Even worse, trying to fix this "metric" with these workarounds can just make user experience worse and break the Swiper.

For me, in this case, issue is in the web-vitals library that treats any, literally, any DOM change as CLS, and adds headache to developers for problem which is not exist

gilbertococchi commented 3 years ago

@nolimits4web Sorry to chime in, I hope you appreciate the feedback.

I don't know your widget in detail but I can confirm that the CLS metric issue I was able to notice here (that is not related with the web-vitals library but the metric implementation in Chromium) should be fixed with Chrome 90.

The fix on the metric has been already committed in Chromium, you can find all the CLS metric change log here: https://chromium.googlesource.com/chromium/src/+/master/docs/speed/metrics_changelog/cls.md

Hope this helps.

nolimits4web commented 3 years ago

@gilbertococchi thanks, just checked and indeed it is fixed Chrome 90.

Screenshot (Chrome 88 vs Chrome 90):

screenshot
mihaon commented 3 years ago

There is my fix for CLS for a full width swiper. Put this CSS before swiper.js initialized:

.swiper-container {
    position: relative !important;
}
.swiper-slide-duplicate:first-child {
    position: absolute !important;
    width: 100% !important; /* seems like you can remove this line but I didn't test without it */
    left: 0 !important;
}
.swiper-wrapper::before {
    content: '' !important;
    min-width: 100% !important;
}
ced--ced commented 3 years ago

Yep, I've been looking everywhere to solve my CLS issue. Changed the loop from true to false and the CLS went right down to 0!

Saying that, @m-haritonov CSS solution does exactly the same while the loop is on true. Nice one! 👍

thaissa commented 3 years ago

The CSS solution didn't work for me. But disabling loop and autoplay initially, and enabling them after the page finished loading did:

$(window).load(function(e) {
    if ( $('.swiper-container').length > 0 ) {
        setTimeout(function(){
            const swiper = document.querySelector('.swiper-container').swiper;
            swiper.loop = true;
            swiper.autoplay.start()
        }, 3000);
    }
});
keithwyland commented 3 years ago

Another potential CSS workaround for this issue: create a Aspect Ratio Box.

An Aspect Ratio Box can set a placeholder that takes up roughly the amount of space that the actual content will when loaded/initialized fully. This can reduce CLS by having consistent height taken up by pieces of content.

Get the aspect ratio of your largest image/item: Height / Width = ratio And use that as a percentage padding for .swiper-container until it's initialized. Note: .swiper-container[class='swiper-container'] will target when swiper is not initialized (the attribute selector is looking for the container with exactly that class value, so when initialized more classes get added and this CSS stops getting applied).

Example: Your image(s) is 1800px by 750px 750px / 1800px = 0.4166666667 % = 41.66666667

.swiper-container[class='swiper-container'] {
  padding-top: 41.66666667%;
  height: 0;
  overflow: hidden;
  position: relative;
}

.swiper-container[class='swiper-container'] .swiper-wrapper {
  position: absolute;
  top: 0;
  left: 0;
}
Falcikas commented 3 years ago

It is fixed in Chrome 90. Confirmed.

PSzczepanski1996 commented 3 years ago

Also it's worth mentioning that LCP scores are also affected. It gives me ~3-4 seconds on google speed page insight score, which is bad.

Falcikas commented 3 years ago

Also it's worth mentioning that LCP scores are also affected. It gives me ~3-4 seconds on google speed page insight score, which is bad.

Only because of Swiper? LCP is on your side. If you load 100 images in one slider, LCP can be 20 seconds, so how it is related to Swiper slider?

PSzczepanski1996 commented 3 years ago

Also it's worth mentioning that LCP scores are also affected. It gives me ~3-4 seconds on google speed page insight score, which is bad.

Only because of Swiper? LCP is on your side. If you load 100 images in one slider, LCP can be 20 seconds, so how it is related to Swiper slider?

So far I was reading about that lately: https://itnext.io/javascript-sliders-will-kill-your-website-performance-5e4925570e2b

And it seems that there are probably faster libraries than SwiperJS (despite it's not described in this link), so there always is a room of improvement.

Falcikas commented 3 years ago

Also it's worth mentioning that LCP scores are also affected. It gives me ~3-4 seconds on google speed page insight score, which is bad.

Only because of Swiper? LCP is on your side. If you load 100 images in one slider, LCP can be 20 seconds, so how it is related to Swiper slider?

So far I was reading about that lately: https://itnext.io/javascript-sliders-will-kill-your-website-performance-5e4925570e2b

And it seems that there are probably faster libraries than SwiperJS (despite it's not described in this link), so there always is a room of improvement.

If you understand what you are doing, you can decrease the LCP as I wrote before - It mainly on your side not slider. The Swiper Demo has 100% in performance https://swiperjs.com/demos/370-lazy-load-images/core.html with less than 0.6s LCP score.

Ofcourse there are faster libraries than SwiperJS but when I see articles in 2020 with OwlCarousel or Slick as examples, it make me smile. Owl is dead for ~2 years, slick is still jQuery dependant.

PSzczepanski1996 commented 3 years ago

Also it's worth mentioning that LCP scores are also affected. It gives me ~3-4 seconds on google speed page insight score, which is bad.

Only because of Swiper? LCP is on your side. If you load 100 images in one slider, LCP can be 20 seconds, so how it is related to Swiper slider?

So far I was reading about that lately: https://itnext.io/javascript-sliders-will-kill-your-website-performance-5e4925570e2b And it seems that there are probably faster libraries than SwiperJS (despite it's not described in this link), so there always is a room of improvement.

If you understand what you are doing, you can decrease the LCP as I wrote before - It mainly on your side not slider. The Swiper Demo has 100% in performance https://swiperjs.com/demos/370-lazy-load-images/core.html with less than 0.6s LCP score.

Ofcourse there are faster libraries than SwiperJS but when I see articles in 2020 with OwlCarousel or Slick as examples, it make me smile. Owl is dead for ~2 years, slick is still jQuery dependant.

Ok, so I pretty much tried most of the things for improving site performance - updating libraries, optimizing database queries using prefetches, using webp depending on page headers, rewriting code from jQuery to VanillaJS and getting rid of dependency (but now I know, that jQuery impact on performance is so small that nobody will notice it - the difference is defined by some float part of milisecond, so, not really any impact). I even use lazyloading at Swiper side. And all I achieved is 83-87 score performance on mobiles.

My website uses bootstrap 3 as template, and I don't know what I can do next. The performance is probably going down because of LCP score, which is 4.1s for really high compressed website. Can you post me an example how to get low LCP?

I have overwritten also init method like this:

on: {
    init: () => {
        document.querySelectorAll('.swiper-pagination-bullets').forEach((el) => {
            el.parentNode.removeChild(el);
        });
    }
}

For removing swiper pagination bullets.

Rest of my config looks like this:

new Swiper(sliderClass, {
    preloadImages: false,
    lazy: true,
    autoplay: {
        delay: 5000,
        disableOnInteraction: false,
    },
    navigation: {
        nextEl: '.home-slider-next',
        prevEl: '.home-slider-prev',
    },
    loop: true,
    on: {
        init: () => {
            document.querySelectorAll('.swiper-pagination-bullets').forEach((el) => {
                el.parentNode.removeChild(el);
            });
        }
    }
})

Thanks for help in advance.

PSzczepanski1996 commented 3 years ago

^ : I gained 1-2 points of performance just using defer and async in correct manner, it seems that the initial page is stuck on loading and Swiper is executing after ~1-1.5 seconds.

Still for some reason LCP score is 4-4.5...

Edit:

Can I also solve somehow this error by changing config:

Preload Largest Contentful Paint image

Hm?

drosendo commented 2 years ago

Hello,

I avoided the CLS to 0 by applying just 1 rule to the parent container of the swiper

.gallery{aspect-ratio: 9 / 16;}

Hope it helps

babakfp commented 2 years ago

How to fix the content shift issue?

/* Add this class to .swiper element */
.swiper-prevent-content-shift {
  --swiper-sidebar-w: 0px;
  --swiper-slidesPerView: 1;
  --swiper-spaceBetween: 16px;
  --swiper-available-width: 100vw - ( var(--page) * 2 ) - var(--swiper-sidebar-w);
  --swiper-SwiperSlide-width: calc(
    (
      var(--swiper-available-width)
      -
      ( ( var(--swiper-slidesPerView) - 1 ) * var(--swiper-spaceBetween) )
    )
    /
    var(--swiper-slidesPerView)
  );

  @screen lg {
    --swiper-sidebar-w: 14rem;
  }

  .swiper-slide {
    width: var(--swiper-SwiperSlide-width) !important;
    /* Only for horizontal swipers (only horizontal swipers need preventing content shift because vertical ones need fixed width and height) */
    margin-left: var(--swiper-spaceBetween);
  }
}

In my swiper component:

<Swiper class="ProductCardSwiper swiper-prevent-content-shift">
  <!-- ... -->
</Swiper>

<style lang="postcss">
  :global(.ProductCardSwiper.swiper-prevent-content-shift) {
    @screen 5xs { --swiper-slidesPerView: 2 }
    @screen xs  { --swiper-slidesPerView: 3 }
    @screen 2md { --swiper-slidesPerView: 4 }
    @screen xl  { --swiper-slidesPerView: 5 }
  }
</style>

I used the code above (both code blocks) for my products, blog posts, gift cards swiper and etc. You can check it out here: https://farsgamer.vercel.app/ You can see the content shift here: https://farsgamer-1svyvc1ck-babakfp.vercel.app/

For the big main slider at the top of the home page, previously I was only using md:w-8/12, now I added md:w-8/12 md:min-w-8/12 md:max-w-8/12.

Look at the blog posts and gift cards to see the content shift happening. Ignore product boxes. In dev tools, you can put the network speed on 3G so you can easily see the content shift. You are going to see gift card boxes getting shifted from 1 per view to a few items per view.

Update:

I also add a few CSS aspect-ratio styles to prevent content shifts even more. In dev tools > elements tab, search for a class called aspect-.


Gtmetrix: Screenshot 2022-06-11 113440

Lighthouse Desktop: Screenshot 2022-06-11 113639

Lighthouse Mobile: Screenshot 2022-06-11 113739

Do this instead

/*
---
Fixing Swiper content shift
---
*/

.swiper-prevent-content-shift {
  --swiper-slidesPerView: 1;
  --swiper-spaceBetween: 16px;
  --swiper-SwiperSlide-width: calc(
    (100%
    - ( var(--swiper-slidesPerView) - 1 )
    * var(--swiper-spaceBetween))
    / var(--swiper-slidesPerView)
  );

  .swiper-slide {
    width: var(--swiper-SwiperSlide-width);
    /* Only horizontal swipers need this because vertical ones have fixed width and height */
    margin-left: var(--swiper-spaceBetween);
  }
}
ryanbjones commented 2 years ago

I have an element within my slide that is positioned to be at the bottom

{
  position: fixed;
  bottom: 0;
}

If I use any top property then it will have CLS at 0, but using bottom it breaks it. Would love any feedback if anyone has any suggestions. I tried babakfp which unfortunately didn't work. Have also tried different aspect ratios but they tend to make it worse. Removing loop helps, but still has a score above 0.2

ryanbjones commented 2 years ago

I have an element within my slide that is positioned to be at the bottom

{
  position: fixed;
  bottom: 0;
}

If I use any top property then it will have CLS at 0, but using bottom it breaks it. Would love any feedback if anyone has any suggestions. I tried babakfp which unfortunately didn't work. Have also tried different aspect ratios but they tend to make it worse. Removing loop helps, but still has a score above 0.2

Found a solution to ours -- adding height: 100vh to the swiper container

crossz commented 1 year ago

now, 16th May 2023, layout shift issue still exists, but narrow down to those prev and next navigation button.

Lighthouse reported:

div#app > div.swiper > div.swiper-button-prev > ::after
<::after>

div#app > div.swiper > div.swiper-button-next > ::after
<::after>
jaromiro commented 9 months ago

I had a big problem with CLS on a site where there are several swiper components in use. ( https://strippoker.app/ ) The solution was to add a class to the images displayed in the components that defines their size for specific resolutions. Everyone needs to adapt a similar patch of their website appearance.

.image_presetsize {
  aspect-ratio: 640/427;
  @media (min-width: 1920px) and (max-width: 2560px) {
      width: 19.33vw;
      padding: 0 2px;
  }
  @media (min-width: 1200px) and (max-width: 1919px) {
      width: 23.94vw;
      padding: 0 2px;
  }
}

etc. for each resolution (number of photos in a row) CLS after this operation is 0.

modbender commented 6 months ago

I have number of slides per view set to 5 and seems slider at first renders only 1 slide which ends up being full width of slider then it changes to 5 slides. This causes CLS increase.

Switching off loop did nothing, seems I have to find a different slider.

thiagodomene commented 5 months ago

I developed a solution for a banner with 3 sliders using SwiperJS that was showing a CLS of 0.664.

  1. Set the non-visible sliders to display:none at the start:
    .slide:not(.swiper-slide-active) {
    display: none;
    }
  2. Inside the Swiper initialization, put a timeout on the init event with an arrow function to capture the this context, and finally change the style to display the inactive sliders again:
    // Starts Swiper JS
    on: {
    init: function () {
        setTimeout(() => {
            const slides = document.querySelectorAll('.swiper-slide');
            slides.forEach(function (slide) {
                slide.style.display = 'block';
            });
            this.update(); // 'this' refers to the Swiper instance
        }, 10);
    }
    }

    With this solution, I achieved a CLS of 0.001 and it went back to performing at > 90/100 performance :)

waseemanwar1 commented 5 months ago

@thiagodomene Your solution has indeed provided a slight enhancement to the performance of the sliders using SwiperJS on my website. However, the abundance of images within the slider is still leading to performance issues on the page.

alexRicc2 commented 2 months ago

@modbender I'm having the same issue, did you find any solution?

opicron commented 1 month ago

This fixed it for me:

.swiper-main .swiper-slide:not(:first-child) {
                opacity:0;
                width:1px;
}