loading...
Cover image for SVG-based image lazyloading as a matter of enjoyable UX
Blue Tomato Dev

SVG-based image lazyloading as a matter of enjoyable UX

manpenaloza profile image Manuel Penaloza Updated on ・6 min read

There was that day somewhere in November 2017, when our team came across this article by José M. Pérez including tons of new-to-us stuff regarding image placeholding and lazyloading. Watching the examples mentioned in the article compared to how we handled it that day was impressing. So impressing that we decided to give it a shot and implement the technique of image lazyloading using inline SVGs on our blue-tomato.com content section named Blue World.

SVG-based image lazyloading
pretty much the transition we've strived for

Apart from the impressing "magic-happens" moment when the SVG resolves to the high-resolution image, we encountered even more advantages that improve user experience:

  • immediate visual response when DOM is ready, as SVG placeholders are inlined and thus part of html source code
  • reduce amount of requests to get all small-pixeled lazyloading initial placeholders

Mentioned upfront: we managed to implement it and have already deployed it, so check it out in our Blue World Section or take a quick look in the video up next to see the implementation.


Implementation - slowmo showcase

The development journey went along with technical challenges, conceptual tasks and finally some rewrites of the implementation. So in case you like SVG-based image lazyloading and want to implement it on your own, check our upcoming hints and implementation notes so you can do this too (without those annoying rewrites). The main stack we used to accomplish it consists of the following awesome libraries:

  • Primitive: Golang tool reproducing images with geometric primitives
  • Svgo: Nodejs tool for optimizing SVG vector graphics files
  • appear.js: execute callbacks when DOM elements appear in and out of view
  • Processwire: PHP-based cms; no need to go with that cms, choose whatever cms you want following the concepts and solutions we suggest up next

Implementation details SVG-based image lazyloading

We won’t go through every line of code we shipped to our repository to make SVG-based image lazyloading happen. Our code is tightly coupled to our internal systems (primarily Processwire cms) and it wouldn’t make sense in terms of giving you a good overview of how we solved the implementation. Instead we will equip you with our roadmap we’d consider today if we’d have to do it again from scratch. Given below you'll be guided through these main steps:

  1. Generate and save SVG placeholders
  2. Check for SVG availability and ship it
  3. Proper SVG lazyload placeholder frontend handling

1st Generate and save SVG placeholders

Challenges
Our first approach was to generate the SVGs during the upload process in the admin panel of our cms. The first problem we faced was that the upload process of an image would take longer due to the converting process we triggered using the excellent Primitive library. But this was the smaller of the two problems. The second issue was that our image assets management system (Adobe Scene7) created all required image formats on the fly. So initially we created only one SVG using the format of the uploaded image without considering other formats handled by the image assets management system.

different image formats as a challenge for creating svg-based lazyload images
Different image formats require different SVG formats

Example
Uploaded image format:
15:4

BUT formats used in our templates:
skateboard-image.jpg?format=16:9
skateboard-image.jpg?format=4:3
skateboard-image.jpg?format=1:1

As we've only had that one SVG based on the original upload image width/height ratio that sometimes led to awkward looking effects transitioning from that one SVG to the original images once those were loaded from the server.

Our second approach was to generate the SVG file on the fly and cache the whole generated html files having all required SVGs inlined. We knew the initial process of producing and caching the whole markup file including all SVGs would take long. But in the end it would not affect user experience. So was our assumption :). Well, finally we ran into http timeout issues our editors would struggle with when saving pages that require a long SVG generation loop.

Solution
Our third approach led to our current working solution. If an editor uploads an image and the SVG is not yet produced in our network file system we automatically create a "svg-to-be-produced-soon.json" file. This file includes an information object about the SVG (dimensions, image server source, hashed name, etc.) that needs to be produced.

{  
   "jpgTmpFilePath":"/root_path/assets/primitives/tmp/kwleklsl090309f0g0.jpg",
   "svgFilePath":"/root_path/assets/primitives/svg/kwleklsl090309f0g0.svg",
   "svgMinFilePath":"/root_path/assets/primitives/svg/kwleklsl090309f0g0.min.svg",
   "bgColor":null,
   "inputUrl":"http://s7w2p3.scene7.com/is/image/bluetomato/ugc/2-1508153542.tif?$mc1$",
   "inputUrlHash":"kwleklsl090309f0g0",
   "env":"production"
}

Example of a svg-to-be-produced-soon.json file

As you can see, this svg-to-be-produced-soon object is quite "hash-intense". We use that hash - being taken from the inputUrlHash property - to match the image and the SVG to have a proper asset identification process starting at the SVG creation up to the usage in our template files.

So each new svg-to-be-produced-soon.json file is part of a queue a cronjob will take care of and trigger on an hourly base to generate the SVGs. Let's see what it does.

#!/bin/bash

for jsonFile in /root_path/primitives/queue/*.json
do
  echo "Processing $jsonFile"

  primitiveBinary="${root_path}tools/primitive" 

  # primitive image
  goPrimitiveCommand="$primitiveBinary -i $jpgTmpFilePath -o $svgFilePath -n 64 -m 1 -r 256 -bg='$bgColor'"
  eval $goPrimitiveCommand

  # svgo
  svgoBinary="${root_path}node_modules/svgo/bin/svgo"
  svgoCommand="$svgoBinary $svgFilePath -o $svgMinFilePath"
  eval $svgoCommand

  rm -f -- $jsonFile
done

echo "All SVGs created"
exit

The simplified cronjob (we removed our system-related code that'd lead to confusion) does four main things.

  1. loop over and get each new svg-to-be-produced-soon.json file
  2. get the primitive binary and execute it to create the SVG
  3. get the svgo binary and execute it to optimize the created SVG code
  4. delete the svg-to-be-produced-soon.json file

The time between uploading the images and publishing the page is usually more than one hour. As a delightful result mostly the already available SVGs are shipped to our users instead of our (meantime internally uncool ;)) lazyload fallback option.

So we finally got it! We have our created and saved SVGs. Let's go the next step and ship fancy and performant placeholder images to our users.

2nd Check for SVG availability and ship it

Once the SVGs creation is done things get easier and more straight forward. We've defined a createPrimitiveSvg function in our code base that takes an image and its format as arguments and returns the final SVG code packed into a base64-based data-uri.

function createPrimitiveSvg($image, $format) {
  if(inSvgCache($image)) {
    $svgFromCache = loadFromSvgCache($image); // svg already available, get it
    return makeBase64DataUri($svgFromCache);
  } else {
    pushToSvgGenerationQueue($image, $format); // svg not available, make sure it will be made
    return getBlurryImageUrl($image); // ship old lazyload fallback
  }
}

Our PHP createPrimitiveSvg function processing SVGs for our templates

Further on using this function and passing the image's root data from our cms to it looks like this.

<div class="imageContainer">
  <img
    class="js-lazyload"
    src='{$image->url|createPrimitiveSvg:"16:9"}'
  />
</div>

<div class="imageContainer">
  <img
    class="js-lazyload"
    src='{$image->url|createPrimitiveSvg:"4:3"}'
  />
</div>

3rd Proper SVG lazyload placeholder frontend handling

appear.js does a great job checking whether a targeted asset in the DOM has reached the viewport. Using it to do so we can manage lazyloading on the frontend using pretty much lightweight code.

appear({
  elements: () => document.querySelectorAll("img.js-lazyload"),
  appear: (element) => {
    const img = $(`<img class='${element.className} image--fadeIn' alt='${element.alt}' />`)
      .attr('src', element.dataset.original)
      .bind('load', () => {
        img.insertAfter(element);
        window.setTimeout(() => {
          $(element).remove();
        }, 2000);
      });
  }
});

Let's see what this does:

  1. Call appear(config: Object) initializing all config object properties appear.js requires
  2. When the lazyload SVG image has reached the users's viewport, the appear callback handles the high-resolution image tag creation
  3. Once the high-resolution image has loaded, it will be inserted behind the already displayed SVG
  4. The SVG image will fade out using CSS3 and its blur transition options
  5. Removal of the lazyload SVG image from the DOM after 2 seconds

That's it!

Conclusion

We are super happy with the results and gains we profit from having SVG-based lazyloading implemented in our Blue World. In case you like the perceived user experience at least a little bit. Give it a try. It makes so much more fun seeing all of this happening in your own projects. Happy coding, happy lazyloading!

Discussion

pic
Editor guide
Collapse
jeansberg profile image
Jens Genberg

Very interesting. The end result looks great!

Collapse
manpenaloza profile image
Manuel Penaloza Author

Hey Jens, glad you like it. Thx!

Collapse
beaniie profile image
Lewis 'Beaniie' Elborn

I searched high and low for this article, really obscure to find! xD

Found it now though, awesome post!

Collapse
manpenaloza profile image
Manuel Penaloza Author

Hey Lewis!
Awesome! Glad you liked it or maybe it even helped to implement it on your own.
all the best, Manuel