Your Site's Static Images Are Source Files Too!

nolan

Posted by nolan

Load a page from a recent site you've built. Take a look at the payload of any given response from a request to your static files—most notably a Javascript or CSS file of your own authoring. Does the string of characters you see exactly represent the content you typed into an editor? Probably not, I'd wager. It's rare, on a site where performance matters (all sites!), to serve static files without an online or offline build step in place to compile, concatenate, or otherwise optimize resources.

To limit requests and cut file weight, JavaScript files are concatenated, stripped of unnecessary whitespace, and even obfuscated—identifier names within replaced with more terse, one-or-two-character stand-ins. CSS files are given much the same treatment, likely with additional build steps such as automatically including vendor prefixes with a tool such as autoprefixer.

The point is that ideally, static files are compiled files. The runtime environment (your browser) will happily interpret your uncompressed scripts, and stylesheets, of course, but if you aren't running them through a compilation build step, you're missing a prime opportunity to squeeze wholly unneeded bytes from your page load.

With this in mind, it's kind of a wonder that I overlooked giving my sites' image media the same treatment for so long. By weight, these files often dwarf their JS/CSS brethren and represent an untapped area prime for optimization.

I propose you treat your static images similar to your static scripts and stylesheets; version track them in their "source" form and serve their compiled analogues. What do I mean by "source"? That can vary, but again, you can think of it in the same way as you do your scripts. When you need to make a change to a script, you dip into your IDE, edit, and tidy up the file a bit, then you save it. Either you or your back-end compiles that down to the version your site will send along in response to a request.

Your images follow the same flow. Imagine tracking a single, 24-bit, high-dpi resolution PNG image and from that, generating a set of 8-bit PNGS of varying resolutions to use at different CSS media breakpoints or screen resolutions.

You can put the same practice in play with vector images. Pretty much any vector-creating software package creates files containing information your site does not need such as metadata and hidden layers. Track these non-optimized files, and open them up in Inkscape or Adobe Illustrator or whatever to make on-the-fly changes to fill colors or stroke weights. Let a build step sitting between your work environment and deployment create lean versions.

I've tossed a few image files (one SVG) into a directory. Let's say these are the source versions of images I'd like to serve with my site.

1
2
3
4
5
| - a.png (1.6K)
| - b.jpg (865K)
| - c.svg (2.8K)
- build/
| - images/

I'd like to optimize each of these and deploy those optimizations. There are plenty of ways to do this, say with ImageMagick for example, but I, being a web developer, have access to a bounty of tools via npm and Node.js. imagemin is one such tool that does quite a bit. I'll use it in a module below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// build.js

const Imagemin = require('imagemin');

new Imagemin()
    .src('images/*.{gif,jpg,png,svg}')
    .dest('build/images')
    .use(Imagemin.jpegtran({progressive: true}))
    .use(Imagemin.optipng({optimizationLevel: 3}))
    .use(Imagemin.svgo())
    .run(function(err, files) {
        files.forEach(function(file) {
            console.log('Generated ' + file.path);
        });
    });

I run this with node build.js, examine the build/images directory and I see

1
2
3
4
- build/images/
| - a.png (537B)
| - b.jpg (343K)
| - c.svg (812B)

Pretty good! I've shaved a good deal off of everything, especially the JPEG. An eyeball examination of the resulting images shows me they are virtually indistinguishable from their source counterparts.

I'm going to go a bit further and modernize my site a bit by choosing to serve icon images as SVG instead of PNG. Modern browsers will have no problem with this, but I'd still like to extend my coverage to older IE without having to introduce a new, intermediary authoring step of tracking and maintaining raster-based versions of my vectors. I can use the same setup as before with another tool, svg2png within the same imagemin module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// build.js

const Imagemin = require('imagemin');

new Imagemin()
    .src('images/*.{gif,jpg,png,svg}')
    .dest('build/images')
    .use(Imagemin.jpegtran({progressive: true}))
    .use(Imagemin.optipng({optimizationLevel: 3}))
    .use(Imagemin.svgo())
    .run(function(err, files) {
        files.forEach(function(file) {
            console.log('Generated ' + file.path);
        });

        var svgs = files.filter(function(file) {
            if (file.extname.toLowerCase() === '.svg') {
                return true;
            } else {
                return false;
            }
        });

        if (!svgs.length) {
            return;
        }

        const fs = require('fs');
        const svg2png = require('svg2png');

        svgs.forEach(function(file) {
            // Generate fallback pngs from svgs.
            svg2png(file.contents).then(function(output) {
                var path = 'build/images/' + file.stem + '.png'
                fs.writeFile(path, output);
                console.log('Generated ' + path);
            });
        });
    });

Running this, I've done the same as before, except now I additionally have created PNG versions of each of my SVG files. Not bad! I could take this even further and generate multiple resolutions of PNGs from those SVGs if my site required them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- build.js
- images/
| - a.png (1.6K)
| - b.jpg (865K)
| - c.svg (2.8K)
- build/images/
| - a.png (537B)
| - b.jpg (343K)
| - c.svg (812B)
| - c.png (9.6K)

This workflow provides the added benefit of clarifying your commit history. Any change to your images is a change in content. This means that commits to your media always denote a change in an aspect such as color, dimension, copy, etc, rather than a new, differently compressed version on of the same source.

Return to Articles & Guides