Image Compression Using Gulp and Imagemin

The simplest way to optimize page speed without breaking everything

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

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:

var gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  fs = require('fs'),
  path = require('path');

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:

var 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');

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

Some people (myself included) like to specify paths to their assets 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.

var gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  imageminJpegtran = require('imagemin-jpegtran'),
  fs = require('fs'),
  path = require('path');
var 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: '/var/www/my-theme/content/images/2018/',
    dest: '/var/www/my-theme/content/images/2018/'
  }
};

Looping Through Folders

We need to look in our /images folder are recursively find all folders containing images. Referencing the image path we set in paths, we’ll build an array of targeted folders:

function image_loop() {
  var folder_arr = []
  fs.readdir(paths.images.src, function(err, folders) {
    for(var i =0; i < folders.length; i++){
      var folder_path = path.join(paths.images.src, folders[i]);
      folder_arr.push(folder_path);
    }
    for(var i =0; i < folder_arr.length; i++) {
        images(folder_arr[i]);
    }
  });
}

fs.readdir() is a method that returns the contents of any directory. We'll create a function called image_loop which loops through all folders in the target directory, and will then call another function to compress the contents:

function image_loop() { 
   fs.readdir(paths.images.src, function(err, folders) { 
      for(var i =0; i < folders.length; i++){ 
         var folder_path = path.join(paths.images.src, folders[i]);   
         images(folders[i]); 
       } 
   }); 
 }

Compressing Images in Each Folder

image_loop calls function images once per folder to compress the contents of each folder. Here’s where we actually get to use imagemin:

function image_loop() {
  fs.readdir(paths.images.src, function(err, folders) {
    for(var i =0; i < folders.length; i++){
      var folder_path = path.join(paths.images.src, folders[i]);
      images(folders[i]);
    }
  });
}

Simple enough, all we’re doing is:

  • Looking for files ending in .jpg in each folder
  • Running imageminJpegtran to compress each JPG file
  • Specifying verbose, which prints the result to the console (for example: “Minified 0 images”)
  • Writing files to the destination (which is the same as the source, thus overwriting our files)

Put it All Together

var gulp = require('gulp'),
  imagemin = require('gulp-imagemin'),
  imageminJpegtran = require('imagemin-jpegtran'),
  fs = require('fs'),
  path = require('path');
var 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: '/var/www/my-theme/content/images/2018/',
    dest: '/var/www/my-theme/content/images/2018/'
  }
};
function images(folder_path) {
  return gulp.src(folder_path + '/*.jpg')
  .pipe(imagemin(
    [imageminJpegtran({progressive: true})],
    {verbose: true}
  ))
  .pipe(gulp.dest(paths.images.dest));
}
function image_loop() {
  fs.readdir(paths.images.src, function(err, folders) {
    for(var i =0; i < folders.length; i++){
      var folder_path = path.join(paths.images.src, folders[i]);
      images(folders[i]);
    }
  });
}
var build = gulp.parallel(styles, scripts, image_loop);
gulp.task('default', build);

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.

Author image
New York City Website
Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.

Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.