Caching SVG Sprite in localStorage

There are two ways of using SVG in HTML via <use>: with external source and without it. “The use element takes nodes from within the SVG document, and duplicates them somewhere else” – MDN. In the first case, the SVG graphics insertion (duplication) usually looks like this:

<svg><use xlink:href="sprite.svg#cart"></use></svg>

The good: the file sprite.svg will be cached by the browser. The bad: this technique does not work in IE11 and below (luckily, there is a JS fallback solution); you have to repeat the file name and the path to it on every insertion.

The second case, which is the topic of this post, is about using the locally defined (inlined) SVG graphics. Note that I use <symbol> when defining SVG. This means it is enough to set viewbox once instead of doing this on every SVG insertion. Also, the SVG definitions should be placed above the insertions (<use>), otherwise this will not work in some browsers:

<body>
	<svg style="display: none;">
		<symbol id="svg-cart" viewbox="0 0 50 50"><path d="..." /></symbol>
	</svg>
	<!-- ... -->
	<svg><use xlink:href="#svg-cart"></use></svg>
	<!-- ... -->
</body>

The good: it works in IE9 and above; you do not need to repeat the file name and the path (which is usually pretty long in real-life scenarios) to it on every image insertion:

<!-- this is much nicer -->
<svg><use xlink:href="#svg-cart"></use></svg>

<!-- than this -->
<svg><use xlink:href="theme/something/assets/img/sprite.svg#cart"></use></svg>

The bad: SVG graphics (definitions) is a part of the document and so it is not cached by the browser as a separate subject, which means it is not reusable across the pages.

What if you have a lot of SVG graphics, like 100 KB in size? It means that every page of your website will have an additional 100 KB because idealistically this amount can be cached and reused. For example, if there was a cellular data user who browsed 11 different pages on your website, they would waste 1 MB – this is bad for performance. So, can we cache the SVG sprite and at the same time avoid referencing an external file on every image insertion?

localStorage

Yes! ​localStorage enables web pages to store the data within the user’s browser. The storage limit is usually 5 MB per domain. This is way more than enough for storing SVG sprites.

I wrote a tiny piece of JavaScript code to handle this part (~444 bytes minified and gziped, no dependencies). Here is how it works:

  • On the very first load of the website it:
    1. Reads the contents of a SVG file;
    2. Inserts the data into the document;
    3. Puts the data into localStorage.
  • On every following load it:
    1. Reads the data from localStorage;
    2. Inserts the SVG data in the document.

If localStorage is not supported, is disabled or overfilled, the script still reads the contents of a SVG file and inserts the data in the document.

;(function(window, document) {
	'use strict';

	var file = 'img/svg.html', revision = 1;

	if(!document.createElementNS || !document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect)
		return true;

	var isLocalStorage = 'localStorage' in window && window['localStorage'] !== null,
      request,
      data,
      insertIT = function() {
        document.body.insertAdjacentHTML('afterbegin', data);
      },
      insert = function() {
        if(document.body) insertIT();
        else document.addEventListener('DOMContentLoaded', insertIT);
      };

	if(isLocalStorage && localStorage.getItem('inlineSVGrev') == revision) {
		data = localStorage.getItem('inlineSVGdata');
		if(data) {
			insert();
			return true;
		}
	}

	try {
		request = new XMLHttpRequest();
		request.open('GET', file, true);
		request.onload = function() {
			if(request.status >= 200 && request.status < 400) {
				data = request.responseText;
				insert();
				if(isLocalStorage) {
					localStorage.setItem('inlineSVGdata',	data);
					localStorage.setItem('inlineSVGrev',	revision);
				}
			}
		}
		request.send();
	}
	catch(e){}

}(window, document));

You can safely place this code anywhere in the document as the SVG file scanning works asynchronously. If you put the code at the head of the document, the icons will load a little bit faster. Test it to see what is best for you.

Caching SVG Sprite in localStorage

File & Revision

There are two lines you need to configure on your own:

var file = 'img/svg.html', revision = 1;

file is the path to the SVG file. It can be *.svg file but personally I do not use the sprite file as a SVG file, I treat it as a part of my HTML document. My svg.html usually looks this:

<svg style="display: none;" aria-hidden="true">
	<symbol id="svg-plane" viewbox="0 0 510 510"><path d="..." /></symbol>
	<symbol id="svg-close" viewbox="0 0 357 357"><path d="..." /></symbol>
	<symbol id="svg-fav" viewbox="0 0 510 510"><path d="..." /></symbol>
	<symbol id="svg-share" viewbox="0 0 459 459"><path d="..." /></symbol>
	<symbol id="svg-cart" viewbox="0 0 510 510"><path d="..." /></symbol>
	<symbol id="svg-tick" viewbox="0 0 510 510"><path d="..." /></symbol>
</svg>

revision is a version number of svg.html file. It tells the script when it should scan the file for changes and update the old SVG data which is cached in localStorage to a new one. For example, if you update the SVG file, you also have to change the value of revision variable in order to make the changes visible on the website.

You can do this manually or you can automate this via PHP (Perl, Python or any other server-side scripting language). If you have the JavaScript code placed directly into your HTML code (which is located in *.php file), you can use PHP function which returns file modification time (as a Unix timestamp):

revision = <?=filemtime( 'img/svg.html' )?>;

If you have the script in a separate JavaScript file, i.e. common.js, you can use a global JavaScript variable:

revision = INLINE_SVG_REVISION;
<html>
	<head>
		<script>var INLINE_SVG_REVISION = <?=filemtime( 'img/svg.html' )?>;</script>
	</head>
<body>
<!-- ... -->
<script src="common.js"></script>
</body>
</html>

No SVG support?

If a browser does not support SVG, we can rely on raster images and JavaScript fallback. We need data-img attribute for specifying the location of raster image, for example:

<li><svg><use xlink:href="#svg-cart" data-img="img/cart.png"></use></svg></li>

The fallback will convert that line to this:

<li><img src="img/cart.png" alt="" /></li>

Then, if you do not use html5shiv, you have to register svg and use elements so that the fallback code can later operate them. This should be placed at the document head, best before the first <script> occurence:

if(!document.createElementNS || !document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect) {
	document.createElement('svg');
	document.createElement('use');
}

Finally, the fallback itself – it should be placed at the end of the document (I borrowed some lines from svg4everybody):

;(function(window, document) {
	if(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect) return true;
	var uses = document.getElementsByTagName 'use'), use;
	while((use = uses[0])) {
		var svg = use.parentNode, img = new Image();
		img.src = use.getAttribute('data-img');
		svg.parentNode.replaceChild(img, svg);
	}
}(window, document));

JavaScript disabled?

The worst part about this technique is that you cannot get SVG sprite functioning if JavaScript is disabled in user’s browser. But, like in “no SVG support” case, we can rely on raster images, for example:

<li><svg><use xlink:href="#svg-cart"></use></svg><noscript><img src="img/cart.png" alt="" /></noscript></li>

Demo

I combined everything in the demo. Feel free to investigate the source code of the demo page and use the stuff in your projects where you have lots of SVG icons:

Demo

P.S. In this article I focused only on the technique of caching SVG and I did not show how to make things accessible in code examples. As for real-life projects I encourage you considering these SVG accessibility tips.

&