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

An Eye-Opening Talk: Building Apps for the Next Billion Users in Africa

William (iChuloo) Imoh, who hails from Lagos, Nigeria, recently embarked on a U.S. speaking tour, February 20-March 12, during which he powwowed with technical and product teams and communities at such renowned enterprises as Netlify, Pluralsight, Lucidchart, Twilio, and more in Salt Lake City, Dallas, Las Vegas, and San Francisco. On March 5, he gave an enlightening talk, entitled International Developers and Development: Building for the Next Billion Users at Cloudinary in Santa Clara, California. Below is a synopsis. For details, see the related slides.

Read more
The Debut of the Cloudinary Customer Advisory Board

Focus on customers has always been Cloudinary’s mantra. Because we owe them our success, we are constantly reaching out to our customers, not just for feedback on our offerings, but also for their vision, wish list, and buy-in of what Cloudinary can do to meet their needs and make them succeed. About six months ago, it occurred to us that it would be beneficial if we could meet regularly with those who are behind innovation at our key customers—executives, product gurus, developers, content managers—to swap strategies, product roadmaps, best practices, and such. In particular, we’d like to solicit actionable feedback as a foundation for our plans of product enhancements.

Read more
Media Management With the Cloudinary-Netlify CMS Integration

Static sites and the JAMstack are quickly becoming a standard for developing safe and performant websites with an optimal workflow for developers. Netlify CMS (not to be confused with the company that created it, Netlify) is an open source content management solution that works especially with static site generators such as Gatsby, Hugo, etc... enabling content storage in your Git repository along with your code for easier versioning, multichannel publishing, and direct content updates in Git.

Read more
Vitaly Friedman's Insights on Media Conferences

Vitaly Friedman is a die-hard devotee of beautiful content. Born in Minsk, Belarus, he studied Computer Science and Mathematics in Germany, unearthing in himself a passion for typography, writing, and design in the interim. After a six-year stint as a freelance designer and developer, he co-founded Smashing Magazine, a leading online publication on web design and development. You can follow SmashingMag on Twitter @SmashingMag.

Read more