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:
- WebP is the new cool image format consuming less space and a good alternative to png/jpeg
- Images should be only loaded when it is visible on the page
- WebP is not universally accepted and has less backward compatibility
- Using image placeholder can be tricky as you need to maintain the ratio of the image that was supposed to be served.
- 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 es6
import 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 anyexport 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"
/>
);
};
Looking for more optimizations with React? Tryout my boilerplate https://www.reactpwa.com and do not forget to give a star to our repository at https://github.com/Atyantik/react-pwa & https://github.com/Atyantik/pawjs