1

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?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Gregory Magarshak
  • 1,883
  • 2
  • 25
  • 35

1 Answers1

1

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

Gregory Magarshak
  • 1,883
  • 2
  • 25
  • 35
  • 3
    not sure what you are trying to achieve by asking a question then answering it yourself within 3 minutes but something is fishy here. – GrahamTheDev Jan 17 '20 at 08:14
  • 1
    It’s one of the features StackOverflow offers when you ask a question, called “Ask to Answer”. They encourage you to share stuff you learned “Q&A style!” If you try to ask a question, you’ll see that option. – Gregory Magarshak Jan 17 '20 at 08:22
  • 1
    Here is their article on it: https://stackoverflow.com/help/self-answer – Gregory Magarshak Jan 17 '20 at 08:23
  • As for what I am trying to achieve — I discovered something that can help me speed up all my websites massively. And I spent another 10 minutes to extract drop-in code for everyone else to use in their own projects. I figured this was the best way to let other people discover the solution, when they search for it. And I invited others to look at my code and maybe suggest improvements (perhaps I missed other methods of inserting HTML into the DOM for instance), so then that would help me and everyone else, too :-) – Gregory Magarshak Jan 17 '20 at 08:26
  • 1
    ahhhh they haven't made that very obvious and I have been on here for years. I will look at that. It just looked like reputation manipulation, my bad. – GrahamTheDev Jan 17 '20 at 08:51