1. 程式人生 > >Use WebP images along with other fallback sources to and placeholder to get max image optimization…

Use WebP images along with other fallback sources to and placeholder to get max image optimization…

Use WebP images along with other fallback sources and placeholder to get max image optimization on the website

I was figuring out the best way to AUTO optimize images and their client loading in the project I was working on and I figured out the following:

  1. WebP is the new cool image format consuming less space and a good alternative to png/jpeg
  2. Images should be only loaded when it is visible on the page
  3. WebP is not universally accepted and has less backward compatibility
  4. Using image placeholder can be tricky as you need to maintain the ratio of the image that was supposed to be served.
  5. I do not need to load 500w of an image on 200w device thus I can use srcset for the purpose

Well let us try to solve the problem one by one:

I wanted to use .webp format for all the images so I decided to convert them to .webp, now all my code would look like below:

<img src="/path/to/image.webp" alt="An Image" />

But now, only Chrome users can see the images as other browsers do not support them, check out the usability at

https://caniuse.com/#feat=webp

So now I need a fallback for the browsers that do not support WebP. Luckily I found the <picture> tag and thus I have the same image of two type on my server, one .png and one .webp and I can show the image as below:

<picture>  <source type="image/webp" srcset="/path/to/image.webp" />  <img src="/path/to/image.png" alt="An Image" /></picture>

Thus browsers supporting WebP would now show WebP images and other browsers can show the png format.

But this was not enough, I wanted to use the JavaScript ability to load the image at a later stage to save user bandwidth, thus I researched more and found that setInterval & setTimeout are not an optimal way to monitor user behavior when then scroll to the image and thus finally came across the IntersectionObserver API, lucky again!

<picture class="lazy-picture">  <source type="image/webp" data-srcset="/path/to/image.webp" />  <source type="image/png" data-srcset="/path/to/image.png" />  <img src="/path/to/placeholder.jpg" alt="Am image" /></picture>
<script>
const lazyPictures = document.querySelectorAll('.lazy-picture');
function loadPicture(picture) {  lazyPictures.forEach(pic => {    Array      .from(pic.getElementsByTagName("source"))      .forEach(function(source) {        source.setAttribute(          "srcset",           source.getAttribute("data-srcset")        );      });    });}
if ('IntersectionObserver' in window) {  const options = {    rootMargin: '0px',    threshold: 0.1,  };  let observer = new IntersectionObserver( function(entries) {    entries.forEach(function(entry) {      if (entry.intersectionRatio > 0) {        const picture = entry.target;        observer.unobserve(picture);        loadPicture(picture);      }    });  }, options);    lazyPictures.forEach(function(picture) {    observer.observe(picture);  });} else {  lazyPictures.forEach(function(picture) {    loadPicture(picture);  });}
</script>

The above solved my problem for loading an appropriate image at the appropriate time, but this was not enough for the performance, I didn’t want the user to load a 1024px wide picture when not viewing in desktop mode.

Thus the srcset helped me out there and I was able to set multiple images in as below:

<picture class="lazy-picture">  <source type="image/webp" data-srcset="/path/to/image-200.webp 200w, /path/to/image-400.webp 400w, /path/to/image-800.webp 800w, /path/to/image-1000.webp 1000w, /path/to/image-2000.webp 2000w" />  <source type="image/png" data-srcset="/path/to/image-200.png 200w, /path/to/image-400.png 400w, /path/to/image-800.png 800w, /path/to/image-1000.png 1000w, /path/to/image-2000.png 2000w" />  <img src="/path/to/placeholder.jpg" alt="Am image" /></picture>

Now everything is awesome for the IntersectionObserver, My browser still has to execute an extra HTTP request for the placeholder image which I don’t personally like to get loaded over HTTP, thus I replaced the `/path/to/placeholder.jpg` to a base64 encoded

<picture class="lazy-picture">
  <!-- WebP Source with srcset -->  <source type="image/webp" data-srcset="/path/to/image-200.webp 200w, /path/to/image-400.webp 400w, /path/to/image-800.webp 800w, /path/to/image-1000.webp 1000w, /path/to/image-2000.webp 2000w" />
  <!-- PNG Source with srcset -->  <source type="image/png" data-srcset="/path/to/image-200.png 200w, /path/to/image-400.png 400w, /path/to/image-800.png 800w, /path/to/image-1000.png 1000w, /path/to/image-2000.png 2000w" />
  <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOsq6mrBwAE8QH5A52ECwAAAABJRU5ErkJggg==" alt="Am image" />
</picture>

Ok, the above looks pretty good, ignoring the fact that implementing the above is pretty impractical in a real project. Why? because one cannot convert all the images manually and resize them just the get the above output.

Well, if you are using a WebPack I can help you with that. I create a loader pwa-srcset-loader for WebPack 4 inspired from srcset-loader so if you visit the documentation you see a simple require request as below:

require("./resources/path/to/image.png?sizes=200w+800w&placeholder");
// or with es6import Image from "./resources/path/to/image?sizes=200w+800w&placeholder";
// output/* [{ "sources": {  "400w": "/images/d92e2c1d0d6e7c6240f4977800b3a4c0.png",  "800w": "/images/9816a2ba208750bf6b3327eaff83cc5a.png" }, "type": "image/png", "srcSet": "/images/d92e2c1d0d6e7c6240f4977800b3a4c0.png 400w,/images/9816a2ba208750bf6b3327eaff83cc5a.png 800w", "placeholder": {  "color": [235, 235, 235, 1],  "url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjAgMTMiPgogICAgICAgICAgPGZpbHRlciBpZD0ieCI+CiAgICAgICAgICAgIDxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjEiIC8+CiAgICAgICAgICA8L2ZpbHRlcj4KICAgICAgICAgIDxpbWFnZSB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bGluazpocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUJRQUFBQU5DQUlBQUFBbU10a0pBQUFBQ1hCSVdYTUFBQllsQUFBV0pRRkpVaVR3QUFBQkkwbEVRVlFvejVXUzZZNkNVQXlGZmY4bjRnbjhBNUlJS0VUREptNFFJanV5R0RMZmNBa3hFOFBFazFEYTB0TjdTdS9xT2FKdDJ5ekw2cnArZm9NVmhHRVlORTJUSk9uMWV0V2ZzRVRtMlB2OW5pUkozL2Y0WGRmaGRHK2c3bU9qaVJ6SDhmRjRkQnpIOTMzYnRnK0hnK2Q1aEpabGtjL3puQmFVQ2M3YzRwZmNOTTNqOGJoY0xtRVlZdUdmeitjZ0NJUUY2T0lUdGlnSzBRWEtSQ2FtVGxWVldaYTMyKzErdjhmcXVvN2Q3WGFtYWVJZ1liUFpvSWp1dEtNTC9FbDJGRVd1Njk1dXQ2cXF5SlJsT1ZzeWxBbzdReWhmOGRDRHYwV0s2bXdFZnBxbWpJcHRSb2l5R2RQTTRrWE15ZXYxR20zb1Z4VEZNQXlrTXVmU251Y2ZpSEoveE9sMHVsNnY2Q1F2OXZRUGVkWXBwQkwrMmNvUytYMlM3NjZuMkROU3Y3M2JQeWtpM1RBL0RWZnJBQUFBQUVsRlRrU3VRbUNDIiBmaWx0ZXI9InVybCgjeCkiLz4KICAgICAgICA8L3N2Zz4=",  "ratio": 1.4731800766283525 }}, { "sources": {  "400w": "/images/f9ef707b92eae5c6e0ba3a6ba94fae5e.webp",  "800w": "/images/7c2961f91fdcf7ab6f029e18df9564b6.webp" }, "type": "image/webp", "srcSet": "/images/f9ef707b92eae5c6e0ba3a6ba94fae5e.webp 400w,/images/7c2961f91fdcf7ab6f029e18df9564b6.webp 800w", "placeholder": {  "color": [235, 235, 235, 1],  "url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMjAgMTMiPgogICAgICAgICAgPGZpbHRlciBpZD0ieCI+CiAgICAgICAgICAgIDxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjEiIC8+CiAgICAgICAgICA8L2ZpbHRlcj4KICAgICAgICAgIDxpbWFnZSB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bGluazpocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQUJRQUFBQU5DQUlBQUFBbU10a0pBQUFBQ1hCSVdYTUFBQllsQUFBV0pRRkpVaVR3QUFBQkkwbEVRVlFvejVXUzZZNkNVQXlGZmY4bjRnbjhBNUlJS0VUREptNFFJanV5R0RMZmNBa3hFOFBFazFEYTB0TjdTdS9xT2FKdDJ5ekw2cnArZm9NVmhHRVlORTJUSk9uMWV0V2ZzRVRtMlB2OW5pUkozL2Y0WGRmaGRHK2c3bU9qaVJ6SDhmRjRkQnpIOTMzYnRnK0hnK2Q1aEpabGtjL3puQmFVQ2M3YzRwZmNOTTNqOGJoY0xtRVlZdUdmeitjZ0NJUUY2T0lUdGlnSzBRWEtSQ2FtVGxWVldaYTMyKzErdjhmcXVvN2Q3WGFtYWVJZ1liUFpvSWp1dEtNTC9FbDJGRVd1Njk1dXQ2cXF5SlJsT1ZzeWxBbzdReWhmOGRDRHYwV0s2bXdFZnBxbWpJcHRSb2l5R2RQTTRrWE15ZXYxR20zb1Z4VEZNQXlrTXVmU251Y2ZpSEoveE9sMHVsNnY2Q1F2OXZRUGVkWXBwQkwrMmNvUytYMlM3NjZuMkROU3Y3M2JQeWtpM1RBL0RWZnJBQUFBQUVsRlRrU3VRbUNDIiBmaWx0ZXI9InVybCgjeCkiLz4KICAgICAgICA8L3N2Zz4=",  "ratio": 1.4731800766283525 }}]*/

PRETTY COOL! RIGHT?

Are you using PawJS & ReactPWA for your project?

if so than it is great news, try out @pawjs/srcset plugin to implement it in a blink of an eye:

Installation:

npm i @pawjs/srcset --save

Edit/Create /src/webpack.js

import Srcset from "@pawjs/srcset/webpack";
// ... other imports if any
export default class ProjectWebpack {  constructor({addPlugin}) {    addPlugin(new Srcset());  }}

And then you can use the images as below:

import BigBannerImage from "../../resources/image/banner.png?sizes=200w+400w+800w+1000w&placeholder";import Picture from "@pawjs/srcset/picture";
// then inside your component you use that image as something like:export default () => {  return (    <Picture      image={BigBannerImage}      alt="banner"      pictureClassName="picture-class"      imgClassName="img-class"    />  );};