jQuery Image Cache Plugin – Cache Images In Browser’s Local Storage

I got an interesting idea from a friend of mine – “what if we could store some site small frequent used images in client’s Local Storage to minimize site requests?”. Hm, seemed like a good plan for a jQuery plugin :) .

There are 3 types of storage in modern browsers – Session Storage, Local Storage and Database Storage (not fully implemented yet). Read more about them here. The one we need is Local Storage because it has some cool futures:

  1. Enough quota (usually 5MB, depends on the browser)
  2. Data is shared between tabs and windows (Session Storage doesn’t do this)
  3. Increased security (no cross domain data sharing)
  4. Easy to use (key/value pairs)

Our plugin should do the following:

  1. Process all the desired images tags on page load (on DOM ready).
  2. Check Local Storage for each image to see if we have a base64 encoded image version in there.
  3. If we found the encoded version – replace the image’s src attribute with the base64 code.
  4. If the image is not in the storage yet – display the original image, do an Ajax call to the server to get the base64 encoded image and  store it in the Local Storage for later.

The plugin is not so complicated. First we should check if the browser supports Local Storage to store our images. If not – show the original images.

;(function ($) {
	$.fn.imageCache = function (options) {
		this.config = {
			// Specify the remote path where you request the encoded images
			base64ImageEncoderPath: 'get_image.php?id=',
			canvasEncoder: true // Experimental
		};
		$.extend(this.config, options);
		
		// Check for canvas support
		this.config.canvasEncoder = typeof HTMLCanvasElement != undefined ? this.config.canvasEncoder : false;

		var self = this;

		// Special canvas encoding function
		var getBase64Image = function (img) {
			try {
				var canvas = document.createElement('canvas');
				canvas.width = img.width;
				canvas.height = img.height;

				var ctx = canvas.getContext('2d');
				ctx.drawImage(img, 0, 0);
				
				var imgType = img.src.match(/(jpg|jpeg|png)/);
				if (imgType.length) {
					imgType = imgType[0] == 'jpg' ? 'jpeg' : imgType[0];
				} else {
					throw 'Invalid image type for canvas encoder.';
				}console.log(imgType,canvas.toDataURL('image/' + imgType));

				return canvas.toDataURL('image/' + imgType);
			} catch (e) {
				console && console.log(e);
				return 'error';
			}
		}

		// Scan the desired DOM elements
		$(document).ready(function () {
			$(self).each(function (i, img) {
				var $img = $(img);
				var src = $img.attr('src') || $img.attr('data-src');

				// Check for Local Storage support
				if (localStorage) {
					var localSrc = localStorage[src];
					// Check if the image is already cached
					// without errors
					if (localSrc && /^data:image/.test(localSrc)) {
						$img.attr('src', localSrc);
					} else {
						$img.attr('src', src);

						// If the image is not stored yet - load from src and store it with ajax
						// or encode it with canvas
						if (localStorage[src] !== 'pending') {
							localStorage[src] = 'pending';
							if (self.config.canvasEncoder) {
								localStorage[src] = getBase64Image(img);
							} else {
								$.ajax({
									url: self.config.base64ImageEncoderPath + src,
									success: function (data) {
										localStorage[src] = data;
									},
									error: function () {
										localStorage[src] = 'error';
									}
								});
							}							
						}
					}
				}
			});
		});
		
		return this;
	}
})(jQuery);

The remote script example runs on PHP:

        // Store the keys of the images to be converted by path
	$encodedImages = array(
		'assets/test-image-1.jpg',
		'assets/test-image-2.jpg',
		'assets/test-image-4.jpg',
		'assets/test-image-5.jpg',
	);

        // Get the id param from the request (that is the image src attribute) and verify if it exists in the array above
	if (isset($_GET['id']) && in_array($_GET['id'], $encodedImages)) {
                // Read the image content, encode it and display
		echo 'data:image/jpg;base64,' . base64_encode(file_get_contents(realpath( './' . $_GET['id'])));
	}

The implementation is pretty easy:


<ul>
    <li><img src="assets/test-image-1.jpg" class="imageCache" /></li>
    <li><img src="assets/test-image-2.jpg" class="imageCache" /></li>
</ul>

<ul>
    <li><img src="" data-src="assets/test-image-4.jpg" class="imageCache" /></li>
    <li><img src="" data-src="assets/test-image-5.jpg" class="imageCache" /></li>
</ul>

<script type="text/javascript">
    // Get images to be cached by class name
    $('img.imageCache').imageCache();
</script>

Didn’t tried yet the plugin on a mass production application but it should be ok.

If you have ideas how to improve the plugin – please leave a comment.

UPDATE:

If you want to avoid any request to the server you can render the base64 code in your html source:

<head>
    <script type="text/javascript">
        localStorage['assets/test-image-4.jpg'] = '<?php echo 'data:image/jpg;base64,' . base64_encode(file_get_contents(realpath( './assets/test-image-4.jpg')));?>';
    </script>
</head>

UPDATE:

Just implemented the request error scenario for loading images. Many thanks to Kevin for spotting the bug and.

UPDATE:

Trying to get the canvas base64 encoding work on modern browsers to avoid any remote request. Special thanks to Nathan for the idea.

Please support this free plugin developement and

42 Responses to jQuery Image Cache Plugin – Cache Images In Browser’s Local Storage

  1. Kevin says:

    Hey there! really nice plugin. I am working on a image heavy website and I will give this a try and let you know how it goes.

    • dumitru says:

      Cool! Let me know if the plugin does the job for you.

      I will have to improve it a little bit to remove the SRC attribute of the image from start and to attache it on load because some browsers still send some requests to the server to see if the image is not updated.

  2. Kevin says:

    Hi

    Looks interesting.

    Quick question: I cannot quite see what would prevent the browser automatically attempting to download the first two images (or load them from its own cache) regardless of whether or not there is offline storage. Am I missing something?

    Cheers

    Kevin

    • dumitru says:

      Yes, indeed, IMG tag with a SRC attribute still gets loaded by latest browsers… I think that’s the reason why lazy image loading plugins don’t work anymore: http://www.appelsiini.net/projects/lazyload :(

      The best way for this plugin to work is to let the SRC attribute empty and use the data-src (as for images 3 and 4). Use it for images that are not important for SEO ranking. Usually I use it for small thumbnails, placeholders and dynamic loaded content.

      If you figured out a way how to stop the image loading with a SRC attribute, please fork on GitHub and refactor the plugin.

      Thanks for your feedback!

  3. Kevin says:

    Strangely, in IE8, I only see a partial image for each one. Seems OK in other browsers though.

  4. Kevin says:

    Another question: What are the advantages over the standard image caching that the browser does?

    • dumitru says:

      Well, the main thing I wanted to achieve with this plugin was to minimize the requests to the server. Even if you have the image cached, the browser sends a request to the server to see if the image wasn’t updated. Loading it from Local Storage should prevent this.

      Also loading it from Local Storage seems to be faster than from cache (because of that request).

  5. Kevin says:

    Hello

    I have found one problem that I have not fathomed out yet (although I haven’t looked hard yet).

    If I have, say, 25 identical images on the page and run .imageCache() against each one, I get 25 requests going to the server. Of course the next time I load the page I get zero requests to the server as expected.

    What is puzzling me is why I get 25 hits the first time. The requests are asynchronous of course, but I would expect only a few requests to the server, which will store the image in the localStorage so that the remainder of the imageCache() calls do not have to make a request to the server.

    Can you replicate this?

    • dumitru says:

      Indeed, that’s a bug. Just solved it. You should not get multiple requests for the same image anymore.

      Thanks a lot for the bug reporting and tell me please if it works properly for you now.

      • Kevin says:

        Yes that is better thanks.

        Now I am comparing to a setup that just relies on the normal browser cache. My test shows that the browser is not going back to the server at all for images that it has cached (i.e. not even to check if it has changed). So the plug-in is working, but I am not sure (yet) if it is gaining me anything.

        My Apache server has these directives, which might explain why my browser never tries to reload or check cached images until you force it to with a hard refresh:

        ExpiresActive On
        ExpiresByType image/jpg “access plus 1 months”
        ExpiresByType image/jpeg “access plus 1 months”
        ExpiresByType image/gif “access plus 1 months”
        ExpiresByType image/png “access plus 1 months”
        ExpiresDefault “access plus 1 days”

        • dumitru says:

          Yes, usually I also set this cacheing directives:

          <IfModule mod_header.c>
          <FilesMatch “\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$”>
          Header set Cache-Control “public”
          Header set Expires “Thu, 15 Apr 2011 20:00:00 GMT”
          </FilesMatch>
          </IfModule>

          Browsers do a request anyway if user hits a hard browser refresh (i.e. CTR+R in Firefox). Storing data in localStorage solves this requests.

          You can also render the bse64 code while rendering the page so you don’t have to do any ajax requests to get the images.

  6. Nathan says:

    Have you given any thought to doing the base64 encoding on the client side rather than the server side? This way the image is loaded normally, then the client itself will encode it. See http://stackoverflow.com/questions/934012/get-image-data-in-javascript.

    Of course you’d have to first detect if the browser supports canvas to do what was suggested in the link, but this would work when you are using a CDN (Amazon Cloundfront for me) and would be completely independent of the backend (ASP, PHP, etc).

    • dumitru says:

      Thanks for the suggestion!

      It’s a good way to avoid the ajax requests on modern browsers. I will implement it ASAP.

  7. Kevin says:

    It is possible for all the image caches to be stuck at “pending” (I have just that situation now). This means that the images are never retrieved and cached, although they do display OK because it just swaps “dta-src” for “src”. I am not sure how I arrived at this scenario, but clearly each request for the image must have failed for some reason.

    This also highlights the need for a function to clear out the image cache without impacting other localStorage, thus enabling the image download to take place the next time through.

    • dumitru says:

      Yes, indeed, I have to support the error scenarios and reload the images. Another task for me.

      Thanks for the bug reporting!

      • Kevin says:

        FWIW, here is my attempt to clear the cache:
        // Clear images from local storage
        if ($.support.localStorage) {
        // $.each crashes interating localStorage?!
        var cache=[];
        for (var v=0;v=10 || localStorage[key]==’pending’)) {
        if (localStorage[key]==’pending’)
        cache.push(key);
        else if (localStorage[key].substr(0,10)==’data:image’)
        cache.push(key);
        }
        }
        $.each(cache,function(){
        delete localStorage[this]
        })

        }

        • Kevin says:

          Dunno what happened there. Most of the code I cut and paste in is missing! I will try again

          // Clear images from local storage
          if ($.support.localStorage) {
          // $.each crashes interating localStorage?!
          var cache=[];
          for (var v=0;v=10 || localStorage[key]=='pending')) {
          if (localStorage[key]=='pending')
          cache.push(key);
          else if (localStorage[key].substr(0,10)=='data:image')
          cache.push(key);
          }
          }
          $.each(cache,function(){

          delete localStorage[this]
          })
          }

  8. FvG says:

    Hey,
    I want to use this for my slider. I’m using this plugin (https://github.com/desandro/imagesloaded) to wait until all images are loaded. If they are I slideDown my div. But I only need this if the images aren’t cached. With your plugin I can cache them but then I have to find out whether they are cached or not. How can I do this? Thx

    • dumitru says:

      Hi,

      Normally you can check the localStorage from Javascript to see if the key with the image SRC attribute exists and it’s value has the “data:image” string in it.
      For example in the demo I can do this:

      if (localStorage && localStorage["assets/test-image-1.jpg"] && /^data:image.*/.test(localStorage["assets/test-image-1.jpg"])) {
      // slide the div here
      }

      I hope this solves your issue.

  9. Gowrishankar says:

    Does your image caching plugin will work in interner explorer

  10. Gowrishankar says:

    Your image caching plugin will work in Internet explorer ????

  11. artmem says:

    Could this be adapted to work with indexeddb?

  12. Túlio says:

    Is it possible to cache background images (CSS)?

    • dumitru says:

      Yes…you can use a little trick, loading the background images as a normal image and hide it with $(‘img.bk-image’).hide(). Or even better is to keep them in a hidden div in the page footer.

      • Túlio says:

        So i would do it like this?

        #header_bg {background:url(/header_bg.jpg) left top no-repeat;}

        $(‘img.imageCache’).imageCache();

        • Túlio says:

          sorry tags did not show

          #header {background:url(/header_bg.jpg) left top no-repeat;}

          ...

          ...

          $('img.imageCache').imageCache();

      • Túlio says:

        omg! i’m sorry.. your blog really hate comments with html tags..
        lets say that = * then:

        *hmtl*
        *head*
        *style type=”text/css”*
        #header {background:url(/image.jpg);}
        */style*
        */head*

        *div style=”display:none”*
        *img src=”/image.jpg” clas=”imageCache”/*
        */div*


        *script type=”text/javascript”*
        $(‘img.imageCache’).imageCache();
        */script*
        */html*

      • dumitru says:

        You’ll have to access local storage directly and change the background url in CSS on the fly. Almost as you did:

        *hmtl*
        *head*
        */head*

        *div style=”display:none”*
        *img src=”/image.jpg” class=”imageCache” id=”header-background”/*
        */div*


        *script type=”text/javascript”*
        $(‘img.imageCache’).imageCache();
        var headerSrc = $(“#header-background”).attr(“src”);
        if (localStorage[headerSrc] && localStorage[headerSrc] !== “pending”) {
        $(“#header”).css({“background-image”: “url(” + localStorage[headerSrc] + “)”});
        }
        */script*
        */html*

        Sorry, but I don’t have a more elegant solution as the plug-in was not made for this… But this code can be added to the plug-in in the future. If you fill like doing it – please fork on GitHub and I will approve the changes right away.

  13. onur pay says:

    Hi, what if the images are in the parent directory or ../../ directory? Does this situation cause a problem?

  14. hitesh says:

    Hi..
    i want to implement this without PHP (because i am using asp.net)
    for that what i need to do ?

    • dumitru says:

      Sorry for the late response.
      I’m not an expert in asp.net but for sure you don’t have to do anything on the frontend part.
      You’ll have to create a script in asp.net that does the job of the on in PHP – to return the encoded image.

  15. Christophe says:

    Hello, i test your plugin for digital signage.
    I have found an error for your UPDATE “If you want to avoid any request to the server you can render the base64 code in your html source”

    localStorage['file_with_relative_path'] : quotes are missing. Doesn’t work without quotes.

    And you have forgotten the file_get_contents next base64_encode func.

  16. rm says:

    Couple of questions:
    I assume the JS script is not needed for the PHP solution?
    Will the the PHP solution work for css background images?
    Does the PHP solution work on all image types?
    Is this solution just for images?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Switch to our mobile site