Responsive Images with HTML and Grunt

Responsive Images with HTML and Grunt

Responsive images are key to a snappier web app and and to show a special care to our mobile user’s who usually load our content using their mobile plans. That is why we can make use of the HTML picture element as well as a simple grunt task (you can use gulp or webpack as well).

Resizing Images

Apart from responsive images, it is very important that we optimize our images the most we can before using them on our websites mainly because images like to store a lot of unnecessary information and tend to be ridiculously large (and heavy). Depending on what resolutions you want to support going above 1200px wide starts making the site load rather slowly.

On average an optimized image that is 1200px wide is about 300kb large which isn’t much but if we are on a smartphone things are a little different. Smartphones are on average 300-400px wide which means our 1200px image is much wider that it needs to be. Furthermore an image that is about 500px wide is about 80kb large which is already 70% less. If we go a step further and reduce the quality a little we can bring the image size down to even 20kb which one smartphone can be a VERY BIG difference. As a recap:

  • 1200px image (unoptimized): 800kb+
  • 1200px image (optimized): ~300kb
  • 500px image (optimized): ~80kb
  • 500px image (optimized & reduce quality): ~20kb

That sounds good but how to do we do it ? Well, if you are a javascript advocate you might be tempted to use javascript to check the clients screen size but there is a much simpler approach which is already build into HTML. The picture element is the magic element that makes it all possible. You see, the picture element is wrapper for our commonly used img element. You can use it like this:

<picture>
  <img src="my-image.jpg" alt="my image">
</picture>

If you take a look at result it will slightly disappoint you. It only renders your normal image like img would. The special power of picture is in the source you can give it. Let suppose you went into an image editor and created 3 copies of your image a large (1200px), a middle (700px) and a small (300px) one. Using the source tag inside the picture element, we can tell the browser which images to use based on the screen size like this:

<picture>
  <source media="(min-width: 750px)" srcset="my-image-lg.jpg">
  <source media="(min-width: 500px)" srcset="my-image-md.jpg">
  <img src="my-image-sm.jpg" alt="my image">
</picture>

This is great, we are telling the browser to load the smallest image by default, the medium sized image when the screen is at least 500px wide and the large image when the screen is 750px wide. We are serving images completely based on the user’s needs. As a recap:

  • Desktop users: Large Image (~300kb)
  • Tablet users: Medium Image (~80kb)
  • Smartphone users: Small Image (~20kb)

This is awesome, user’s on a smartphone can expect their data plans to last at least 100x more now on our site with a single image. Image if we had more images, maybe 3 or 5 that would have been over 1_000Kb only in images. Once again lets remember that a site that has to load over 1_000Kb in Images will obviously slower than one that only has to load 60KB of images.

At this points our main concern is how to scale this. If we have a backend server like Rails we can use the carrierwave gem which supports multiple image sizes by default but for this guide let’s assume we are building a somewhat static site such as a clients website (restaurant page, blog, etc.). We can add images and do it all manually or switch to an automation tool such as Grunt.

Resize in masses

Automation tools are must these days as the number of task we have to do increases and the time we have to do them decreases. Grunt is one of the options out there although Gulp and Webpack are more popular currently and we can certainly use those tools as well but for the sake of this guide we are going to stick with Grunt because it is pretty much the same.

To resize images we need image magick, don’t let the somewhat old design of the website fool you. Many of the popular options out there rely on it and just add an abstraction layer on top of it like the above mentioned carrierwave gem. if you are on a mac I highly recommend using homebrew:

brew install imagemagick

If you are not on a mac visit the site and got to the download section. Good now we need Grunt itself and the required modules which requires node. I’m not going into depth on how to install node but I recommend you install it using a version manager like nvm:

npm install grunt grunt-cli grunt-responsive-images -g

We are installing it globally which means we don’t have to install for every project separately. All that is left is to create our task and run it. The task itself will not make sense if you haven’t used task managers before:

// Gruntfile.js
module.exports = function(grunt) {

  grunt.initConfig({
    responsive_images: {
      dev: {
        options: {
          engine: 'im',
          sizes: [{
            width: '70%',
            suffix: '_large',
            quality: 80
          }, {
            width: '50%',
            suffix: '_medium',
            quality: 50
          }, {
            width: '40%',
            suffix: '_small',
            quality: 50
          }]
        },
        files: [{
          expand: true,
          src: ['*.{gif,jpg,png}'],
          cwd: 'assets/images_src/',
          dest: 'assets/img/'
        }]
      }
    },
    clean: {
      dev: {
        src: ['assets/img'],
      },
    },
    mkdir: {
      dev: {
        options: {
          create: ['assets/img']
        },
      },
    },
  });

  grunt.loadNpmTasks('grunt-responsive-images');
  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-contrib-copy');
  grunt.loadNpmTasks('grunt-mkdir');
  grunt.registerTask('default', ['clean', 'mkdir', 'responsive_images']);

};

To run our task we have to open terminal and navigate to the root of our project where Gruntfile.js is located and run grunt:

grunt

Before that lets go through what this will do. First we specify the image sizes we want it to output in this case it will take each image and output them with 80%, 50% and 40% widths respectively. We can specify the quality reduction we want as well which is also advisable. You would have to play around with those settings and see the results you get but for my blog this is what I use. Next under file we specify where to locate our original images. In my case my images are located in a nested folder assets/images_src/ which you can see under cwd and the dest tells grunt where to put the optimized images. The clean section does exactly that. It will remove anything that is inside the assets/img folder which is my output folder before placing my optimized images inside which makes sure I always have the latest and up to date versions of my images. Finally the mkdir section makes a directory to store my images after cleaning everything up. The result is that all the original images will be copied, resized and placed inside the assets/img folder. The result looks like this with these settings:

ll assets/img/
total 1760
drwxr-xr-x  23 alexander  staff   736B Jan 19 21:33 .
drwxr-xr-x   7 alexander  staff   224B Jan 19 21:33 ..
-rw-r--r--   1 alexander  staff    17K Jan 19 21:33 my-image-40pc_small.jpg
-rw-r--r--   1 alexander  staff    23K Jan 19 21:33 my-image-50pc_medium.jpg
-rw-r--r--   1 alexander  staff    60K Jan 19 21:33 my-image-70pc_large.jpg

Going back to the HTML all we have to do now is to use these images:

<picture>
 <source media="(min-width: 750px)" srcset="assets/img/my-image-70pc_large.jpg">
 <source media="(min-width: 500px)" srcset="assets/img/my-image-50pc_medium.jpg">
 <img src="assets/img/my-image-40pc_small.jpg" alt="my image">
</picture>

And we are done. If we want to add new images we can place them in our source folder and run grunt again.

PS: The large image at the top for example was originally 300Kb large. The large, middle and small images are 67Kb, 24Kb and 18Kb respectively. Especially on a smartphone the size is a mere 6% of the original size.