Search code examples
javascriptimagegoogle-chromebrowser-cachehttp-caching

Determine if an image is cached in browser (either memory or disk cache) before displaying the image?


For lazy-loading visualization, I'm attempting to apply a fade-in animation style class only to images that have not been cached in the browser.

To determine if an image was previously loaded and cached in the browser, we can employ the HTMLImageElement.complete property in a function like this:

const isImageCached = (imageSource) => {

    let image = new Image();
    image.src = imageSource;

    const isCached = image.complete;
    image.src = "";
    image = null;
    
    return isCached;
};

However, in Chrome, HTTP Cache items are either located in memory cache or disk cache. The above function only works if the image is in memory cache and will always return false if the image is in disk cache. I assume this is because memory is accessed immediately at 0 milliseconds while the disk cache can take up to several milliseconds to be retrieved.

Code Test (JSFiddle)

When you first open and run the above script the image fades in since it hasn't yet been cached in the browser. When you re-run the script by pressing the "Run" button, the image doesn't fade in because it is now cached in Chrome's memory cache. The problem occurs if the memory cache has been cleared. The image has already been cached but now it may be located in the disk cache so the image will fade in.

Below is a screen capture of Chrome Developer Tools detailing the output when the page is refreshed (disk cache) and then the script is re-run without a page refresh (memory cache):

Chrome DevTools Screen Capture

Is it possible to determine in JavaScript if an image is located in the HTTP Cache, either memory cache or disk cache, before displaying the image?

const image = document.querySelector("img");
const marioSrc = "https://www.pinclipart.com/picdir/big/361-3619269_mario-16-bit-mario-bros-super-mario-world.png";

const isImageCached = (imageSource) => {

    let image = new Image();
    image.src = imageSource;

    const result = image.complete;
    image.src = "";
    image = null;
    
    return result;
};

if (!isImageCached(marioSrc)) {
    image.classList.add("image-fade-in");
}

image.setAttribute("src", marioSrc);
:root {
    margin: 0;
    padding: 0;
}

body {
    background-color: #444444;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow-y: hidden;
}

@keyframes fade-in {

    0%   { opacity: 0; }
    100% { opacity: 1; }
}

.image-fade-in {
    animation: fade-in 1s ease-in-out;
}
<img />


Solution

  • Setting a specific wait time to determine if an image is in the browser disk cache is required, and while most of the time it's a very small wait time (<5 milliseconds), the value can fluctuate dramatically depending on the event loop stack and hardware.

    This can be accomplished by racing promises: a loading image vs. a timeout.

    For my specific use-case, if an image loads in under 25ms I can assume it's cached, otherwise I'll assume it has never been loaded and apply the fade-in style.

    const promiseTimeout = (ms, promise, timeoutMessage = null) => {
    
        let timerID;
    
        const timer = new Promise((resolve, reject) => {
    
            timerID = setTimeout(() => reject(timeoutMessage), ms);
        });
    
        return Promise
            .race([ promise, timer ])
            .then((result) => {
    
                clearTimeout(timerID);
    
                return result;
            });
    };
    
    const imageURL = "https://dummyimage.com/600x400/000/fff.png&text=a";
    let image = new Image();
        
    const imageComplete = new Promise((resolve, reject) => {
    
        image.onload = () => resolve(image);
        image.onError = () => reject(image);
        
        image.src = imageURL;
    });
    
    promiseTimeout(25, imageComplete, "Not loaded from cache")
        .then((result) => console.log("Loaded from cache:", result))
        .catch((error) => {
            
            image.src = "";
            image = null;
            
            console.log(error);
        });