Gregory Magarshak
Gregory Magarshak

Reputation: 2217

I have a ton of images. How to make them lazy-load on demand as they scroll into view?

Sometimes I am loading a lot of HTML from the server, either as part of the initial document, or as AJAX. Other times, I insert HTML dynamically into the document.

The problem is that this HTML often contains a lot of images. For example, on a social network you may have tons of user avatars with photos. That results in tons of requests to the server.

I know I can use HTTP/2 to pipeline these requests, but still, I don't need to actually get all those files from the server until they scroll into view. How can I do it?

Upvotes: 1

Views: 405

Answers (1)

Gregory Magarshak
Gregory Magarshak

Reputation: 2217

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

Upvotes: 1

Related Questions