What is lazy loading

Lazy loading (lazy loading) is a strategy of loading resources only when they are needed.

Principle: When the user scrolls the page, determine whether the element is in the visible area, and the element resources outside the visible area (such as pictures) will not load;

Why: This is a way to shorten the length of critical render paths, which can shorten page load times. Lazy loading is commonly used to implement functions such as:

  • Lazy loading of images
  • Infinite scrolling of lists
  • Calculate exposure of AD elements
  • Clickable link preloading

Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It’s a way to shorten the length of the critical rendering path, which translates into reduced page load times. Lazy loading can occur on different moments in the application, but it typically happens on some user interactions such as scrolling and navigation.

Lazy loading scheme

Knowing that the core principle of lazy loading is to determine whether the element is in the visible area, there are two ways to achieve:

The first is to determine the position of the current relative view of an element when listening for the Scroll event

The other is through IntersectionObserver (overlapping observer, which is used to judge whether two elements overlap without monitoring events and compatibility). Existing schemes:

All three schemes are compatible and IntersectionObserver API is used. In other words, if the browser supports IntersectionObserver, the browser API is directly used. If not, the scrolllistener listens for elements appearing or approaching the viewport area. Here’s a quick look at each implementation:

lazysizesimplementation

Lazysizes core source is lazysizes-core.js and lazysizes-intersection.js, where the implementation of lazysizes-core scroll monitor, Lazyzes-intersection is an implementation of IntersectionObserver. First we define the self-executing function (code), define the default parameter variable, and merge it if the user has a custom parameter lazySizesConfig.

(function(){
    var prop;
    var lazySizesDefaults = {
            lazyClass: 'lazyload'.loadedClass: 'lazyloaded'.loadingClass: 'lazyloading'./ /... omit
    };
    lazySizesCfg = window.lazySizesConfig || window.lazysizesConfig || {};
    // Merge the default configuration
    for(prop in lazySizesDefaults){
            if(! (propinlazySizesCfg)){ lazySizesCfg[prop] = lazySizesDefaults[prop]; }}}) ();Copy the code

Loader._ () ¶ Loader._ () ¶ Loader._ () ¶ loader._() ¶

_ :function(){
    // Get the DOM element that needs lazyLoad
    // The lazySizescfg. lazyClass configuration item can be customized
    lazysizes.elements = document.getElementsByClassName(lazySizesCfg.lazyClass);
    // Listen for scroll
    addEventListener('scroll', throttledCheckElements, true);
}
Copy the code

The main implementation of the listener is throttledCheckElements, but it is actually called checkElements. Throttling is performed for performance reasons. It is interesting to see its throttling implementation (code), as well as the implementation of requestIdleCallback. Take a look at how checkElements checks if an element is in the viewable area and how it replaces the real resource in the attribute.

// Trigger distance
if(! defaultExpand) { defaultExpand = (! lazySizesCfg.expand || lazySizesCfg.expand <1)? docElem.clientHeight >500 && docElem.clientWidth > 500 ? 500 : 370 :
  lazySizesCfg.expand;
}
/ /... omit
// If the trigger condition is met when traversing the current element
rect = lazyloadElems[i].getBoundingClientRect();
if ((eLbottom = rect.bottom) >= elemNegativeExpand &&
    (eLtop = rect.top) <= elvH &&
    (eLright = rect.right) >= elemNegativeExpand * hFac &&
    (eLleft = rect.left) <= eLvW &&
    (eLbottom || eLright || eLleft || eLtop) &&
    (lazySizesCfg.loadHidden || isVisible(lazyloadElems[i])) &&
    ((isCompleted && isLoading < 3 && !elemExpandVal && (loadMode < 3 || lowRuns < 4)) || isNestedVisible(lazyloadElems[i], elemExpand))){
  unveilElement(lazyloadElems[i]);
  loadedSomething = true;
  if(isLoading > 9) {break;}
}
Copy the code

When iterating through the current element, judge that the trigger condition is met and lazyUnveil (code) in unveilElement is called to complete the attribute replacement. Moreover, IntersectionObserver can be implemented in the same way, but IntersectionObserver and Scroll can judge in a different way.

Loza.jsimplementation

The implementation uses the Intersection Observer API and MutationObserver directly for better performance. The code is only ~ 1K, which is very lightweight. Add a comment to the code:

export default function (selector = '.lozad', options = {}) {
  // Merge custom configuration with default configuration
  const {root, rootMargin, threshold, enableAutoReload, load, loaded} = Object.assign({}, defaultConfig, options)
	// Initializes the observer, mutationObserver does the element listener and passes the listener callback
  let observer
  let mutationObserver
  // Determine whether IntersectionObserver is supported
  if (support('IntersectionObserver')) {
    observer = new IntersectionObserver(onIntersection(load, loaded), {
      root,
      rootMargin,
      threshold
    })
  }
	// Check whether MutationObserver is supported, enableAutoReload is automatically reloaded when the property changes
  if (support('MutationObserver') && enableAutoReload) {
    mutationObserver = new MutationObserver(onMutation(load, loaded))
  }
	// According to the selector selector, get all the DOM nodes and iterate over them
  const elements = getElements(selector, root)
  for (let i = 0; i < elements.length; i++) {
    // Large image optimization, preLoad method implemented, when the large image is not loaded, can be customized placeholder background
    preLoad(elements[i])
  }

  return {
    // External call: Register the observed, that is, register the DOM node that needs to be observed
    observe() {
      const elements = getElements(selector, root)
			/ / traverse
      for (let i = 0; i < elements.length; i++) {
        if (isLoaded(elements[i])) {
          continue
        }
				
        if (observer) {
          if (mutationObserver && enableAutoReload) {
            mutationObserver.observe(elements[i], {subtree: true.attributes: true.attributeFilter: validAttribute})
          }
					Observe.observe (target) is a simple way to register the observed
          observer.observe(elements[i])
          continue
        }
        load(elements[i])
        markAsLoaded(elements[i])
        loaded(elements[i])
      }
    },
    triggerLoad(element) {
      if (isLoaded(element)) {
        return
      }
      load(element)
      markAsLoaded(element)
      loaded(element)
    },
    observer,
    mutationObserver
  }
}
Copy the code

Lazy loading principle: the realization of visual area judgment

Look at the implementation of their respective schemes above, in fact, are around the principle of lazy loading, and then expand their functions. So the main thing to understand is the core implementation, so let’s learn how to determine if an element is in the viewable area.

Method 1: getBoundingClientRect

GetBoundingClientRect is a free method for the element. The return value is a DOMRect object with attributes left, top, right, bottom, x, y, width, and height to indicate the relative position of the element in the view. Unfamiliar people are strongly advised to read the documentation

The Element.getBoundingClientRect() method returns a DOMRect object providing information about the size of an element and its position relative to the viewport.



Based on the above figure, we can conclude that the conditions for determining whether an element is in a view are:top >= 0 && bottom <= viewHeight(Window height) andLeft >= 0 && right <= viewWidth

Hence the following code:

function checkElement(element) :boolean {
  const viewWidth = window.innerWidth || document.documentElement.clientWidth
  const viewHeight = window.innerHeight || document.documentElement.clientHeight
  const {
    top,
    right,
    bottom,
    left,
  } = element.getBoundingClientRect() || {}
  return (
    top >= 0 &&
    bottom <= viewHeight &&
    left >= 0 &&
    right <= viewWidth
  );
}
Copy the code

Mode 2: IntersectionObserver

The nice performance!!!!! Usage is also strongly recommended

The IntersectionObserver interface (which is part of the IntersectionObserver API) provides a way to asynchronously observe the intersecting status of a target element with its ancestor element or top-level document viewport. Ancestor elements and viewports are called roots. When a IntersectionObserver object is created, it is configured to listen on a given proportion of visible areas in the root. Once IntersectionObserver is created, its configuration cannot be changed, so a given observer object can only be used to monitor specific changes in the visible area. However, you can configure to listen on multiple target elements in the same observer object.

Combined with the implementation of the existing library scheme above, it can be obtained:


// IntersectionObserver attributes
const options = {}
const intersectionObserver = new IntersectionObserver(function(entries, observer) { 
    entries.forEach(entry= > {
      	// The trigger time
        entry.time          
      	// The position of the root element is a rectangle, in this case the window position
        entry.rootBounds
      	// The position of the observed rectangle
        entry.boundingClientRect 
      	// The position rectangle of the overlapping area
        entry.intersectionRect
      	// The proportion of the overlapped area to the observed area (even if the observed is not a rectangle)
        entry.intersectionRatio 
      	// Target element
        entry.target
    });
}, options);
// start observing
const observer = intersectionObserver.observe(document.querySelector('.scrollerFooter'));
Copy the code

Handwriting pictures lazy loading

getBoundingClientRect

<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>Lazyload-getBoundingClientRect</title>
  <style>
    img {
      display: block;
      margin-bottom: 50px;
      height: 400px;
    }
  </style>
</head>

<body>
  <img src="" lazyload="true" data-src="assets/images/iu.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu1.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu2.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu3.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu1.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu2.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu3.jpeg" />
  <script>
    function checkElement(element) {
      const viewWidth = window.innerWidth || document.documentElement.clientWidth
      const viewHeight = window.innerHeight || document.documentElement.clientHeight
      const {
        top,
        right,
        bottom,
        left,
      } = element.getBoundingClientRect() || {}
      return (
        top >= 0 &&
        bottom <= viewHeight &&
        left >= 0 &&
        right <= viewWidth
      )
    }
    function lazyload() {
      var eles = document.querySelectorAll('img[data-src][lazyload]')
      Array.prototype.forEach.call(eles, function (el, index) {
        var rect
        if (el.dataset.src === "")
          return
        if (checkElement(el)) {
          !function () {
            var img = new Image()
            img.src = el.dataset.src
            img.onload = function () {
              el.src = img.src
            }
            el.removeAttribute("data-src")// Remove the attribute and do not iterate over it again
            el.removeAttribute("lazyload")
          }()
        }
      })
    }
    lazyload()// Trigger a function to initialize the first page image before scrolling
    document.addEventListener("scroll", lazyload)
  </script>
</body>

</html>
Copy the code

IntersectionObserver

<script>
    function lazyload() {
      // IntersectionObserver attributes
      const options = { threshold: 1.0 }
      const intersectionObserver = new IntersectionObserver(function (entries, observer) {
        entries.forEach(entry= > {
          console.log('entry', entry);
          const el = entry.target
          if (entry.isIntersecting) {
            !function () {
              var img = new Image()
              img.src = el.dataset.src
              img.onload = function () {
                el.src = img.src
              }
            }()
          }
        });
      }, options);
      // start observing
      var eles = document.querySelectorAll('img[data-src][lazyload]')
      Array.prototype.forEach.call(eles, function (el, index) {
        intersectionObserver.observe(el);
      })
    }
    lazyload()
  </script>
Copy the code

Attach a code

Github.com/AutumnWhj/l…

reference

GetBoundingClientRect IntersectionObserver lazy loading and preloading