First of all, for any icons like buttons etc., you should be loading and using a font with CSS, or for more flexibility, just SVG. However, you will still be left with various photos and user-generated content that you'd like to render on your page.
Thanks to cool new features of the modern Web, now we can lazy-load those suckers! We will be using the Intersection Observer API and Object.defineProperty to override the various ways an image might find its way into the document at runtime.
Here is the code that will do it for you. You can just copypaste it into a Javascript file and include that file from your document. Or you can also analyze it, learn and tell me what I missed:
(function () {
var Elp = Element.prototype;
var observer = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (entry) {
if (!entry.target || entry.target.tagName.toUpperCase() !== 'IMG') {
return;
}
var img = entry.target;
var rect = entry.intersectionRect;
var src = img.getAttribute('data-defer-src');
if (src && rect.width > 0 && rect.height > 0) {
img.setAttribute('src', src);
img.removeAttribute('data-defer-src');
}
});
}, {
root: null, rootMargin: '0px', threshold: 0
});
// Observe whatever is on the page already
(function () {
var imgs = document.body.getElementsByTagName('img');
imgs = Array.from(imgs);
imgs.forEach(function (img) {
observer.observe(img);
});
})();
// Override innerHTML
var originalSet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').set;
var originalGet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').get;
Object.defineProperty(Elp, 'innerHTML', {
set: function (html) {
var element = document.createElement('div');
originalSet.call(element, html);
var imgs = element.getElementsByTagName('img');
var found = false;
imgs = Array.from(imgs);
imgs.forEach(function (img) {
var src = img.getAttribute('src');
if (src) {
img.setAttribute('data-defer-src', src);
img.removeAttribute('src');
found = true;
}
});
if (!found) {
originalSet.call(this, html);
return html;
}
originalSet.call(this, originalGet.call(element));
var imgs2 = this.getElementsByTagName('img');
imgs2 = Array.from(imgs);
imgs2.forEach(function (img) {
observer.observe(img);
});
},
get: originalGet
});
// Override any ways to insert elements
['insertBefore', 'appendChild'].forEach(function (fn) {
var orig = Elp[fn];
Elp[fn] = function (element) {
var imgs = null;
if (!element) {
return;
}
if (element.tagName && element.tagName.toUpperCase() === 'IMG') {
imgs = [element];
} else {
imgs = element.getElementsByTagName('img');
}
var found = false;
imgs.forEach(function (img) {
var src = img.getAttribute('src');
if (src) {
img.setAttribute('data-defer-src', src);
img.removeAttribute('src');
observer.observe(img);
found = true;
}
});
return orig.apply(this, arguments);
};
});
})();
Bonus points if you can tune your server to render <img data-defer-src="{{url here}}" alt="{{description here}}" title="{{title here}}">
instead of <img src="{{url here}}" alt="{{description here}}" title="{{title here}}">
. Because there is no reliable way across all browsers to intercept images once they've been inserted into the DOM with