Lazy Loading

현유진·2022년 1월 31일
0

웹 페이지 내에서 바로 로딩을 하지 않고 로딩 시점을 뒤로 미루는 것, 페이지 내에서 실제로 필요로 할 때까지 리소스의 로딩을 미루는 것이다.

왜 Lazy Loading을 사용해야 하나?

1. 웹 성능과 디바이스 내 리소스 활용도 증가
페이지 최초 로딩 시점에 필요한 리소스만 다운로드하기 때문에, 다운로드 bytes를 줄일 수 있다. 이는 유저 측면에서는 네트워크 대역폭을 줄여주고 디바이스 측면에서는 다른 리소스들을 더 빠르게 처리해서 다운로드할 수 있도록 한다. 따라서 lazy loading을 쓰지 않는 것에 비해서 훨씬 빠르게 유저가 이용할 수 있게된다.

2. 비용 감소
이미지와 같은 여러 리소스들은 주로 전송 bytes에 기반해 비용이 청구된다. lazy loading을 사용하면 이미지가 보여지지 않으면 절대로 loading하지 않기 때문에 페이지 내에서 전달할 총 bytes를 줄일 수 있다. 이처럼 네트워크로부터 전송될 bytes의 감소는 비용을 줄이도록 도와준다.

Lazy Loading 적용하기

원리는 간단하다. 사용자가 페이지 스크롤을 내렸을 때 이미지의 placeholder가 뷰포트에 보여지게되면 이미지를 로딩하도록 트리거를 일으키면 된다.

방법은 크게 2가지가 있다.
1. 이벤트 리스너를 사용하는 방법
2. Intersection Observer API를 사용하는

Lazy Loading을 적용 원리는 다음과 같다.

  1. src 속성 대신 data-src 속성 이용하기
    우리가 일반적으로 img를 로드하기 위해서는 src라는 속성을 사용하는데, 브라우저가 src 속성을 가지면 몇번째 순서에 있던 무조건 로드를 하게 된다.
    이를 막기 위해서 data-src라는 속성을 대신해서 여기에 로드할 이미지 url를 지정한다.
    <img data-src="https://ik.imagekit.io/demo/default-image.jpg" />
  2. 브라우저에게 해당 이미지를 언제 로딩할지 알려주기
    placeholder가 뷰포트에 들어오면 이미지를 로딩해준다.

이벤트 리스너를 사용하는 방법

브라우저 내 'resize', 'scroll', 'orientationChange'(디바이스 화면이 가로/세로 모드로 바뀔 때 발생) 이벤트를 사용하는 방법이다.

어떤 이미지가 뷰포트 안으로 들어왔는지 확인하고 뷰포트 안으로 들어가면 <img> 태그의 data-src 속성에 지정된 URL을 src 속성에 넣어서 이미지를 로드하는 방식이다. 모든 이미지가 로딩되면 그때 트리거를 일으키던 이벤트 리스너를 제거한다.

HTML

<div class="wrap">
    <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" />
</div>
  

CSS

img {
  display: block;
  width: 400px;
  height: 300px;
  background-color: #F1F1FA;
  border: 0;
  margin: 10px auto;
}

Javascript

const nodes = {
  lazyLoadImages: document.querySelectorAll('img.lazy')
}

function init() {
  initEvents()
}

function initEvents() {
  // DOMContentLoaded: 브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 발생한다. 이미지나 스타일시트 등의 기타 자원은 기다리지 않는다.
  document.addEventListener('DOMContentLoaded', lazyload)
  document.addEventListener('scroll', lazyload)
  window.addEventListener('resize', lazyload)
  window.addEventListener('orientationChange', lazyload)
}

function lazyload() {
  let lazyloadThrottleTimeout = 0

  if (lazyloadThrottleTimeout) {
    clearTimeout(lazyloadThrottleTimeout)
  }

  lazyloadThrottleTimeout = setTimeout(() => {
    let scrollTop = window.pageYOffset

    nodes.lazyLoadImages.forEach(img => {
      if (img.offsetTop < window.innerHeight + scrollTop) {
        img.src = img.dataset.src
        img.classList.remove('lazy')
      }
    })

    if (nodes.lazyLoadImages.length === 0) {
      document.removeEventListener('scroll', lazyload)
      window.removeEventListener('resize', lazyload)
      window.removeEventListener('orientationChange', lazyload)
    }
  }, 500)
}

init()

Intersection Observer API 사용하기

이미지가 뷰포트에 들어간 것은 API가 감지했을 때, isIntersecting 속성을 이용하도록 한다. URL을 data-src 속성에서 src 속성으로 이동시켜서 브라우저가 이미지를 로드하도록 트리거를 일으킨다. 전부 로드되면 lazy 클래스명을 이미지에서 삭제하고 부착했던 옵저버를 제거하는 방식으로 동작한다.

function lazyloadIO() {
  if ('IntersectionObserver' in window) {
    const lazyImageObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          let image = entry.target
          image.src = image.dataset.src
          image.classList.remove('lazy')
          lazyImageObserver.unobserve(image)
        }
      })
    })

    nodes.lazyLoadImages.forEach(lazyImage => {
      lazyImageObserver.observe(lazyImage)
    })
  }
}

두 방식 비교

이미지 로딩 시간

Intersection Observer 방식이 이벤트 리스너를 이용한 방식보다 이미지를 로드하는 트리거가 훨씬 빠르며 스크롤할 때 이미지가 느리게 나타나지 않는 것을 확인할 수 있었다.

이벤트 리스너를 이용한 방식은 성능을 위해 timeout을 추가했었는데, 이 부분은 사용성 측면에서 이미지 로드에 약간의 딜레이가 발생하는 미미한 영향을 끼쳤다.




참고: 웹성능 최적화를 위한 Image Lazy Loading 기법
profile
WEB FE Developer

0개의 댓글