DEV Community

Alex
Alex

Posted on

Minify, generate WebP and lazyload images in your Vue & Nuxt application

I wouldn't have written this post if standard image optimization flow hadn't been so tough to me. At first, all I wanted is to apply my favourite lazysizes to the app I'm developing now. I was stuck in this concept of modules and plugins and couldn't figure out what it actually is. Next step was image optimization and transforming current PNGs and JPEGs into WebPs — and that became a pain in the ass.

We all read posts like this to see a working example, so here it is!

Inject lazysizes

npm i lazysizes — no need to explain.

Then create a vue-lazysizes.client.js file and put it into @/plugins/ folder. .client in the name signalizes server that this plugin shouldn't be run on server — it's important! This file shoud contain just two lines:

import lazySizes from 'lazysizes'
export default lazySizes
Enter fullscreen mode Exit fullscreen mode

Configure nuxt.config.js:

module.exports = {
  build: {
    extend (config, { isDev, isClient, loaders: { vue } }) {
      if (isClient) {
        vue.transformAssetUrls.img = ['data-src', 'src']
        vue.transformAssetUrls.source = ['data-srcset', 'srcset']
      }
    }
  },
  plugins: [
    '~/plugins/vue-lazysizes.client.js'
  ]
}
Enter fullscreen mode Exit fullscreen mode

You can see that our "plugin" is inserted and in build key we extend Webpack config, so vue-loader can transform aliases in data-srcset, data-src and srcset attributes in <img> and <source> tags — we'll need it in future.

That's all! Now you can make images "lazy" as you usually do:

<figure class="picture">
  <img data-src="~assets/images/image.png" class="lazyload" alt="Alternate text for the image">
</figure>
Enter fullscreen mode Exit fullscreen mode

Image optimization & convertation to WebP

But what about making images smaller and loading WebP images + fallback? After hours of struggling and many attempts with whole bunch of npm-packages I found brilliant module nuxt-optimized-images. I prefer to use mozjpeg and pngquant for JPEG and PNG optimization respectively.

And this is how it should be cooked:

npm i @bazzite/nuxt-optimized-images imagemin-mozjpeg imagemin-pngquant webp-loader --save-dev — installation of dependencies.

Configure nuxt.config.js:

module.exports = {
  modules: [
    '@bazzite/nuxt-optimized-images',
  ],

  optimizedImages: {
    inlineImageLimit: -1,
    handleImages: ['jpeg', 'png', 'svg', 'webp', 'gif'],
    optimizeImages: true,
    optimizeImagesInDev: false,
    defaultImageLoader: 'img-loader',
    mozjpeg: {
      quality: 85
    },
    optipng: false,
    pngquant: {
      speed: 7,
      quality: [0.65, 0.8]
    },
    webp: {
      quality: 85
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Settings in optimizedImages key are arguable, but this is my choice.

After all these manipulations you can use this snippet finally:

<figure class="picture">
  <picture>
    <source data-srcset="~assets/images/image.png?webp" type="image/webp">
    <source data-srcset="~assets/images/image.png" type="image/png">
    <img data-src="~assets/images/image.png" class="lazyload" alt="Alternate text for the image">
  </picture>
</figure>
Enter fullscreen mode Exit fullscreen mode

Note that WebP image source is added with ?webp.

With this great module you can go further and create, for instance, small SVG-fallback and use it while big image is loading. You can create responsive images, generate fallback color and (probably) much more. But let it be your homework for today.

Thanks for reading and I hope you'll find this post with Google before you spend couple of days on experiments like me.

Latest comments (28)

Collapse
 
justdevelop profile image
JustDevelop

If I want to dynamically set the image path, then it is crashing with the following error:

Error: Cannot find module './path-to/image.jpg?resize&size=750&format=webp'
at webpackContextResolve (eval at ./assets/images sync recursive

My code:

...
      <source
        :data-src="getPath('?resize&size=750&format=webp')"
        type="image/webp"
        :media="`(max-width: 768px)`"
      />
...

...
  methods: {
    getPath(suffix) {
      suffix = typeof suffix === 'undefined' ? '' : suffix
      return require('~/assets/images/' + this.src + suffix)
    },
  }
...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
justdevelop profile image
JustDevelop

Where this.src is a prop. It is all working without the suffix parameters. But when I add the suffix (thus, the nuxt-optimised-images params), the code fails and I get this error.

I have been on this for days. I just cannot fix this issue and I am willing to learn more about my problem but I don't know where to look. I hope someone can clarify this to me, since I think this dynamic declaration is essential.

Collapse
 
ignore_you profile image
Alex • Edited

I don't work with Vue now, so unfortunately I can't reproduce and examine your code. Though, I have an opinion that adding suffix dynamically might be impossible due to nature of methods. Methods are client-side, they apply in runtime, so creation of webp images with specific size cannot be done, 'cause this is a server-side action.

Collapse
 
theolavaux profile image
Théo LAVAUX • Edited

Great post! However, I cannot get this to work in my local project. Indeed, even though I included the webpack code, my images are not getting handled properly and I get a 404 error. stackoverflow.com/questions/643033...

Collapse
 
ignore_you profile image
Alex

It seems that you didn't insert lazysizes as a plugin - it's an important part.

Collapse
 
theolavaux profile image
Théo LAVAUX • Edited

Forgot to specify that part, you're right, but it is configured as you suggested. I have "lazysizes": "^5.2.2" in my package.json.

nuxt.config.js

  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    '~/plugins/lazysizes.client.ts',
  ],

plugins/lazysizes.client.js

import lazySizes from 'lazysizes'
export default lazySizes

I assume that lazysizes is loaded correctly because my final html tag looks like that, I have a lazyloaded class.

<img data-src="~/assets/img.png" alt="Image description" loading="lazy" class=" lazyloaded" data-v-13f1b7fa="" src="~/assets/img.png">

Original tag is

<img
  class="lazyload"
  data-src="~/assets/img.png"
  alt="Image description"
  loading="lazy"
/>
Thread Thread
 
ignore_you profile image
Alex

Well, the only difference I can notice is that you start path in img from ~/assets, not ~assets. Also attr loading=lazy looks like an overkill, I'd rather lean or on lazysizes, or on native browser's API. Or am I missing smth?

Thread Thread
 
theolavaux profile image
Théo LAVAUX • Edited

I thought that lazysizes acted as a plugin for browsers that don't support the loading attribute. The extra slash might be due to different Nuxt versions but it's standard for now : nuxtjs.org/guide/assets/. I'm wondering why my image URL isn't interpreted by vue-loader/webpack even though I specified the transformAssetUrls configuration. It seems to be partially taken into account because the src tag is appended to my image, even though I only specified a data-src attribute... I'm using a static target for my Nuxt project but that shouldn't interfere with configuration here.

Thread Thread
 
ignore_you profile image
Alex

Okay, can you try to put path to src directly, not to data-src? Just to check that with assets everything is fine by default. Maybe issue is out of scope of this tutorial :)

Thread Thread
 
theolavaux profile image
Théo LAVAUX

I removed the loading attribute as you suggested and changed data-src to src and my image is loading fine, the path is different as you can see.

<img src="/_nuxt/assets/img.png" alt="Image description" class=" ls-is-cached lazyloaded" data-v-13f1b7fa="">
Thread Thread
 
theolavaux profile image
Théo LAVAUX

I finally found the solution thanks to this article ! medium.com/@dannjb/a-lazy-loading-.... I was doing a check on the isClient property before setting the transformAssetUrls on vue-loader. Problem is, it wouldn't work for SSR, maybe it will help someone that comes here in the future.

As I run SSR using yarn generate, I need the asset url transform to happen on the server too; the isClient check is removed.

Thread Thread
 
lmuzquiz profile image
lmuzquiz • Edited

Thanks, i use yarn generate and it worked without the if (isClient)
code

i also used ~/ instead of just ~
code

Collapse
 
sankha profile image
Sankha Lakshan Karunasekara

A great article..! Work like a charm. =D Thanks, man.

Collapse
 
granttransition profile image
Grant Smith

Hi,

This looks great. I am either misunderstanding (highly likely), or I've made a mistake (even more likely). After implementation of the above, I no longer see images whilst on Dev. This will be because I have changed all my src attributes to data-src. Is this the expected behaviour?

Collapse
 
ignore_you profile image
Alex

Hello! At first, are there any console errors or warnings? Did you implement webpack settings, so when you look into source code, you see correct path to images?

Collapse
 
leonbuchner profile image
Leon Buchner • Edited

I have the same error.
I have no images anymore...

Image Code:
<img data-src="~/assets/chat-icon.svg" alt="" class="chat-icon mr-2 lazyload">

Error in Browser Console:
GET localhost:3000/~/assets/chat-icon.svg 404 (Not Found)

Thread Thread
 
moeses profile image
Moe

getting a blank 1x1 image aswell

Collapse
 
techmely profile image
Techmley

Thank man 😍. Now I knew why you use lazysizes over vue-lazyload. For now, It's doesn't support tag & source tag lol. Thank you one more!

Collapse
 
ignore_you profile image
Alex

you’re welcome, man

Collapse
 
anishgeorge2690 profile image
anish

Awesome

Collapse
 
boykininteractive profile image
TJ Boykin

this was golden! great write up. extremely helpful and exactly what I needed. Thank you!

Collapse
 
ignore_you profile image
Alex

thanks for appreciation :)

Collapse
 
themodernpk profile image
Pradeep Kumar

@alex any idea how to lazyload background image for inline style?

Collapse
 
ignore_you profile image
Alex

I never needed this option, but there is possibility to handle it with lazysizes: github.com/aFarkas/lazysizes#js-ap...

Collapse
 
kregenrek profile image
Kevin Regenrek • Edited

Thanks for the post. Especially the combination with nuxt-optimized-images.

I have two question please:

  • Why you don't use vue-lazyload for your project?
  • How do you implement events with lazysizes + nuxt/vue?
Collapse
 
ignore_you profile image
Alex

For me lazysizes is like industry standard library, well documented, predictable and SEO-friendly. Also, it supports picture + source (probably, vue-lazyload too, but this behaviour is not described directly). I won't describe all pros about lazysizes, you can check it yourself.

Sadly I can't tell you about events handling, bc I never met a situation when I had to handle one. Though, lazysizes do support them AFAIK.

Collapse
 
kregenrek profile image
Kevin Regenrek

Hey! I totally agree on the predictable part. I tried also some lazyload libs with nuxt and your proposal was the only one which worked out of the box. I made some additions including lqip blur up and aspect ratio for images in this repository github.com/regenrek/nuxt-lazysizes...

Thread Thread
 
ignore_you profile image
Alex

Really great to see your job, hope it will save some time for next generations. Take my star :)
And I'm happy to know that it helped you!