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.

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:
<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:
    cl_image_tag("string_1.jpg", :width=>640, :crop=>"scale")
    PHP:
    cl_image_tag("string_1.jpg", array("width"=>640, "crop"=>"scale"))
    Python:
    CloudinaryImage("string_1.jpg").image(width=640, crop="scale")
    Node.js:
    cloudinary.image("string_1.jpg", {width: 640, crop: "scale"})
    Java:
    cloudinary.url().transformation(new Transformation().width(640).crop("scale")).imageTag("string_1.jpg");
    JS:
    cloudinary.imageTag('string_1.jpg', {width: 640, crop: "scale"}).toHtml();
    jQuery:
    $.cloudinary.image("string_1.jpg", {width: 640, crop: "scale"})
    React:
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
    </Image>
    Angular:
    <cl-image public-id="string_1.jpg" >
      <cl-transformation width="640" crop="scale">
      </cl-transformation>
    </cl-image>
    .Net:
    cloudinary.Api.UrlImgUp.Transform(new Transformation().Width(640).Crop("scale")).BuildImageTag("string_1.jpg")
    Android:
    MediaManager.get().url().transformation(new Transformation().width(640).crop("scale")).generate("string_1.jpg");
    iOS:
    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:
    cl_image_tag("string_1.jpg", :transformation=>[
      {:width=>640, :crop=>"scale"},
      {:effect=>"blur:1000", :quality=>1},
      {:effect=>"grayscale"}
      ])
    PHP:
    cl_image_tag("string_1.jpg", array("transformation"=>array(
      array("width"=>640, "crop"=>"scale"),
      array("effect"=>"blur:1000", "quality"=>1),
      array("effect"=>"grayscale")
      )))
    Python:
    CloudinaryImage("string_1.jpg").image(transformation=[
      {'width': 640, 'crop': "scale"},
      {'effect': "blur:1000", 'quality': 1},
      {'effect': "grayscale"}
      ])
    Node.js:
    cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]})
    Java:
    cloudinary.url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("grayscale")).imageTag("string_1.jpg");
    JS:
    cloudinary.imageTag('string_1.jpg', {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]}).toHtml();
    jQuery:
    $.cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "grayscale"}
      ]})
    React:
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
      <Transformation effect="blur:1000" quality="1" />
      <Transformation effect="grayscale" />
    </Image>
    Angular:
    <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:
    cloudinary.Api.UrlImgUp.Transform(new Transformation()
      .Width(640).Crop("scale").Chain()
      .Effect("blur:1000").Quality(1).Chain()
      .Effect("grayscale")).BuildImageTag("string_1.jpg")
    Android:
    MediaManager.get().url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("grayscale")).generate("string_1.jpg");
    iOS:
    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:
    cl_image_tag("string_1.jpg", :transformation=>[
      {:width=>640, :crop=>"scale"},
      {:effect=>"blur:1000", :quality=>1},
      {:effect=>"blackwhite"}
      ])
    PHP:
    cl_image_tag("string_1.jpg", array("transformation"=>array(
      array("width"=>640, "crop"=>"scale"),
      array("effect"=>"blur:1000", "quality"=>1),
      array("effect"=>"blackwhite")
      )))
    Python:
    CloudinaryImage("string_1.jpg").image(transformation=[
      {'width': 640, 'crop': "scale"},
      {'effect': "blur:1000", 'quality': 1},
      {'effect': "blackwhite"}
      ])
    Node.js:
    cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]})
    Java:
    cloudinary.url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("blackwhite")).imageTag("string_1.jpg");
    JS:
    cloudinary.imageTag('string_1.jpg', {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]}).toHtml();
    jQuery:
    $.cloudinary.image("string_1.jpg", {transformation: [
      {width: 640, crop: "scale"},
      {effect: "blur:1000", quality: 1},
      {effect: "blackwhite"}
      ]})
    React:
    <Image publicId="string_1.jpg" >
      <Transformation width="640" crop="scale" />
      <Transformation effect="blur:1000" quality="1" />
      <Transformation effect="blackwhite" />
    </Image>
    Angular:
    <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:
    cloudinary.Api.UrlImgUp.Transform(new Transformation()
      .Width(640).Crop("scale").Chain()
      .Effect("blur:1000").Quality(1).Chain()
      .Effect("blackwhite")).BuildImageTag("string_1.jpg")
    Android:
    MediaManager.get().url().transformation(new Transformation()
      .width(640).crop("scale").chain()
      .effect("blur:1000").quality(1).chain()
      .effect("blackwhite")).generate("string_1.jpg");
    iOS:
    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:
cl_image_tag("string_1.jpg", :transformation=>[
  {:width=>640, :crop=>"scale"},
  {:effect=>"blur:1000", :quality=>1},
  {:effect=>"cartoonify"}
  ])
PHP:
cl_image_tag("string_1.jpg", array("transformation"=>array(
  array("width"=>640, "crop"=>"scale"),
  array("effect"=>"blur:1000", "quality"=>1),
  array("effect"=>"cartoonify")
  )))
Python:
CloudinaryImage("string_1.jpg").image(transformation=[
  {'width': 640, 'crop': "scale"},
  {'effect': "blur:1000", 'quality': 1},
  {'effect': "cartoonify"}
  ])
Node.js:
cloudinary.image("string_1.jpg", {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]})
Java:
cloudinary.url().transformation(new Transformation()
  .width(640).crop("scale").chain()
  .effect("blur:1000").quality(1).chain()
  .effect("cartoonify")).imageTag("string_1.jpg");
JS:
cloudinary.imageTag('string_1.jpg', {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]}).toHtml();
jQuery:
$.cloudinary.image("string_1.jpg", {transformation: [
  {width: 640, crop: "scale"},
  {effect: "blur:1000", quality: 1},
  {effect: "cartoonify"}
  ]})
React:
<Image publicId="string_1.jpg" >
  <Transformation width="640" crop="scale" />
  <Transformation effect="blur:1000" quality="1" />
  <Transformation effect="cartoonify" />
</Image>
Angular:
<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:
cloudinary.Api.UrlImgUp.Transform(new Transformation()
  .Width(640).Crop("scale").Chain()
  .Effect("blur:1000").Quality(1).Chain()
  .Effect("cartoonify")).BuildImageTag("string_1.jpg")
Android:
MediaManager.get().url().transformation(new Transformation()
  .width(640).crop("scale").chain()
  .effect("blur:1000").quality(1).chain()
  .effect("cartoonify")).generate("string_1.jpg");
iOS:
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.

Additional Resources:

Recent Blog Posts

CoreMedia Adds Cloudinary to its CoreMedia Studio Platform

Today we’re pleased to announce a new technology partnership with CoreMedia, a leading Content Experience Platform provider. CoreMedia users can now leverage Cloudinary’s web-based digital asset management (DAM) solution to organize, search, manage and optimize their media assets, including images and videos, and to orchestrate, preview and deliver digital experiences consistently and optimized across all channels and browsers. The official press release is available here.

Read more
Facial-Surveillance System for Restricted Zones

In Africa, where Internet access and bandwidth are limited, it’s not cost-effective or feasible to establish and maintain a connectivity for security and surveillance applications. That challenge makes it almost impossible to build a service that detects, with facial-recognition technology, if someone entering a building is authorized to do so. To meet the final-year research requirement for my undergraduate studies, I developed a facial-surveillance system. Armed with a background in computer vision, I decided to push the limits and see if I could build a surveillance system that does not require recording long video footage.

Read more
Complex Networks Case Study

Complex Networks has been using Cloudinary since 2014 to manage and optimize images across seven websites and two mobile apps, making editorial workflow more efficient, improving page performance and load time, and increasing user engagement. Cloudinary was instrumental in enabling Complex Networks to redesign its web properties. Without the flexibility that Cloudinary offers to both creative and development teams, it would not have been possible for Complex Networks to achieve such a fast time to market.

Read more
Best Practices for Optimizing Web Page Speed

If you're like most consumers today, you engage more with pictures or videos on a website than text. The stats don't lie - four times as many visitors would rather watch a video about a product than read about it, and sites with compelling images average twice as many views as text-heavy ones.

Read more
A day of fun with Girls Who Code and Cloudinary

During both my computer science studies and work in the tech field, there have not been a lot of women present. While our ranks have grown, women still make up only a small percentage. In many ways, I think the traditionally male-dominated world can be intimidating to women and girls who may be interested in pursuing these types of tech careers.

Read more
Chrome 67 Changed Client Hints for Responsive Images

In 2015, Chrome 35 added support for Client Hints. Client Hints are awesome! With Client Hints, you can simplify the HTML required for Responsive Images, ensure that images are crisp on high-DPR displays, and improve #webperf. At the time of writing, Chrome 35+ is the only browser to have adopted this standard. Cloudinary has introduced support with the w_auto, dpr_auto, and q_auto parameters.

Read more