So this is browser related, it occurs in Chrome, but not in Firefox. I think I found a good solution, based on the recommended approach from this answer (it deserves more upvotes btw).
The idea is to turn the gif into a data URL and then randomizing the URL every time the component loads. The advantage here is that the image does not have to be loaded every time, but it still looks like a different image to the browser.
There is one caveat: If the image is hosted on a different server and does not have CORS enabled, this will fail (or need workarounds).
Here is a link to a playground, might be easier to look at it there, but I'll add the code with some comments here for completeness.
It's a composable that will give you a new URL every time its useRandomizedGifDataUrl()
is called. So in your SpinnerLoader component, you just have to call the method and assign it to the src
:
<template>
<img :src="src" class="custom-size" />
</template>
<script setup>
import {useRandomizedGifDataUrl} from './loadingGif.js'
const src = useRandomizedGifDataUrl()
</script>
And here it the composable:
import {ref, watchEffect} from 'vue'
const url = 'https://...'
const dataUrl = ref('')
/**
* Loads an image and turns it into a data URL
* !!! this is sensitive to CORS !!!
*/
const loadImageDataUrl = async () => {
const blob = await fetch(url).then(res => res.blob())
const reader = new FileReader();
reader.readAsDataURL(blob);
const event = await new Promise(resolve => reader.onload = resolve)
return event.currentTarget.result
}
/**
* Provides access to the original dataUrl (empty on first call)
*/
export function useLoadingGifDataUrl(){
if (!dataUrl.value){
loadImageDataUrl().then(imageUrl => {dataUrl.value = imageUrl})
}
return dataUrl
}
/**
* Builds a random data URL based on the original dataUrl by
* adding a random number into the data header.
*/
export function useRandomizedGifDataUrl(){
const dataUrl = useLoadingGifDataUrl()
const randomizedDataUrl = ref()
const head = 'data:image/gif;base64'
const randomize = (url) => !url ? '' : url.replace(head, `${head};${Math.random()}`)
watchEffect(() => randomizedDataUrl.value = randomize(dataUrl.value))
return randomizedDataUrl
}
This can be improved and tailored, for example by avoiding the watchEffect
or adjusting behavior during the first load, but in general, this is how it seems to work.