I promised myself I wouldn’t get involved in any more Gulp tutorials; task runners aren’t exactly the sexiest topic in the world, and chances are if you’ve made it to this blog, you’ve either solidified a CI/CD pipeline for going live with software, or you simply don’t need one. We’ll make an exception this time, because gulp-imagemin is particularly dope.

Imagemin is a standalone Node library which also comes with a CLI, and of course, a Gulp plugin. In short, imagemin compresses images in a given directory and is intelligent enough to recognize images it has already compressed. This is huge because it means we can recklessly tell imagemin to compress the same folder of images hundreds of times, and each image will only be compressed exactly once.

For this tutorial, we’ll be taking gulp-imagemin and creating a task to compress images in complex folder structures.

Using Imagemin on Complex Folder Structures

We’ve probably mentioned this once or twice before, but this blog is a theme running on a Ghost stack. The thing about Ghost (and probably any other blogging platform) is that it stores content in a date-based folder hierarchy. /images looks like this:

/images
├─ /2017
│  └─ /01
│  └─ /02
│  └─ /03
│  └─ /04
│  └─ /05
│  └─ /06
│  └─ /07
│  └─ /08
│  └─ /09
│  └─ /10
│  └─ /11
│  └─ /12
└─ /2018
   └─ /01
   └─ /02
   └─ /03
   └─ /04
   └─ /05
   └─ /06
   └─ /07
   └─ /08
   └─ /09
   └─ /10
   └─ /11
A typical directory hierarchy for storing images over time

Imagemin does not work recursively, so we’ll need to handle looping through this file structure ourselves.

Starting our Gulpfile

Let’s get started by going through the barebones of the libraries required to make this happen:

let gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  fs = require('fs'),
  path = require('path');
Including requirements in our Gulpfile

gulp-imagemin is the core Gulp plugin we need to compress our images, but is actually useless on it’s own — we need to also import plugins-for-a-plugin; gulp-imagemin requires a separate plugin for each image type we need to express.

We’re also requiring fs and path here, which will let us walk through folder structures programmatically.

Imagemin Plugins

As mentioned imagemin itself has plugins per image type: only require the ones you think you’ll need:

let gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  imageminWebp = require('imagemin-webp'),
  imageminJpegtran = require('imagemin-jpegtran'),
  imageminPngquant = require('imagemin-pngquant'),
  imageminGifSicle = require('imagemin-gifsicle'),
  imageminOptiPng = require('imagemin-optipng'),
  imageminSvgo = require('imagemin-svgo'),
  fs = require('fs'),
  path = require('path');
Gulp Imaginemin plugins per type of image

For the sake of keeping this tutorial simple, we’ll limit our use case to JPGs.

A particular standout here worth mentioning here is WebP: a “next-gen” image compression for the web which supposedly offers the best image quality for the smallest file size available.

Let’s Get This Going

A common practice is to specify the filepaths to an app’s assets (such as styles, or images) as a single variable in their Gulpfile. This is even more relevant in the case of anybody using Ghost, where images are in a totally different file structure from where our Gulpfile lives.

let gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  imageminJpegtran = require('imagemin-jpegtran'),
  fs = require('fs'),
  path = require('path');

let paths = {
  styles: {
    src: 'src/less/*.less',
    dest: 'assets/css'
  },
  scripts: {
    src: 'src/js/*.js',
    dest: 'assets/js'
  },
  html: {
    src: 'views/*.hbs',
    dest: 'assets/'
  },
  images: {
    src: 'src/images/',
    dest: 'dist/images/'
  }
};

Looping Through Subdirectories

We defined some key directories in our paths variable, notably the two directories defined in paths.images: the source directory (uncompressed) and the dist directory (for production-ready, compressed images). We need to step through our /src/images folder and recursively fetch all subdirectories containing images.

We can accomplish this with a simple loop:

function optimize_images() {
  fs.readdir(paths.images.src, function(err, directories) {
    for(let i=0; i < directories.length; i++){
      let directory = path.join(paths.images.src, directories[i]);
      // Logic to optimize images goes here
    }
  });
}
Find all subdirectories in a given directory

fs.readdir() is a method that returns the contents of any directory. Our newly defined function optimize_images() will loop through all subfolders in a target directory recursively, which allows us to fetch all images in a nested directory. Finally, we'll then call another function to compress the images (that comes next).

Compressing Images in Each Folder

optimize_images() kicks us off by recursively looping through all subdirectories of src/images/. We can use imagemin on a directory of images to find and compress every image of a certain type (or all types, but we're sticking to .jpgs for the sake of this tutorial).

To do this, we can write a new function to execute on each subdirectory we find. Lets name this function compress_images_in_directory(), as we'll be executing it per subdirectory:

function compress_images_in_directory(directory) {
  return gulp.src(folder_path + '/*.jpg')
  .pipe(imagemin(
    [imageminJpegtran({progressive: true})],
    {verbose: true}
  ))
  .pipe(gulp.dest(paths.images.dest));
}

While it looks simple, this function accomplishes a number of things for us:

  • Looking for files ending in .jpg in each subdirectory of src/images.
  • Run imageminJpegtran to compress a copy of each JPG file.
  • Write the new, compressed images to the destination directory of dist/images.
  • Print the results to the console via the verbose parameter (this prints as: “Minified 10 images”).

Put it All Together

We're left with a final gulpfile that ultimately looks for uncompressed images in a source folder, and then creates compressed versions of said images to a dist folder which maintaining the same file structure:

let gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  imageminJpegtran = require('imagemin-jpegtran'),
  fs = require('fs'),
  path = require('path');

let paths = {
  styles: {
    src: 'src/less/*.less',
    dest: 'assets/css'
  },
  scripts: {
    src: 'src/js/*.js',
    dest: 'assets/js'
  },
  html: {
    src: 'views/*.hbs',
    dest: 'assets/'
  },
  images: {
    src: 'src/images/',
    dest: 'dist/images/'
  }
};

function compress_images_in_directory(directory) {
  return gulp.src(folder_path + '/*.jpg')
  .pipe(imagemin(
    [imageminJpegtran({progressive: true})],
    {verbose: true}
  ))
  .pipe(gulp.dest(paths.images.dest));
}

function optimize_images() {
  fs.readdir(paths.images.src, function(err, directories) {
    for(let i =0; i < folders.length; i++){
      let directory = path.join(paths.images.src, directories[i]);
      compress_images_in_directory(directories[i]);
    }
  });
}

let build = gulp.parallel(styles, scripts, optimize_images);

gulp.task('default', build);
Recursively transform images and copy them to a destination path

And there you have it; a Gulpfile which compresses your images without intruding requiring any sort of relinking.

If you’re interested in imagemin or further optimizing your site, I highly recommend Google’s recently announced beta of https://web.dev. This is an excellent resource for auditing your site for opportunities on speed, SEO, and more.