I've created a script that utilises the element with webp and jpg fallbacks with some lazy-loading to serve very high-quality images. So far it works perfectly in every 'modern' browser except iOS11 Safari.
I have implemented a fallback for IntersectionObserver, as I know this isn't entirely supported yet, which uses getClientBoundingRect() to find the position of the images within the viewport. I've checked on iOS 10, 11 and then upgraded one of the same devices to iOS 12 where the images began loading.
<div class="lazy-container">
<picture class="lazy-thumbnail">
<source srcset="{{ asset( '/images/media/placeholders/3.webp' ) }}" type="image/webp">
<source srcset="{{ asset( '/images/media/placeholders/3.jpg' ) }}" type="image/jpeg">
<img class="" src="{{ asset( '/images/media/placeholders/3.jpg' ) }}" />
</picture>
<picture data-lazy>
<source data-srcset="{{ asset( '/images/media/3.webp' ) }}" type="image/webp">
<source data-srcset="{{ asset( '/images/media/3.jpg' ) }}" type="image/jpeg">
<img class="" src="{{ asset( '/images/media/3.jpg' ) }}" />
</picture>
</div>
const lazyObserver = new IntersectionObserver( ( entries, observer ) => {
// Break into function to allow immediate calling
const exchangeImage = ( image, source ) => {
source.forEach( ( s ) => {
s.setAttribute( 'srcset',
s.getAttribute( 'data-srcset' ) );
});
image.previousElementSibling.classList.remove( 'lazy-thumbnail' );
image.previousElementSibling.classList.add( 'ex-lazy-thumbnail' );
image.classList.add( 'ex-lazy' );
image.removeAttribute( 'data-lazy' );
// If the image is in view and src has been swapped out, stop observing the image
lazyObserver.unobserve( image );
};
entries.forEach( ( entry ) => {
const image = entry.target;
const source = image.querySelectorAll( 'source' );
// If the image is either in view or data-lazy='immediate', load the image.
if( entry.target.dataset.lazy === 'immediate' || entry.isIntersecting ){
exchangeImage( image, source );
}
});
});
The picture element should go through and load the first image srcset that works for the browser, webp in most instances, then with these selected the JS should run when the image is in view, swap out the srcset for the data-srcset with the large image and load. This works on every browser except
.lazy-container {
overflow: hidden;
position: relative;
picture {
width: 100%;
position: absolute;
top: 0;
left: 0;
img {
display: block;
width: 100%;
height: auto;
}
}
}
.ex-lazy {
filter: blur(0px);
transition: all 500ms ease-in-out;
}
.lazy-thumbnail {
opacity: 1;
filter: blur(8px);
transition: all 500ms ease-in-out;
z-index: 1;
}
.ex-lazy-thumbnail {
opacity: 0;
filter: blur(0px);
transition: all 500ms ease-in-out;
z-index: 0;
}
[data-lazy]{
width: 100%;
height: auto;
filter: blur(8px);
transition: all 500ms ease-in-out;
}
I solved this in my way. Also look at polyfill can help polyfill
let images = document.querySelectorAll('source, img');
if ('IntersectionObserver' in window) {
// we check if IntersectionObserver is supported by our browsers
let config = {
root: null,
rootMargin: '0px',
threshold: 0.5
};
let observer = new IntersectionObserver(onChange, config);
images.forEach(function (img) { observer.observe(img) });
function onChange(changes, observer) {
changes.forEach(function (change) {
if (change.intersectionRatio > 0) {
// we stop observing and loading pictures
loadImage(change.target);
observer.unobserve(change.target);
}
});
}
} else {
// if IntersectionObserver is not supported, we load all photos
images.forEach(function (image) { loadImage(image) });
}
function loadImage(image) {
image.classList.add('fade-in');
if (image.dataset && image.dataset.src) {
image.src = image.dataset.src;
}
if (image.dataset && image.dataset.srcset) {
image.srcset = image.dataset.srcset;
}
}
// -- How to add polyfil
const modernBrowser = ('IntersectionObserver' in window);
if (!modernBrowser) {
loadScripts([
"./polyfills/intersection-observer.js"
])
}
function loadScripts(array, callback) {
var loader = function (src, handler) {
var script = document.createElement("script");
script.src = src;
script.onload = script.onreadystatechange = function () {
script.onreadystatechange = script.onload = null;
handler();
}
var head = document.getElementsByTagName("head")[0];
(head || document.body).appendChild(script);
};
(function run() {
if (array.length != 0) {
loader(array.shift(), run);
} else {
callback && callback();
}
})();
}
<picture>
<source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
<source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
<img data-src="https://place-hold.it/100x100/15252D/fff">
<noscript><img src="https://place-hold.it/100x100/15252D/fff"></noscript>
</picture>