dwook.record
Published on

이미지 Lazy Loading

Authors
  • avatar
    Name
    dwook
    useEffect(() => {
        const images = [
            '/static/images/encarpay/encarpay_loan_menual_1.jpg',
            '/static/images/encarpay/encarpay_loan_menual_2.jpg',
            '/static/images/encarpay/encarpay_loan_menual_3.jpg'];
        let count = 0;
        const onload = () => {
            count += 1;
            if (count === images.length) {
                setLoaded(true);
            }
        };
        for (let i = 0; i < images.length; i += 1) {
            const img = new Image();
            img.src = images[i];
            img.onload = onload;
        }
    }, []);
  • Image 객체
    • <img> 태그에 의해 생성된 DOM 객체. 자바스크립트로 이미지를 다룰 수 있음
    • 프로퍼티
      • src: 이미지 경로
      • width: 이미지가 출력될 폭. 이미지가 로드되면 실제폭으로 변경.
      • height: 이미지가 출력될 높이. 이미지가 로드되면 실제폭으로 변경.
      • complete: 이미지의 로드이 완료되었는지 여부. true/false
      • onload: 이미지 로드 완료 후 호출되는 함수

Lazy Loading 기술

<img> 태그를 이용하는 일반적인 방법

  • [1단계] 이미지 로딩을 사전에 막기
    • <img>태그에 src 속성이 있으면, 브라우저는 이미지를 무조건 로드.
    • 이미지 로딩을 지연시키려면 src 속성 대신 다른 속성에다가 이미지 url을 넣음.
<img data-src="https://ik.imagekit.io/demo/default-image.jpg" />
  • [2단계] 자바스크립트 이벤트를 이용하여 이미지로드

  • 유저 사용성 때문에 처음 3개의 이미지는 미리 로딩되어 있음.

<img src="https://ik.imagekit.io/demo/img/image1.jpeg?tr=w-400,h-300" />
<img src="https://ik.imagekit.io/demo/img/image2.jpeg?tr=w-400,h-300" />
<img src="https://ik.imagekit.io/demo/img/image3.jpg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image5.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image6.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image7.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image8.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image9.jpeg?tr=w-400,h-300" />
<img class="lazy" data-src="https://ik.imagekit.io/demo/img/image10.jpeg?tr=w-400,h-300" />
document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages = document.querySelectorAll("img.lazy");
  var lazyloadThrottleTimeout;

  function lazyload () {
    if(lazyloadThrottleTimeout) {
      clearTimeout(lazyloadThrottleTimeout);
    }

    lazyloadThrottleTimeout = setTimeout(function() {
        var scrollTop = window.pageYOffset;
        lazyloadImages.forEach(function(img) {
            if(img.offsetTop < (window.innerHeight + scrollTop)) {
              img.src = img.dataset.src;
              img.classList.remove('lazy');
            }
        });
        if(lazyloadImages.length == 0) {
          document.removeEventListener("scroll", lazyload);
          window.removeEventListener("resize", lazyload);
          window.removeEventListener("orientationChange", lazyload);
        }
    }, 20);
  }

  document.addEventListener("scroll", lazyload);
  window.addEventListener("resize", lazyload);
  window.addEventListener("orientationChange", lazyload);
});
  • [2단계] Intersection Observer API를 이용하여 이미지 로드

  • 이미지 로드를 지연시키기 위해 모든 이미지에 옵저버를 부착

  • 엘리먼트가 뷰포트에 들어간 것을 API가 감지했을 때, isIntersecting 속성을 이용해서 URL을 data-src 속성에서 src 속성으로 이동시켜서 브라우저가 이미지를 로드

  • 전부 로드되면 lazy 클래스명을 이미지에서 삭제하고 부착했던 옵저버를 제거

document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages;

  if ("IntersectionObserver" in window) {
    lazyloadImages = document.querySelectorAll(".lazy");
    var imageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          var image = entry.target;
          image.src = image.dataset.src;
          image.classList.remove("lazy");
          imageObserver.unobserve(image);
        }
      });
    });

    lazyloadImages.forEach(function(image) {
      imageObserver.observe(image);
    });
  } else {
    var lazyloadThrottleTimeout;
    lazyloadImages = document.querySelectorAll(".lazy");

    function lazyload () {
      if(lazyloadThrottleTimeout) {
        clearTimeout(lazyloadThrottleTimeout);
      }

      lazyloadThrottleTimeout = setTimeout(function() {
        var scrollTop = window.pageYOffset;
        lazyloadImages.forEach(function(img) {
            if(img.offsetTop < (window.innerHeight + scrollTop)) {
              img.src = img.dataset.src;
              img.classList.remove('lazy');
            }
        });
        if(lazyloadImages.length == 0) {
          document.removeEventListener("scroll", lazyload);
          window.removeEventListener("resize", lazyload);
          window.removeEventListener("orientationChange", lazyload);
        }
      }, 20);
    }

    document.addEventListener("scroll", lazyload);
    window.addEventListener("resize", lazyload);
    window.addEventListener("orientationChange", lazyload);
  }
})
  • 임계점을 500px 주는 경우
$(document).ready(function() {
  var lazyloadImages;

  if ("IntersectionObserver" in window) {
    lazyloadImages = document.querySelectorAll(".lazy");
    var imageObserver = new IntersectionObserver(function(entries, observer) {
      console.log(observer);
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          var image = entry.target;
          image.src = image.dataset.src;
          image.classList.remove("lazy");
          imageObserver.unobserve(image);
        }
      });
    }, {
      root: document.querySelector("#container"),
      rootMargin: "0px 0px 500px 0px"
    });

    lazyloadImages.forEach(function(image) {
      imageObserver.observe(image);
    });
  } else {
    var lazyloadThrottleTimeout;
    lazyloadImages = $(".lazy");

    function lazyload () {
      if(lazyloadThrottleTimeout) {
        clearTimeout(lazyloadThrottleTimeout);
      }

      lazyloadThrottleTimeout = setTimeout(function() {
          var scrollTop = $(window).scrollTop();
          lazyloadImages.each(function() {
              var el = $(this);
              if(el.offset().top < window.innerHeight + scrollTop + 500) {
                var url = el.attr("data-src");
                el.attr("src", url);
                el.removeClass("lazy");
                lazyloadImages = $(".lazy");
              }
          });
          if(lazyloadImages.length == 0) {
            $(document).off("scroll");
            $(window).off("resize");
          }
      }, 20);
    }

    $(document).on("scroll", lazyload);
    $(window).on("resize", lazyload);
  }
})


Native Lazy Loading 방식

  • Chrome 76에서는 Native Lazy Loading을 지원
  • loading 속성에 사용할 수 있는 값
    • lazy: 뷰포트에서 일정한 거리에 닿을 때까지 로딩을 지연
    • eager: 페이지가 로딩되자마자 해당 요소를 로딩
    • auto: loading 속성을 쓰지 않을 것과 동일
  • Native Lazy Loading을 지원하지 않는 브라우저의 경우, 위 방식을 이용
  • 로딩 지연된 이미지들이 다운로드될 때 감싸고 있는 내용들이 밀려나는 것을 방지하려면, 반드시 height와 width 속성을 <img>태그에 추가하거나 inline style로 직접값을 지정
<img src="example.jpg" loading="lazy" alt="" width="200" height="200">
<img src="example.jpg" loading="lazy" alt="" style="height:200px; width:200px;">
<iframe src="example.html" loading="lazy"></iframe>

CSS 속성 중 Background Image를 Lazy Loading 하는 방법

  • 이 예시에서 주목할 점은 lazy loading 관련해서 구현한 자바스크립트 코드가 똑같다는 것.
  • ID bg-image를 가진 엘리먼트는 지정된 background-image를 가집니다. lazy라는 클래스를 엘리먼트에 추가하게 되면, 해당 엘리먼트는 background-image 속성을 none으로 변경.
  • 브라우저는 초기에 엘리먼트에게 background-image: none 속성을 적용. 이후에 엘리먼트의 lazy 클래스를 삭제함으로써, background image를 로드하여 적용하게 됨.
<div id="bg-image" class="lazy"></div>
#bg-image.lazy {
  background-image: none;
  background-color: #F1F1FA;
}
#bg-image {
  background-image: url("https://ik.imagekit.io/demo/img/image10.jpeg?tr=w-600,h-400");
  max-width: 600px;
  height: 400px;
}
document.addEventListener("DOMContentLoaded", function() {
  var lazyloadImages;

  if ("IntersectionObserver" in window) {
    lazyloadImages = document.querySelectorAll(".lazy");
    var imageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          var image = entry.target;
          image.classList.remove("lazy");
          imageObserver.unobserve(image);
        }
      });
    });

    lazyloadImages.forEach(function(image) {
      imageObserver.observe(image);
    });
  } else {
    var lazyloadThrottleTimeout;
    lazyloadImages = document.querySelectorAll(".lazy");

    function lazyload () {
      if(lazyloadThrottleTimeout) {
        clearTimeout(lazyloadThrottleTimeout);
      }

      lazyloadThrottleTimeout = setTimeout(function() {
        var scrollTop = window.pageYOffset;
        lazyloadImages.forEach(function(img) {
            if(img.offsetTop < (window.innerHeight + scrollTop)) {
              img.src = img.dataset.src;
              img.classList.remove('lazy');
            }
        });
        if(lazyloadImages.length == 0) {
          document.removeEventListener("scroll", lazyload);
          window.removeEventListener("resize", lazyload);
          window.removeEventListener("orientationChange", lazyload);
        }
      }, 20);
    }

    document.addEventListener("scroll", lazyload);
    window.addEventListener("resize", lazyload);
    window.addEventListener("orientationChange", lazyload);
  }
})

참조링크