Skeleton loader blinks when the data loads too fast

295 Views Asked by At

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.

2

There are 2 best solutions below

13
Jaromanda X On BEST ANSWER

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

11
Phil On

I need to skeleton loader be not visible when the data loads fast. I want it only be visible, when the data loads slowly.

Sounds like you need a timeout on displaying the skeleton instead. Cancel it when the data is loaded.

For example, display the skeleton after a suitable minimum delay, eg 100ms. If the data loads first, don't display it at all.

const showPlaceholder = ref(false);

// ...

onMounted(async () => {
  // wait 100ms to show the placeholder
  const placeholderTimer = setTimeout(() => {
    showPlaceholder.value = true;
  }, 100);

  const img = new Image(); // the constructor only accepts dimensions, not src
  img.src = getSrc('.jpg');
  await img.decode(); // much easier to use than onload
  clearTimeout(placeholderTimer);
  showPlaceholder.value = false;
  isLoaded.value = true;
});

Then use the showPlaceholder ref value to conditionally render the skeleton.