Cloudinary Blog

Low Quality Image Placeholders (LQIP) Explained

Automate Placeholder Generation and Accelerate Page Loads

If you run a Google search on LQIP you’ll see very few relevant articles, very little guidance, and definitely no Wikipedia articles. In this post, we’ll discuss some of the feedback on LQIP we have gathered from the community and suggest and open for conversation a few approaches based on the built-in capabilities of the Cloudinary service. Specifically, we’ll explain what LQIP are, where they are best used, and how you can leverage them to accelerate page loads and optimize user experience.

Webinar
How to Optimize for Page Load Speed

LQIP Background

LQIP was originally introduced to enable web pages to load correctly in an orderly manner, simultaneously deriving extremely small-size, low-quality images to replace the content during the loading process of the actual images. From a use-case perspective, LQIP was best used in conjunction with JavaScript lazy loading. Then a dilemma emerged: Should we add more JavaScript to help images load faster, when it is actually the same JavaScript that we need to wait for before the images can load? It was a bit of a chicken-and-egg situation.

The Current Options

That situation was similar to today’s debate on achieving responsive images with the available technologies: which option is the most practical in terms of performance and scalability and whether JavaScript can play any relevant role. Here are the options:

  • Leverage HTML5's image attributes, such as srcset and sizes, to make images responsive. That is, always trust the browser to make the right decision according to image densities, which actually results in a complete lack of control over such elements as Device Pixel Ratio (DPR). Given that different browsers treat srcset differently, it is very difficult to scale and deliver a consistent cross-browser experience. Also, applying inline HTML code to each and every image is a labor-intensive chore.
  • Implement Google's HTTP Client Hints (server side), which were at some point touted as the Holy Grail of automation for image optimization strategies. Currently, Client Hints are supported by Chrome and Opera browsers only and thus not an ideal cross-browser option. We at Cloudinary are constantly monitoring new and exciting technologies and are certainly keeping an eye on Client Hints. As soon as they work on other major browsers, Client Hints will most likely become the new golden standard.
  • Though probably not the most ideal solution, the strongest candidate is still a client-side, JavaScript-based, responsive library because it can acquire all the pertinent image information, such as the accurate width and height, viewport, the browser’s user agent, and DPR, and pass on those details to Cloudinary. Cloudinary then transforms the master image on the fly to the perfect size and characteristics, irrespective of the device, window size, orientation, or resolution.

To learn more about responsive images and the process of adapting website images to multiple screen sizes, regardless of your favorite option, check out the free Cloudinary tool Responsive Image Breakpoints Generator.

The LQIP Options

Given the above, is JavaScript the best solution for allowing temporary image placeholders to load while the actual images are still preloading in the background? There is certainly a combination of things that can make JavaScript very reliable and perhaps the best suited for user experience. You guessed it: LQIP, again. Serving the placeholders until the originals are preloaded and then swapping the placeholders with the actual images is also something that can be achieved with JavaScript.

Let's look at some of the options available for LQIP today:

  • For responsive design and responsive images, displaying white space instead of image placeholders is not an option. Layout changes and content shifts are definitely detrimental to both user experience and performance. An alternative is generic image placeholders, but they don’t look tailored nor suggestive that content is still loading.
  • A better alternative could a simple, solid-color image (perhaps based on the predominant color in the discovered palette) with a simple or gradient option to fill in while the main image preloads. Fun fact: Cloudinary automatically detects and maps out (even for advanced search) the color palette and the predominant colors so that you can perform such transformations on the fly.
  • Use a Scalable Vector Graphics (SVG) object as the preview image, aka SQIP, as in the following code:
Copy to clipboard
<img src=“sample.jpg”
    style="background-size: cover; background-image:
    url(data:image/svg+xml;base64,<svg text>);"

For the scope and purpose of this post, we’ll focus on LQIP only and not on their more exciting and snazzier sibling, SQIP (SVG image placeholders). Even though SQIP is definitely an elegant approach with a great user experience and an accurate representation of the original image at infinitely smaller payloads, it is not scalable, requiring preparation work, numerous resources, and processing time. We would recommend this article that covers the topic very well, in our opinion: https://jmperezperez.com/svg-placeholders/

The Recommended LQIP Option

The alternative we suggest is effortless and on-the-fly. It also achieves comparable results to the above. Take a scaled-up, tiny image that is compressed for quality, potentially also chroma subsampled, blurred or pixelated, for which adding an f_auto parameter to the image URL would also deliver color blending, such as in a WebP format. To that you can then add color effects, such as grayscale, black and white, colorized, or various hues. Because Cloudinary supports URL-based parameters for instant manipulation of images and videos, all you need to do is upload any image and Cloudinary automatically performs all the transformations for you. Before you start, register for a free account on Cloudinary.

LQIP Examples

Here are a few examples:

  • Original image: 640 pixels wide, 40 KB, JPG format

    Ruby:
    Copy to clipboard
    cl_image_tag("string_1.jpg", :width=>640, :crop=>"scale")
    PHP:
    Copy to clipboard
    cl_image_tag("string_1.jpg", array("width"=>640, "crop"=>"scale"))
    Python:
    Copy to clipboard
    CloudinaryImage("string_1.jpg").image(width=640, crop="scale")
    Node.js:
    Copy to clipboard
    cloudinary.image("string_1.jpg", {width: 640, crop: "scale"})
    Java:
    Copy to clipboard
    cloudinary.url().transformation(new Transformation().width(640).crop("scale")).imageTag("string_1.jpg");
    JS:
    Copy to clipboard
    cloudinary.imageTag('string_1.jpg', {width: 640, crop: "scale"}).toHtml();
    jQuery:
    Copy to clipboard
    $.cloudinary.image("string_1.jpg", {width: 640, crop: "scale"})
    React:
    Copy to clipboard
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
    </Image>
    Vue.js:
    Copy to clipboard
    <cld-image publicId="string_1.jpg" >
      <cld-transformation width="640" crop="scale" />
    </cld-image>
    Angular:
    Copy to clipboard
    <cl-image public-id="string_1.jpg" >
      <cl-transformation width="640" crop="scale">
      </cl-transformation>
    </cl-image>
    .Net:
    Copy to clipboard
    cloudinary.Api.UrlImgUp.Transform(new Transformation().Width(640).Crop("scale")).BuildImageTag("string_1.jpg")
    Android:
    Copy to clipboard
    MediaManager.get().url().transformation(new Transformation().width(640).crop("scale")).generate("string_1.jpg");
    iOS:
    Copy to clipboard
    imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setWidth(640).setCrop("scale")).generate("string_1.jpg")!, cloudinary: cloudinary)
    Original image

  • The LQIP version: same size, compressed for quality, grayscale effect, optimized for WebP format

    Ruby:
    Copy to clipboard
    cl_image_tag("string_1.jpg", :transformation=>[
      {:width=>640, :crop=>"scale"},
      {:effect=>"blur:1000", :quality=>1},
      {:effect=>"grayscale"}
      ])
    PHP:
    Copy to clipboard
    cl_image_tag("string_1.jpg", array("transformation"=>array(
      array("width"=>640, "crop"=>"scale"),
      array("effect"=>"blur:1000", "quality"=>1),
      array("effect"=>"grayscale")
      )))
    Python:
    Copy to clipboard
    CloudinaryImage("string_1.jpg").image(transformation=[
      {'width': 640, 'crop': "scale"},
      {'effect': "blur:1000", 'quality': 1},
      {'effect': "grayscale"}
      ])
    Node.js:
    Copy to clipboard
    cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]})
    Java:
    Copy to clipboard
    cloudinary.url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("grayscale")).imageTag("string_1.jpg");
    JS:
    Copy to clipboard
    cloudinary.imageTag('string_1.jpg', {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]}).toHtml();
    jQuery:
    Copy to clipboard
    $.cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]})
    React:
    Copy to clipboard
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
      <Transformation effect="blur:1000" quality="1" />
      <Transformation effect="grayscale" />
    </Image>
    Vue.js:
    Copy to clipboard
    <cld-image publicId="string_1.jpg" >
      <cld-transformation width="640" crop="scale" />
      <cld-transformation effect="blur:1000" quality="1" />
      <cld-transformation effect="grayscale" />
    </cld-image>
    Angular:
    Copy to clipboard
    <cl-image public-id="string_1.jpg" >
      <cl-transformation width="640" crop="scale">
      </cl-transformation>
      <cl-transformation effect="blur:1000" quality="1">
      </cl-transformation>
      <cl-transformation effect="grayscale">
      </cl-transformation>
    </cl-image>
    .Net:
    Copy to clipboard
    cloudinary.Api.UrlImgUp.Transform(new Transformation()
      .Width(640).Crop("scale").Chain()
      .Effect("blur:1000").Quality(1).Chain()
      .Effect("grayscale")).BuildImageTag("string_1.jpg")
    Android:
    Copy to clipboard
    MediaManager.get().url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("grayscale")).generate("string_1.jpg");
    iOS:
    Copy to clipboard
    imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation()
      .setWidth(640).setCrop("scale").chain()
      .setEffect("blur:1000").setQuality(1).chain()
      .setEffect("grayscale")).generate("string_1.jpg")!, cloudinary: cloudinary)
    The LQIP version
    This image takes up only 2.17 KB in WebP format or 1.46 KB in JPG format in non-Chrome browsers.

  • See the same image with a black-and-white effect.

    Ruby:
    Copy to clipboard
    cl_image_tag("string_1.jpg", :transformation=>[
      {:width=>640, :crop=>"scale"},
      {:effect=>"blur:1000", :quality=>1},
      {:effect=>"blackwhite"}
      ])
    PHP:
    Copy to clipboard
    cl_image_tag("string_1.jpg", array("transformation"=>array(
      array("width"=>640, "crop"=>"scale"),
      array("effect"=>"blur:1000", "quality"=>1),
      array("effect"=>"blackwhite")
      )))
    Python:
    Copy to clipboard
    CloudinaryImage("string_1.jpg").image(transformation=[
      {'width': 640, 'crop': "scale"},
      {'effect': "blur:1000", 'quality': 1},
      {'effect': "blackwhite"}
      ])
    Node.js:
    Copy to clipboard
    cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]})
    Java:
    Copy to clipboard
    cloudinary.url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("blackwhite")).imageTag("string_1.jpg");
    JS:
    Copy to clipboard
    cloudinary.imageTag('string_1.jpg', {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]}).toHtml();
    jQuery:
    Copy to clipboard
    $.cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]})
    React:
    Copy to clipboard
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
      <Transformation effect="blur:1000" quality="1" />
      <Transformation effect="blackwhite" />
    </Image>
    Vue.js:
    Copy to clipboard
    <cld-image publicId="string_1.jpg" >
      <cld-transformation width="640" crop="scale" />
      <cld-transformation effect="blur:1000" quality="1" />
      <cld-transformation effect="blackwhite" />
    </cld-image>
    Angular:
    Copy to clipboard
    <cl-image public-id="string_1.jpg" >
      <cl-transformation width="640" crop="scale">
      </cl-transformation>
      <cl-transformation effect="blur:1000" quality="1">
      </cl-transformation>
      <cl-transformation effect="blackwhite">
      </cl-transformation>
    </cl-image>
    .Net:
    Copy to clipboard
    cloudinary.Api.UrlImgUp.Transform(new Transformation()
      .Width(640).Crop("scale").Chain()
      .Effect("blur:1000").Quality(1).Chain()
      .Effect("blackwhite")).BuildImageTag("string_1.jpg")
    Android:
    Copy to clipboard
    MediaManager.get().url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("blackwhite")).generate("string_1.jpg");
    iOS:
    Copy to clipboard
    imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation()
      .setWidth(640).setCrop("scale").chain()
      .setEffect("blur:1000").setQuality(1).chain()
      .setEffect("blackwhite")).generate("string_1.jpg")!, cloudinary: cloudinary)
    black and white image

Or this one with a Cartoonify effect.

Ruby:
Copy to clipboard
cl_image_tag("string_1.jpg", :transformation=>[
  {:width=>640, :crop=>"scale"},
  {:effect=>"blur:1000", :quality=>1},
  {:effect=>"cartoonify"}
  ])
PHP:
Copy to clipboard
cl_image_tag("string_1.jpg", array("transformation"=>array(
  array("width"=>640, "crop"=>"scale"),
  array("effect"=>"blur:1000", "quality"=>1),
  array("effect"=>"cartoonify")
  )))
Python:
Copy to clipboard
CloudinaryImage("string_1.jpg").image(transformation=[
  {'width': 640, 'crop': "scale"},
  {'effect': "blur:1000", 'quality': 1},
  {'effect': "cartoonify"}
  ])
Node.js:
Copy to clipboard
cloudinary.image("string_1.jpg", {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation()
  .width(640).crop("scale").chain()
  .effect("blur:1000").quality(1).chain()
  .effect("cartoonify")).imageTag("string_1.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('string_1.jpg', {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("string_1.jpg", {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]})
React:
Copy to clipboard
<Image publicId="string_1.jpg" >
  <Transformation width="640" crop="scale" />
  <Transformation effect="blur:1000" quality="1" />
  <Transformation effect="cartoonify" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="string_1.jpg" >
  <cld-transformation width="640" crop="scale" />
  <cld-transformation effect="blur:1000" quality="1" />
  <cld-transformation effect="cartoonify" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="string_1.jpg" >
  <cl-transformation width="640" crop="scale">
  </cl-transformation>
  <cl-transformation effect="blur:1000" quality="1">
  </cl-transformation>
  <cl-transformation effect="cartoonify">
  </cl-transformation>
</cl-image>
.Net:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation()
  .Width(640).Crop("scale").Chain()
  .Effect("blur:1000").Quality(1).Chain()
  .Effect("cartoonify")).BuildImageTag("string_1.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation()
  .width(640).crop("scale").chain()
  .effect("blur:1000").quality(1).chain()
  .effect("cartoonify")).generate("string_1.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation()
  .setWidth(640).setCrop("scale").chain()
  .setEffect("blur:1000").setQuality(1).chain()
  .setEffect("cartoonify")).generate("string_1.jpg")!, cloudinary: cloudinary)
Cartoonify

The possibilities are truly endless, promising a tremendous effect on the user perspective without sacrificing performance or image weight. Many effects abound, including Instagram-like filters, which you can mix and match to obtain the desired result. Feel free to let your imagination run wild with Cloudinary’s Neural Artwork Style Transfer add-on.

Simplification With Named Transformation Classes

For ease of use, you can simplify and shorten those long, complex URLs by leveraging a feature called named transformations, which are akin to CSS classes. To group together multiple URL-based, chained transformations and turning them into templates, follow these simple steps:

  1. After creating your preferred LQIP transformation for an image from your Cloudinary account, log in to your Cloudinary account and navigate to the Transformation menu. Click Edit before the most recent dynamic transformation.
  2. In the next screen, save a template, also known as a named transformation, with a simple name of your choice, such as lqip. Alternatively, you can start directly from the Image Transformations screen (see below) to create a newly named transformation: click Transformation and then click Create a new transformation on the top-right corner, and, finally, save it with your preferred name.

Console

The output shows a significantly streamlined URL in place of the long, complicated one before, such as something like this— http://res.cloudinary.com/demo/image/upload/w_640,f_auto/t_lqip/string_1.jpg —where t_lqip actually encompasses most of the manipulations performed on the image and can even conceal resizing, cropping, text or image overlays, and other complex transformations.

Conclusion

We hope that this post has clued you in on how you can leverage Cloudinary to automate the process of generating image placeholders, simultaneously reducing development time and effort. We welcome your feedback and would appreciate your sharing with us your experience, preferably with examples, with the options we described earlier.

Regardless of your media-performance strategies, we recommend that you adopt preferred flavors of low-quality image placeholders, especially in situations in which you are already leveraging lazy loading. Keep in mind the tests that we ran and the enhancements to performance and loading times you would gain by taking advantage of the Cloudinary options that we recommended.

About Cloudinary

Cloudinary provides easy-to-use, cloud-based media management solutions for the world’s top brands. With offices in the US, UK and Israel, Cloudinary has quickly become the de facto solution used by developers and marketers at major companies around the world to streamline rich media management and deliver optimal end-user experiences.

For more information, visit www.cloudinary.com or follow us on Twitter.


Additional Resources:

Recent Blog Posts

Maya Shavin: How I Built My Website

Besides working as a senior front-end developer at Cloudinary, I'm also a content creator, a blogger, and an open-source developer. Follow me at @mayashavin and on mayashavin.com.

In the beginning, my website, mayashavin.com, was mainly for showcasing the status of my development projects and keeping me organized with my speaking schedule. Initially, I built it with Vue.js, later on switching to Nuxt.js (aka Nuxt) for a higher SEO score, and deployed it with Netlify. After some time, I added a blog section with Netlify CMS as the content management system (CMS). Everything was fine until I added more content and features, which led to a significant decline in the site’s performance. Also, the site design needed a modern look. So, I gave the site a makeover.

Read more
Automation Frees Up PetRescue’s Staff to Help Pets Find Their Forever Homes

As we spend more time at home, many of us are adopting pets for the joy, companionship and a surprising range of health benefits. In Australia, where our nonprofit customer PetRescue is located, there’s a shortage of pets to adopt. Last August, the Guardian reported that dog shelters in Australia emptied and adoption fees for puppies were running as high as $AUS1800.

Read more
Cloudinary and Contentful Make Modern Content Management Easier

I am pleased to share that Cloudinary and Contentful have joined forces to further streamline the creation, processing, and delivery of online content through Cloudinary’s digital asset management (DAM) solution and advanced transformation and delivery capabilities for images and video. What’s more, the partnership delivers a headless approach to DAM. By leveraging APIs for media management tasks, marketers and developers alike benefit from an integrated stack of optimized assets for optimization and automation. As a result, page loads are fast and beautiful, and at scale—with less overhead and effort.

Read more
Introducing Cloudinary's Nuxt Module

Since its initial release in October 2016 by the Chopin brothers as a server-side framework that runs on top of Vue.js, Nuxt (aka Nuxt.js) has gained prominence in both intuitiveness and performance. The framework offers numerous built-in features based on a modular architecture, bringing ease and simplicity to web development. Not surprisingly, Nuxt.js has seen remarkable growth in adoption by the developer community along with accolades galore. At this writing, Nuxt has earned over 30K stars on GitHub and 96 active modules with over a million downloads per month. And the upward trend is ongoing.

Read more
How Quality and Quantity can go Hand in Hand

When it comes to quality versus quantity, you’ll often hear people say, “It’s the quality that counts, not the quantity”. While that’s true in many situations, there are also cases where you want both quality and quantity. You may have thousands of images on your website and you want them all to look great. This is especially important if your website allows users to upload their own content, for example, to sell their own products or services. You don't want their poor quality images to reflect badly on your brand.

Read more