Picture lazy loading trample pit

The principle of

The biggest influence on the loading speed of web pages is pictures. An ordinary picture may be several meters in size. When there are many pictures, the loading speed of web pages becomes very slow.

In order to optimize web performance and user experience, we lazy load images.

Lazy loading is a way to optimize web page performance by prioritizing images that are in the viewable area rather than loading all images at once. Load the image when the browser scrolls and the image is in view. Make the browser request the image by setting the image’s SRC attribute. When this property is empty or absent, the request is not sent.

implementation

The lazy loading involved in this article is in the vertical direction of the rolling load, horizontal rolling is not considered.

Lazy loading implementation is mainly to determine whether the current image is in the viewable area of the core logic. Let’s start with the implementation idea:

  1. Get all the picturesimg dom
  2. Walk through each image to determine if the current image is in the viewable area.
  3. If it does, set the image’s SRC property.
  4. The binding of windowscrollEvent to listen for events.

HTML structure

<div class="container">
    <div class="img-area">
        <img id="first" data-src="./img/ceng.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/data.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/huaji.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi1.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/liqi2.png" alt="">
    </div>
    <div class="img-area">
        <img data-src="./img/steve-jobs.jpg" alt="">
    </div>
</div>
Copy the code

Now the img tag does not have SRC attribute, we put the real image address in an attribute, here we use HTML5 data attribute, put the real address in the custom data-src.

Determine if the image has entered the viewable area

There are two ways to apply this logic, as I explain.

Methods a

The first method is to determine whether the height of the image from the top of the document is less than the current visual area relative to the top of the Document.

Calculation method of visual area relative to the top height of Document:

const clientHeight = document.documentElement.clientHeight; // Viewport height is the height of the window.
const scrollHeight = document.documentElement.scrollTop + clientHeight; // The scroll bar offsets the height of the top of the document (i.e., the height from the top of the document until the viewport area is erased) + viewport height
Copy the code

Draw a picture for easy understanding:

The next step is to calculate the height of the image from the top of the document. There are two ways to do this. The first way is through the offsetTop attribute of an element. From the above we know that the offsetTop attribute of an element is relative to an ancestor whose position is non-static, that is, child.offsetParent. Need to take into consideration of the ancestor element’s border at the same time, through our child. The offsetParent. ClientTop can get the thickness of the border.

From this we get the calculation of the height of the element from the top of the document:

function getTop(el, initVal) {
    let top = el.offsetTop + initVal;
    if(el.offsetParent ! = =null) {
        top += el.offsetParent.clientTop;
        return getTop(el.offsetParent, top);
    } else {
        returntop; }}Copy the code

This method here uses a tail recursive call. Can improve recursive performance. Of course, this can also be done with a loop:

function getTop(el) {
    let top = el.offsetTop;
    var parent = el.offsetParent;
    while(parent ! = =null) {
        top += parent.offsetTop + parent.clientTop;
        parent = parent.offsetParent;
    }
    return top;
}
Copy the code

The second method is to use element. GetBoundingClientRect () API directly by top value.

The return value of getBoundingClientRect is shown below:

var first  = document.getElementById('first');
getTop(first, 0);  / / 130
console.log(first.getBoundingClientRect().top); / / 130
Copy the code

So we get the judgment method:

function inSight(el) {
    const clientHeight = document.documentElement.clientHeight;
    const scrollHeight = document.documentElement.scrollTop + clientHeight;
    / / method
    return getTop(el, 0) < scrollHeight;
    / / method 2
    // return el.getBoundingClientRect().top < clientHeight;
}
Copy the code

The next step is to judge and assign each image.

function loadImg(el) {
    if (!el.src) {
        el.src = el.dataset.src;
    }
}

function checkImgs() {
    const imgs = document.getElementsByTagName('img');
    Array.from(imgs).forEach(el= > {
        if(inSight(el)) { loadImg(el); }})console.log(count++);
}
Copy the code

Bind the onScroll event and onload event to the window:

window.addEventListener('scroll', checkImgs, false);
window.onload = checkImgs;
Copy the code

We know that event browsers like Scroll or resize can fire many times in a short period of time. To improve web page performance, we need a throttling function to control the function firing many times and perform only one callback over a period of time (say 500ms).

/** * continuously triggers events, and only executes one event at a time. * @param fun Function to be executed * @param delay Delay time * @param time Must be executed once within the time */
function throttle(fun, delay, time) {
    var timeout;
    var previous = +new Date(a);return function () {
        var now = +new Date(a);var context = this;
        var args = arguments;
        clearTimeout(timeout);
        if (now - previous >= time) {
            fun.apply(context, args);
            previous = now;
        } else {
            timeout = setTimeout(function () { fun.apply(context, args); }, delay); }}}window.addEventListener('scroll', throttle(checkImgs, 200.1000), false);
Copy the code

Method 2

HTML5 has a new IntersectionObserver API that automatically observes whether elements are visible or not.

Main usage:

var observer = new IntersectionObserver(callback, option);

// Start observing
observer.observe(document.getElementById('first'));

// Stop observing
observer.unobserve(document.getElementById('first'));

// Close the viewer
observer.disconnect();
Copy the code

The observer’s callback is called when the visibility of the target changes.

function callback(changes: IntersectionObserverEntry[]) { console.log(changes[0]) } // IntersectionObserverEntry { time: 29.499999713152647, intersectionRatio: 1, intersectionRatio: 1, boundingClientRect: DOMRectReadOnly {bottom: 144, height: 4, left: 289, right: 293, top: 140, width: 4, x: 289, y: 140 }, intersectionRect: DOMRectReadOnly, isIntersecting: true, rootBounds: DOMRectReadOnly, target: img#first }Copy the code

1.

  • Time: indicates the time when visibility changes. It is a high-precision timestamp in milliseconds
  • IntersectionRatio: Visibility ratio of target elements, i.e., proportion of intersectionRect to boundingClientRect, is 1 when it is completely visible, and less than or equal to 0 when it is completely invisible
  • BoundingClientRect: Information about the rectangular region of the target element
  • IntersectionRect: information on the intersection area between target element and viewport (or root element)
  • RootBounds: Information about the rectangular region of the root element, returned by the getBoundingClientRect() method, or null if there is no root element (that is, scrolling directly relative to the viewport)
  • IsIntersecting: Whether it enters the viewport, Boolean value
  • Target: The observed target element, which is a DOM node object

IntersectionObserver can be used for lazy image loading:

function query(tag) {
    return Array.from(document.getElementsByTagName(tag));
}
var observer = new IntersectionObserver(
    (changes) = > {
        changes.forEach((change) = > {
            if (change.intersectionRatio > 0) {
                var img = change.target;
                img.src = img.dataset.src;
                observer.unobserve(img);
            }
        })
    }
)
query('img').forEach((item) = > {
    observer.observe(item);
})
Copy the code

See github for the full code

Finish:)