Article | Posted on November 2015

Optimising SVG load with Service Worker

Reading time: 10 minutes

Topics: WebGL, GLSL, three.js, making of

This is a legacy post, ported from the old system:
Images might be too low quality, code might be outdated, and some links might not work.

Service Workers are a hot topic nowadays, and righly so, since they're an excellent and powerful idea. They're esentially great for caching and enabling a full offline version of a website. But that's just the beginning, there's a lot to explore in having a scriptable network proxy working in your browser. This is an idea for a possible application.

This demo is a proof of concept and will only work on browsers with Service Worker enabled:

Working with SVG

Let's consider for a moment the way we normally work with SVG assets.

SVG assets are awesome, specially in this age of multiple device pixel ratios: one asset for many resolutions, looking crisp at any size. They're lightweight -and can be very lightweight; they have a structure that can be easily read and modified (like it or not, it's easier to modify on the fly an SVG than a JPG); they can enable a lot of trickery to compress a site's size.

They can be used in img, object, background-image, iframe, etc. It's really a useful format, and probably because of that, we end up with many different files, and the page load times are affected from the multiple requests.

Look at this Network log from https://developers.google.com/:

15 requests, and the total download size is under 6KB. There's a few things that can be done to mitigate this.

Creating an SVG "spritesheet": a single file using defs and :target to select specific SVG. Using a spritesheet changes the way we code, and it's usually designed for a very specific uses. It doesn't play well with other ways of loading content, like XMLHttpRequest. There's many different packers but in the end one runs into the same problem: it's not as flexible as the native syntax to load assets.

Icon fonts are a better solution, whether you use Awesome Font or build your own. They still cannot provide all the freedom and flexibility of using assets where and how we please.

The idea

So let's try to find a way that doesn't disrupt or change the way of using SVGs. The goal is to be able to use one file or many, and use them with the original syntax of the different DOM elements and JavaScript objects: we still write img src, object or embed normally. And don't have to worry about the number of requests or latency.

Let's assume you have a /assets/svg/ folder in your project, and you include your assets normally from that location. We'll do it so there's a build step that takes all those different SVGs and packs them together in a single file, creating a node for each individual SVG. Each node has an id attribute with the name of the file. We'll call that file the master SVG file.

Then we'll use ServiceWorker (SW) to:

So in all situations, we end up getting the file.

So let's get started!

You'll need to run a version of Firefox or Chrome with Service Workers enabled. Check "Setting up to play with Service Workers" on Using Service Workers.

The first thing we need to do is register the Service Worker script on our page. This code goes on our index.html:

Installing a Service Worker

JavaScript - index.html

var sw_registration;

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js').then(function(registration) {
    sw_registration = registration;
    // ServiceWorker registration successful
  }).catch(function(err) {
    // ServiceWorker registration failed (err)
  });
}

We'll also add an unregister() method, because even though there's DevTools, it might be useful to have a call to remove everything and start anew:

Uninstalling a Service Worker

JavaScript - index.html

function unregister() {

  if ('serviceWorker' in navigator) {
    sw_registration.unregister().then(function(boolean) {
      // ServiceWorker unregistration successful
    });
  }

}

Both Chrome and Firefox have tools to manage installed Service Workers. Chrome has chrome://inspect/#service-workers and chrome://serviceworker-internals/; Firefox has about:serviceworkers. Aditionally, Chrome DevTools has a Service Worker sidebar on the Sources panel to unregister SW, and a full-fledged SW tab in the Resources panel.

Let's start writing the SW code in another file, service-worker.js.

First step of the SW, specify the files to cache when installing, by adding a caching request on the install event listener. We're going to cache only the index page and the master SVG file:

Caching our files when SW is installed

JavaScript - service-worker.js

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        'index.html',
        'assets/svg/svg-built.svg'
      ]);
    })
  );
});

Let's look at that svg-built.svg file closer. In our assets folder we have several SVG files. Let's look at heart.svg for instance:

Example of the contents of an SVG file

SVG - heart.svg

<?xml version="1.0" ?>
<svg height="24" version="1.1" width="24" xmlns="http://www.w3.org/2000/svg" >
  <g transform="translate(0 -1028.4)">
    <path d="/* a lot of data */" fill="#ecf0f1"/>
  </g>
</svg>

We need a task that takes all those different SVG files and creates a single file with the following structure:

Example of the SVG atlas file we want to create

SVG - master.svg

<?xml version="1.0" ?>
<built>
  <svg id="heart.svg" height="24" version="1.1" width="24" xmlns="http://www.w3.org/2000/svg" >
    <g transform="translate(0 -1028.4)">
      <path d="/* a lot of data */" fill="#ecf0f1"/>
    </g>
  </svg>
  <svg id="sign-error.svg" height="24" version="1.1" width="24" xmlns="http://www.w3.org/2000/svg" >
    <g transform="translate(0 -1028.4)">
      <path d="/* some more data */" fill="#c0392b" transform="translate(0 1029.4)"/>
      <path d="/* and more data */" fill="#e74c3c" transform="translate(0 1028.4)"/>
      <path d="/* and even more data */" fill="#c0392b"/>
      <path d="/* and data */" fill="#ecf0f1"/>
    </g>
  </svg>
</built>

I'm writing this master file by hand for this demo. In production, it would be a node or python script.

As you can see, it contains the same original files, with the xml header removed, and inserted into a built root node. Each node has an extra attribute id to specify the name of the original file. This doesn't look like a valid SVG, but it renders!

Here's the file: SVG master file.

Then, we implement the fetch event listener:

Basic web page

HTML - index.html

this.addEventListener('fetch', function(event) {

  // the request URL is to a file with SVG extension

  if (/.svg$/.test( event.request.url ) ) {

    console.log( 'Trying to fetch an SVG: ', event.request.url )

    // we'll respond the fetch request with the fullfilment of the SVGPromise

    event.respondWith(

      SVGPromise( event ).then( function( r ) {

        // successfully retrieved the SVG file from master file in cache

        return r;

      } ).catch( function( e ) {

        // couldn't find the file in the master file

        console.log( 'ERROR: ', e );

        // fetch from server normally

        return fetch( event.request.url );

      } )

    );

  }

});

As you probably are aware, SW is heavily based on Promises. The event object has to be responded with a Promise. In this case, we create an SVGPromise that will take care of the logic of resolving where to get that file from.

Let's take a look now at what SVGPromise does:

Basic web page

HTML - index.html

function SVGPromise( event  ) {

  // we create the Promise object that will get returned

  var promise = new Promise( function(resolve, reject) {

    // retrieve from cache the master svg file

    var r = new Request( 'assets/svg/svg-built.svg' );

    caches.match( r ).then( function( response ) {

      return response.text();

    } ).then( function( svgMaster ) {

      // master file was successfully retrieved from cache
      // svgMaster is the text content of the file

      // extract the name of the file from the request
      // i.e. http://www.domain.com/folder/assets/file.svg to file.svg

      var id = matchInString( /.*/(.*)/gmi, event.request.url )[ 1 ];

      // try to extract the svg tag with the id attribute with the name

      var regExp = new RegExp( '(<svg id="' + id + '".*</svg>)', 'gi' );
      var res = matchInString( regExp, svgMaster );

      if( res ) {

        // we found it, we can return the node as am SVG file
        // check what happens in SVGPromise.then

        var code = res[ 0 ];
        var svgResponse = new Response(
          code,
          { headers: { 'Content-Type': 'image/svg+xml' } }
        );
        resolve( svgResponse );

      } else {

        // we didn't find it, the promise is rejected
        // check what happens in the SVGPromise.catch

        reject( id + ' is not on master' );

      }

    } );

  } );

  return promise;

}

matchInString() matches the regular expression with the string provided, and returns the first match. In the case to extract the id from the request URL, we know that there's going to be a file, although it wouldn't hurt to add more error checking. In the second use, it might or might not find a match for the required SVG

You might be wondering why all this messing with regular expressions. I first tried to use DOMParser to easily traverse and modify the XML, but alas, it's a DOM API and it's not available on Service Workers!

To be able to tell if the code is working, we're going to modify further the returned SVG, by adding a green dot to it. To do that, let's find the closing tag for the svg, and use a regular expression to append the dot:

Basic web page

HTML - index.html

var code = res[ 0 ].replace( /</svg>/gmi, '<circle cx=3 cy=3 r=3 fill=#00ff00 /></svg>' );

All set

Run the Demo site now

The first time the site runs, it will install the Service Worker, so it won't be ready right away: none of the images will have a green dot. The next reload, the SW should be installed and running, and the images should have the green dot.

If you run the demo now, you should see something like this:

The green dots indicate that the SVG has been successfully processed by the SW in the manner we expect. All of the files are in the master, except the heart.

You can try run it on Android. Chrome stable for Android has support for Service Worker!

If you get the demo running, and see the green dots, check the Network panel (throttling like a 3G network):

The requests are still there, but notice that all of them are marked (from ServiceWorker), but only one has actually network time. That means that the only one that has hit the network as been heart.svg, and the rest have been retrieved from the master file.

Also, if you got it running, open a new tab to the star asset and notice that it has the green dot: that means it's been processed by the Service Worker. Try closing the original tab, and reload, and the green dot is still there: it works now for all the requests in that scope!

Final thoughts

Interestingly, at the time of writing this, Firefox doesn't seem to process Object the same way as the other elements.

The way this technique works, you can develop by adding SVG files where needed, without having to worry about anything. You don't even have to build the master SVG file every time a new SVG is added to the project: if the SW cannot find it in the cached version, it will fetch it directly, so everything will still work.

It has also the advantage that you don't have to keep track of what SVG files need to be kept track for caching in the cache step of the SW: it's just one file.

In the case that there were different icons for desktop and mobile, it would be as simple as caching two different master files, and request from one or the other depending on the platform: the name and path of the files themselves would remain the same in the code.

If there's no SW support, it just works as usual.

One of the disadvantages is that since it's a single file, if there's an update to a single file, the whole master file has to be updated. This might not be a big deal if there are few updates to that file. If there are, there could be a system on the Service Worker to only fetch the modified files and rebuild the local cached master file.

And, of course, there's the big question of what's the point, since you can download once all separate files and then serve them from the Service Worker cache. Fair point, and the only real argument I can think of is that once the SW is running, it's only one file to download.

So here you have it, a simple but powerful way of optimising SVG load, that doesn't get in the way of how you code your page.

All suggestions, comments and pull requests are welcome!

This of course can be applied to many other file formats -like for instance, binaries like mp3-, and improve how assets are packed, downloaded and expanded. It's going to be fun seeing all the things everyone will be coming up with!

(Diagrams made in SVG with draw.io)

Comments