Medium-like Image Loading with Vue.js (part 2)

Quick summary of part 1

I’m quite fond of the way Medium displays its images while they’re loading.
At first they display a grey placeholder, then displays a small version of the image – something like 27×17 pixels.

The trick is that most browsers will blur a small image if it is streched out.
Finally, when the full-size image is downloaded, it replaces the small one. You can see a live demo of what I had done on this Codepen.

In this post I intend to make a component that is as close as possible to what Medium actually does, as it is explained on this excellent post by José M. Perez.
And I have also switched from Vue 1 to Vue 2 😉

Adding a placeholder behind the images

Let’s add the first element which we wait for the images to be loaded: a grey placeholder.

waiting

In our template we have 3 elements:

  • a grey placeholder that will be shown while the low-resolution version of the image is not loaded yet
  • a low-resolution image that will be shown streched out while the high-resolution is not loaded yet
  • a high-resolution image that will be displayed to the user

The javascript logic will set the sources of the images and display the right element depending on which images are already loaded.
To load the images we’ll use the mounted function of the component.
To set the “state” of the component, we’ll use a data called currentSrc that will be initialized to null and will take the value of the source of the image that should be displayed.

This far, the Vue component should look like this:

<template>
  <div v-show="currentSrc === null" class="placeholder"></div>
  <img v-show="currentSrc === hiResSrc" :src="lowResSrc"></img>
  <img v-show="currentSrc === hiResSrc" :src="hiResSrc"></img>
</template>

<style scoped>
  img, .placeholder {
    height: 600px;
    width: 900px;
    position: absolute;
  }
  .placeholder {
    background-color: rgba(0,0,0,.05);
  }
</style>

<script>
  export default {
    props: [
      'hiResSrc',
      'loResSrc'
    ],
    data: function() {
      return {
        currentSrc: null // setting the attribute to null to display the placeholder
      }
    },
    mounted: function () {
      var loResImg, hiResImg, that, context;
      loResImg = new Image();
      hiResImg = new Image();
      that = this;

      loResImg.onload = function(){
        that.currentSrc = that.loResSrc; // setting the attribute to loResSrc to display the lo-res image
      }
      hiResImg.onload = function(){
        that.currentSrc = that.hiResSrc; // setting the attribute to hiResSrc to display the hi-res image
      }
      loResImg.src = that.loResSrc; // loading the lo-res image
      hiResImg.src = that.hiResSrc; // loading the hi-res image
    }
  }
</script>

Adding transitions

transition

Then we need to add some transitions when the value of currentSrc changes.
To be more accurate, we want to fade-in/out every element as they appear/disappear.
Vue.js lets you handle CSS transitions in a pretty easy way by adding and removing classes.

As we have multiple elements to transition between we have to use a transition group:

<template>
  <transition-group name="blur" tag="div">
    <div v-show="currentSrc === null" key="placeholder" class="placeholder blur-transition"></div>
    <img v-show="currentSrc === loResSrc" :src="loResSrc" key="lo-res" class="blur-transition"></canvas>
    <img v-show="currentSrc === hiResSrc" :src="hiResSrc" key="hi-res" class="blur-transition"></img>
  </transition-group>
</template>

Here is how Vue.js handles the transition when the value of currentSrc changes from null to loResSrc:

  1. the ‘blur-leave’ class is added to the placeholder, thus triggering the transition for the placeholder
  2. the ‘blur-enter’ class is added to the low resolution image
  3. the placeholder is hidden and the image is shown
  4. on the next frame, the ‘blur-enter’ and ‘blur-leave’ classes are removed, thus triggering the transition for the image

Knowing this we can make the following changes in our style:

<style scoped>
  img, .placeholder {
    height: 600px;
    width: 900px;
    position: absolute;
  }
  .placeholder {
    background-color: rgba(0,0,0,.05);
  }
  .blur-transition {
    transition: opacity linear .4s 0s;
    opacity: 1;
  }
  .blur-enter, .blur-leave {
    opacity: 0;
  }
</style>

That way the images and placeholders will fade in and out when they appear and disappear.

You can see a live demo here: http://codepen.io/zkilo/pen/wgdxWq.

Well, it looks good but there is a slight difference with what it actually looks like on Medium.
Can you spot it?

Using canvas

If you’ve looked well at the previous Codepen you may have found out a little issue.
When we change the opacity of the low resolution image, we can see the ugly pixels because browsers aren’t able to blur the image while its opacity changes.

But don’t worry, there’s an easy way to solve this!
We’re going to use canvas, because browsers can actually blur canvas while their opacity changes with a little trick!

So, let’s change our template:

<template>
  <transition-group name="blur" tag="div">
    <div v-show="currentSrc === null" class="placeholder blur-transition" key="placeholder"></div>
    <canvas v-show="currentSrc === loResSrc" height="17" width="27" key="canvas" class="blur-transition"></canvas>
    <img v-show="currentSrc === hiResSrc" :src="hiResSrc" key="image" class="blur-transition"></img>
  </transition-group>
</template>

And our style:

<style scoped>
  img, canvas, .placeholder {
    height: 600px;
    width: 900px;
    position: absolute;
  }
  .placeholder {
    background-color: rgba(0,0,0,.05);
  }
  canvas {
    filter: blur(10px);
  }
  .blur-transition {
    transition: opacity linear .4s 0s;
    opacity: 1;
  }
  .blur-enter, .blur-leave {
    opacity: 0;
  }
</style>

You can see that we’ve set the height and weight attributes of our canvas in the template and streched it out in our style.
You might also have spotted the little trick: we can add a blur filter attribute on the canvas and it will still be here even if the opacity of our element changes!
I’ve set it to 10px empirically, but you can learn more about the canvas filter here.

Once we’ve done this, we need to draw our low resolution image inside the canvas once it is loaded:

<script>
  export default {
    props: [
      'hiResSrc',
      'loResSrc'
    ],
    data: function() {
      return {
        currentSrc: null
      }
    },
    mounted: function () {
      var loResImg, hiResImg, that, context;
      loResImg = new Image();
      hiResImg = new Image();
      that = this;
      context = this.$el.getElementsByTagName('canvas')[0].getContext('2d'); // get the context of the canvas

      loResImg.onload = function(){
        context.drawImage(loResImg, 0, 0);
        that.currentSrc = that.loResSrc;
      }
      hiResImg.onload = function(){
        that.currentSrc = that.hiResSrc;
      }
      loResImg.src = that.loResSrc;
      hiResImg.src = that.hiResSrc;
    }
  }
</script>

You can see the final result on this Codepen: http://codepen.io/zkilo/pen/ZLyweL.

And that’s it!

brad

You can find the component as a .vue file on this Github repository.


You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us

  • Mikhail Stralenya

    Why don’t use progressive jpeg images by default and user low-res images as fallback-only?

  • Louis Zawadzki

    Hi Mikhail,
    The goal of the article is to focus on how to use low-res images, so I think talking about progressive jpeg images as well would be a bit out of place.

    Still, I like the idea of mixing different approaches!

  • Polo Rith

    Is this functional with Vue 2? Looks like demo is loading with /1.0.26.

  • Hi Polo,

    Yes this is working with Vue 2, you can launch the component from this repository: https://github.com/louiszawadzki/vue-lazy-img-loading using vue-cli to try it.