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

Hipcamp Optimizes Images and Improves Page Load Times With Cloudinary

When creating a website that allows campers to discover great destinations, Hipcamp put a strong emphasis on featuring high-quality images that showcased the list of beautiful locations, regardless of whether users accessed the site on a desktop, tablet, or phone. Since 2015, Hipcamp has relied on Cloudinary’s image management solution to automate cropping and image optimization, enabling instant public delivery of photos, automatic tagging based on content recognition, and faster loading of webpages. In addition, Hipcamp was able to maintain the high standards it holds for the look and feel of its website.

Read more
New Image File Format: FUIF: Why Do We Need a New Image Format

In my last post, I introduced FUIF, a new, free, and universal image format I’ve created. In this post and other follow-up pieces, I will explain the why, what, and how of FUIF.

Even though JPEG is still the most widely-used image file format on the web, it has limitations, especially the subset of the format that has been implemented in browsers and that has, therefore, become the de facto standard. Because JPEG has a relatively verbose header, it cannot be used (at least not as is) for low-quality image placeholders (LQIP), for which you need a budget of a few hundred bytes. JPEG cannot encode alpha channels (transparency); it is restricted to 8 bits per channel; and its entropy coding is no longer state of the art. Also, JPEG is not fully “responsive by design.” There is no easy way to find a file’s truncation offsets and it is limited to a 1:8 downscale (the DC coefficients). If you want to use the same file for an 8K UHD display (7,680 pixels wide) and for a smart watch (320 pixels wide), 1:8 is not enough. And finally, JPEG does not work well with nonphotographic images and cannot do fully lossless compression.

Read more
 New Image File Format: FUIF:Lossy, Lossless, and Free

I've been working to create a new image format, which I'm calling FUIF, or Free Universal Image Format. That’s a rather pretentious name, I know. But I couldn’t call it the Free Lossy Image Format (FLIF) because that acronym is not available any more (see below) and FUIF can do lossless, too, so it wouldn’t be accurate either.

Read more