So I found a potential solution using IntersectionObserver thanks to deanhume.com. It works beautifully in Chrome and newer versions of Firefox.
Sadly, we still code our site work work with IE 11 as our standard. I don't know why, but we do so I have to deal with that. The problem is this solution does not work with an older version of Firefox I use for validation nor does it work with IE 11. So I want to use a polyfill.
The GitHub page for the polyfill talks about using <script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
for older browser support, but it does not seem to work. I also placed the polyfill JavaScript file on the site and referenced it in the page head before any other Javascript, but it also does not seem to work.
What is the proper way to load a polyfill file?
The first line referencing it is: <script src="../pathto/polyfills/intersectionobserver.js"></script>
Then there is some unrelated stuff before getting to the Intersection Observer bit; CSS for the fade in and calling in the lazy-load script:
<style>
.fade-in {
animation-name: fadeIn;
animation-duration: 1.3s;
animation-timing-function: cubic-bezier(0, 0, 0.4, 1);
animation-fill-mode: forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.centered {
display:block;
margin:0 auto;
}
</style>
<script type="module">
import LazyLoad from "../pathto/lazy-load.js";
LazyLoad.init();
</script>
Below is the lazy-load.js file.
const defaults = {
imageLoadedClass: 'js-lazy-image--handled',
imageSelector: '.js-lazy-image',
// If the image gets within 100px in the Y axis, start the download.
rootMargin: '100px 0px',
threshold: 0.01
};
let config,
images,
imageCount,
observer;
/**
* Fetches the image for the given URL
* @param {string} url
*/
function fetchImage(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.src = url;
image.onload = resolve;
image.onerror = reject;
});
}
/**
* Preloads the image
* @param {object} image
*/
function preloadImage(image) {
const src = image.dataset.src;
if (!src) {
return;
}
return fetchImage(src).then(() => { applyImage(image, src); });
}
/**
* Load all of the images immediately
* @param {NodeListOf<Element>} images
*/
function loadImagesImmediately(images) {
// foreach() is not supported in IE
for (let i = 0; i < images.length; i++) {
let image = images[i];
preloadImage(image);
}
}
/**
* Disconnect the observer
*/
function disconnect() {
if (!observer) {
return;
}
observer.disconnect();
}
/**
* On intersection
* @param {array} entries
*/
function onIntersection(entries) {
// Disconnect if we've already loaded all of the images
if (imageCount === 0) {
disconnect();
return;
}
// Loop through the entries
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
// Are we in viewport?
if (entry.intersectionRatio > 0) {
imageCount--;
// Stop watching and load the image
observer.unobserve(entry.target);
preloadImage(entry.target);
}
}
}
/**
* Apply the image
* @param {object} img
* @param {string} src
*/
function applyImage(img, src) {
// Prevent this from being lazy loaded a second time.
img.classList.add(config.imageLoadedClass);
img.src = src;
}
let LazyLoad = {
init: (options) => {
config = {...defaults, ...options};
images = document.querySelectorAll(config.imageSelector);
imageCount = images.length;
// If we don't have support for intersection observer, loads the images immediately
if (!('IntersectionObserver' in window)) {
loadImagesImmediately(images);
} else {
// It is supported, load the images
observer = new IntersectionObserver(onIntersection, config);
// foreach() is not supported in IE
for (let i = 0; i < images.length; i++) {
let image = images[i];
if (image.classList.contains(config.imageLoadedClass)) {
continue;
}
observer.observe(image);
}
}
}
};
export default LazyLoad;
And just in case it's needed, a sample of the image code on the page:
<img class="js-lazy-image" data-src="url/members/thumbs/ab_banff_np.jpg" alt="rockies, rocky mountains, trees, summer" aria-describedby="image_g2">