Search code examples
javascriptvuejs3fetch-apiplaceholder

Skeleton loader blinks when the data loads too fast


I'm using Vue 3 and I try to implement a skeleton loader for a placeholder when the data is loading and I can see it works if I turn on throttling in the Network tab of the browser. However, when the data loads too quick, I can also see how the skeleton loader blinks.

Here I tried using a setTimeout and inside I put the isLoaded ref which value is set to true when the image is fully loaded. But what the delay does is it prolongs time, and I need to skeleton loader be not visible when the data loads fast. I want it only be visible, when the data loads slowly.

onMounted(() => {
    const img = new Image(getSrc('.jpg'));
    img.onload = () => {
        setTimeout(() => {
            isLoaded.value = true;
        }, 300);
    };
    img.src = getSrc('.jpg');
});

Update: This is how I use skeleton loader in the <template>:

<ItemCardSkeleton v-if="pending === false || !isLoaded" />
<template v-else>
    <img
        class="card__image"
        :src="getSrc('.jpg')"
        :alt="recipe.alt"
        width="15.625rem"
        loading="lazy" />
    <div class="card__content">
        <h2 class="card__title">{{ recipe.title }}</h2>
    </div>
</template>

Please, give a solution.


Solution

  • I would leave the skeleton logic alone, just fade it in after some time, avoiding the jump in content if you delay displaying the skeleton (as discussed in @Phil's answer)

    The example below, there's no skeleton visible for 1000ms, then the skeleton will fade in for 1000ms - the times are probably longer than you want, I just wrote it this way to see it actually working - so, just adjust the animation-duration (and animation-timing-function if you want) as appropriate to your needs

    <style scoped>
    @keyframes skeletonAnimate {
        0% {
            opacity:0;
        }
        50% {
            opacity:0;
        }
        100% {
            opacity:1;
        }
    }
    .skeleton {
        opacity:0;
        animation-name: skeletonAnimate;
        animation-duration: 2000ms;
        animation-direction: forward;
        animation-delay: 0;
        animation-iteration-count: 1;
        animation-fill-mode: forwards;
        animation-timing-function: ease-out;
    }
    </style>
    

    Not sure how your skeleton is done, but here's a non-vue demo in action

    @keyframes skeletonAnimate {
      0% {
        opacity: 0;
      }
      50% {
        opacity: 0;
      }
      100% {
        opacity: 1;
      }
    }
    
    .skeleton-content {
      opacity: 0;
      animation-name: skeletonAnimate;
      animation-duration: 2000ms;
      animation-direction: forward;
      animation-delay: 0;
      animation-iteration-count: 1;
      animation-fill-mode: forwards;
      animation-timing-function: ease-out;
    }
    
    @keyframes bgAnimate {
      0% {
        background-position: 50% 0;
      }
      100% {
        background-position: -150% 0;
      }
    }
    
    .skeleton {
      height: 100px;
      width: 200px;
      background-image: linear-gradient( to right, hsla(210, 2%, 54%, 20%) 0%, hsla(210, 4%, 89%, 20%) 10%, hsla(210, 2%, 54%, 20%) 40%, hsla(210, 2%, 54%, 20%) 100%);
      background-repeat: repeat-x;
      background-size: 200% 100%;
      box-shadow: 0 4px 6px -1px hsla(0, 0%, 0%, 0.1), 0 2px 4px -2px hsla(0, 0%, 0%, 0.1);
      animation: bgAnimate 2s linear infinite;
    }
    <div class="skeleton-content">
    <div class="skeleton"></div>
    </div>
    Other content