웹 페이지 내에서 바로 로딩을 하지 않고 로딩 시점을 뒤로 미루는 것, 페이지 내에서 실제로 필요로 할 때까지 리소스의 로딩을 미루는 것이다.
1. 웹 성능과 디바이스 내 리소스 활용도 증가
페이지 최초 로딩 시점에 필요한 리소스만 다운로드하기 때문에, 다운로드 bytes를 줄일 수 있다. 이는 유저 측면에서는 네트워크 대역폭을 줄여주고 디바이스 측면에서는 다른 리소스들을 더 빠르게 처리해서 다운로드할 수 있도록 한다. 따라서 lazy loading을 쓰지 않는 것에 비해서 훨씬 빠르게 유저가 이용할 수 있게된다.
2. 비용 감소
이미지와 같은 여러 리소스들은 주로 전송 bytes에 기반해 비용이 청구된다. lazy loading을 사용하면 이미지가 보여지지 않으면 절대로 loading하지 않기 때문에 페이지 내에서 전달할 총 bytes를 줄일 수 있다. 이처럼 네트워크로부터 전송될 bytes의 감소는 비용을 줄이도록 도와준다.
원리는 간단하다. 사용자가 페이지 스크롤을 내렸을 때 이미지의 placeholder가 뷰포트에 보여지게되면 이미지를 로딩하도록 트리거를 일으키면 된다.
방법은 크게 2가지가 있다.
1. 이벤트 리스너를 사용하는 방법
2. Intersection Observer API를 사용하는
<img data-src="https://ik.imagekit.io/demo/default-image.jpg" />
브라우저 내 '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()
이미지가 뷰포트에 들어간 것은 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을 추가했었는데, 이 부분은 사용성 측면에서 이미지 로드에 약간의 딜레이가 발생하는 미미한 영향을 끼쳤다.