Frontend Development

Lazy loading modern image formats with Vue and Nuxt

Nick Ciolpan

Nick Ciolpan

One of the common optimizations for your website, which has an enormous impact on your site's speed, is the format and size of the image assets you're loading.

Serving your images in modern formats such as WebP and JPEG2000, and deferring loading until your visitor reaches a specific section, will give you a substantial speed boost. This will also be reflected in your Google Page Speed Insights score.

Your bandwidth restricted users will thank you, and you will also get an overall improved user experience.

Our target platform is Nuxt. We'll assume you already have some basic experience building with VueJS and Nuxt as well.

Should I do this kind of optimization right from the project start, or wait until after my website is done?

We've been asked this question a lot. Some say early optimizing pointless. We on the other hand, think that early optimization should be standard. It becomes increasingly difficult to optimize a project that has already been developed, especially if we're talking about large projects.


  • Vue components. We're going to create a component that will receive the asset path as a prop, process it, and return an optimized and context-aware asset.
  • Webpack. We'll use Webpack to convert and return optimized WebP and JPEG2000 assets. This process will strip the assets from unnecessary meta information and pipe them through different optimizers.
  • Lozad.js. Is a dependency-free JavaScript image loader, which uses the new Intersection Observer API to detect when our target image, enters the viewport.
  • LQIP. Is a low-quality image placeholder which will display a blurred version of our image asset until Lozad.js loads the full version.

How it all works

Initially every image will be deferred until it enters the viewport.

If scrolling happens too fast, the full image won't have enough time to load, so a very low quality, blurry placeholder will be presented until the full image loads. As soon as a full image has loaded, the placeholder content will be swapped with the full image.

All image assets will be optimized converted to WebP at compilation time. If the browser supports WebP, only the WebP version will load. If not, a lossless (or lossy) JPEG will load.

There are other optimizations worth mentioning:

  • Implement a CDN for all assets. This is great for blazing static content delivery all around the world.
  • Implement image sprites where possible. This is an efficient way to aggregate lots of small assets as icons into a big image to prevent tens of small requests to your server.
  • Implement srcset and sizes tags for all your images. This will make help the browser decide which image to load.
  • Configure expiration tags for all assets and pages. This makes repeat loading of your website pages and assets a breeze by using already stored assets from the browser's cache.

Let's get to work

We assume you already have a working application set up.
First, we have to install all the necessary libraries.

Install libraries

              yarn add lqip-loader webp-loader lozad


In the template we'll use a picture element with a JPG and a WebP source.

Vue Template

                <picture ref="picture" :data-iesrc="imageJPG.src" :data-alt="imageAlt" class="lazy-image">
                <source :srcset="imageJPG.srcSet" :sizes="sizes" />
                <source :srcset="imageWebP" :sizes="sizes" />

Later, we'll going to call it like this:

Component call

              imageAlt="Photo of a Never Settle poster"
              sizes="(max-width: 640px) 80vw,(max-width: 970px) 40vw, 30vw"


Component script

                import lozad from 'lozad'
                export default {
                name: 'LazyImage',
                props: {
                  imageJPG: {
                    type: Object,
                    default: null,
                  width: {
                    type: Number,
                    default: null,
                data() {
                  return {
                    loading: true,
                    imageWebp: null,
                computed: {
                  aspectRatio() {
                  if (!this.width || !this.height) return 100
                    return (this.height / this.width) * 100
                  style() {
                    const style = `background-image: url(${this.imageLQIP});   padding-top: ${this.aspectRatio}%;`
                    return style
                mounted() {
                  const setLoadingState = () => {
                  this.loading = false
                this.$el.addEventListener('load', setLoadingState)
                  this.$once('hook:destroyed', () => {
                    this.$el.removeEventListener('load', setLoadingState)
                  const observer = lozad(this.$el)

These are basic style to configure the low-resolution placeholder as the background of the image. The size and aspect ratio of the image are maintained via padding (check the style() function in the script above). The image uses an absolute position to be placed on top, as soon as it finishes loading.


            .lazy-image {
              display: block;
              width: 100%;
              height: 0;
              background-size: cover;
              background-repeat: no-repeat;

              &__wrapper {
                position: relative;
                z-index: 200;

              & > img {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;

What's next?

As of last year, native image lazy-loading support has been announced and supported in all major browsers except Safari. Safari has experimental support for lazy loading, so expect it to follow suit very soon.

For more information, check this out this extensive article on the state of native lazy-loading for the web.

Thanks for reading this. Let us know your thoughts on Twitter.

You deserve a better brand.

We’ll build something beautiful that converts!