← Go Back

Working with images, optimized solution in Vue.js

The main problem about images is pretty simple to understand. They are pretty big compared to code itself. Usually an modern website's size is 80% only from images. There's a solution?

No, the size of images can't be reduced so much. Of course you can optimize to reduce size (removing exif data, metadata or something like), but there's no magic trick about it. but browser / server do some compression in the middle, but it's a different thing from this post argument.

So this post is useless? Oh no, in this post i will explain better solution to delay and stagger images's load, this mainly achieved using <picture> tag and a Vue Component to handle animation and image's load.

2 Different image format? This code is made for my own website. I use only jpg image. Then in static build i create WebP version of image to provide within browser which support it.

How 2 different image format, waste of time?

Using node, in my build system (Netlify + Github CI) using gridsome's hook beforeBuild, if your system does not have hook, just do a regular script and concat before in package.json command.

	const path = require('path')
	const fs = require('fs')
	const sharp = require("sharp")
  	...
	api.beforeBuild(() => {
    const directoryPath = path.join(__dirname, 'static', 'posts-image')
    let images = []
    fs.readdir(directoryPath, (err, files) => {
      if (err) {
        return console.log('Unable to scan directory: ' + err)
      }
      files.forEach((file) => {
        if (file.split('.').pop() === 'jpg') {
          const imgpath = path.join(__dirname, 'static', 'posts-image', file)
          images.push(imgpath)
        }
      })

      images.forEach((img) => {
        const destPath = img.split('.').slice(0, -1) + '.webp'
        console.log('converting jpg to => ', destPath)
        sharp(img)
          .webp()
          .toFile(destPath)
          .catch((err) => console.log(err))
      })
    })
  })

Code todo: Detect image's format by image, and not by extension. Support to PNG and Gif. But right now it's not my own priority.

Picture tag

When you have a webp and jpg images together, you can simply use HTML5 to show your image, if your browser supports WebP, you will download only WebP (smaller).

<picture class="LazyImage -has-animation" style="background-color: rgb(16, 137, 255); display: block; width: 967px;">
  	<source type="image/webp" media="(max-width: 600px)" srcset="/posts-image/2-480w.webp">
  	<source type="image/webp" media="(min-width: 1087px)" srcset="/posts-image/2-967w.webp">
  	<source type="image/jpeg" media="(max-width: 600px)" srcset="/posts-image/2-480w.jpg">
  	<source type="image/jpeg" media="(min-width: 1087px)" srcset="/posts-image/2-967w.jpg">
  	<img data-src="/posts-image/2.jpg" class="id-11679 loaded" src="/posts-image/2.jpg" data-was-processed="true">
</picture>

Future Todo: Missing middle resolution? there's no middle resolution max-width and min-width are different. Why? Simply because fallback image is the one :P I'm pretty lazy in these deadline days.

Lazy

Another future Todo is include Polyfill. This code require polyfill to work in some browsers which do not support <picture> but i do not care so much, this is my own blog. I don't want increase build size to support IE or any other no-html5 sh*tty browser.

Vue Component

Why i do use Vue if this is already optimized? This HTML5 Tag is good due you can load right image in right device, but loading is still sequential and not staggered.

Progressive

See this image, we want to download "loading" the page only 1 and 2, 3 and 4 is not needed instantly then could be downloaded when user enter with viewport within, or simply later also without any user interaction.

Full file used on this website: Github Gist

<template>
  <picture
    :class="`LazyImage ${animating ? '-has-animation' : ''}`"
    :style="style"
  >

    <source
      type="image/webp"
      v-for="(src, i) in lazySrcset"
      :key="'source-webp-' + id + '-' + i"
      :media="getMediaQueries(src, i, lazySrcset.length)"
      :data-srcset="getSrcSet(src, lazySrc, 'webp')"

    />
    <source
      type="image/jpeg"
      v-for="(src, i) in lazySrcset"
      :key="'source-' + id + '-' + i"
      :media="getMediaQueries(src, i, lazySrcset.length)"
      :data-srcset="getSrcSet(src, lazySrc, 'jpg')"
    />
    <img
      :data-src="srcComputed"
      :class="id"
    />
  </picture>
</template>

Like you see i'm using class id, id is generated big random number.

then lazy load using vanilla-lazyload, simply on mounted vue.js lifecycle. If you look template, data-src and data-srcset is not src and srcset: Browser will not load it in this way until lazyload copy them from data-* to direct attribute.

mounted() {
    const observer = new LazyLoad({
      elements_selector: '.' + this.id,
      callback_loaded: () => {
        this.animating = true
        this.loading = false
      }
    })
  },

I have 2 different Vue Instance variable. animating and loading, only animating is useful.

`${ animating ? '-has-animation' : '' }`
.LazyImage img {
    max-width: 100%;
    clip-path: inset(100% 0 0 0);
    transition: clip-path 0.4s ease-in-out;
    transition-delay: 1s;
    will-change: clip-path;
  }
  
  .LazyImage.-has-animation img {
    clip-path: inset(0 0 0 0);
  }

This is mainly reveal animation, just did this trick because lazy load have ugly issue. Cause image appear from nothing moving layout or something like this. This animation plus padding-top calculation ( background color and calc padding-top based on aspect ratio, so also when image is not ready you still see colored background instead. ). In this blog i use also more complex solution. But did my own component just because i can't use base64 blur in my own post list ( images are static and hosted outside of src's repo. Yeah could do some graphQL hack to manage them, but seems so complex instead this easy solution, and yeah i have also custom loading animation!)

Code Todo:

  • Do my own npm package to handle this situation. Not super complex but very easy vanilla js
  • Add fallback to clip-path
  • Remove useless code on my component, like loading ( which is deprecated now, due animating replace ). Not a big issue, but neither so wonderful to see.

Results?

First test

In first loading ( cache disabled ) website just download 934 KB, scrolling cause download of others 2 file ( 1, 2 webp, optimized with right html5 tag you remember? )

Second Test

I'm pretty happy with this solution. I still need more work on this blog. I have 2 different image loading solution, one based on gridsome <g-image> and this one. Both are pretty good. Now on my blog it does not matter. But think apply this solution on 400 wallpapers hosted on homepage ( example wallpaper site ). Interesting uh?

Test3 gif

Written by Salvatore Criscione
follow me on: GitHub / Twitter / Instagram / LinkedIn