요즘 성능 최적화에 관심이 많아져 쓰로틀링, 디바운스 다음으로 이미지 Lazy Load를 공부해봤습니다. 제가 만든 현재 스킨에도 적용을 해서 나름 만족하고 있습니다.
Lazy Load 구현 순서는 대략 이렇습니다.
- HTML <img/> 태그의 src 속성을 data-src 속성으로 변경
- fade 효과를 줄 .fade 클래스 CSS 작성
- Intersection Observer API를 사용한 함수 작성
Lazy Load의 필요성
Lazy Load 적용이 안되어있는 일반적인 웹사이트의 경우 사용자가 방문하면 브라우저는 해당 페이지의 모든 리소스를 로드시킵니다.
사용자는 페이지의 모든 내용을 확인하지 않기 때문에 보지 않은 이미지는 이미 네트워크 리소스가 낭비되는 겁니다. 이미지가 많은 웹사이트의 경우 자원의 손실은 커져 Lazy Load 기능을 활용해 호스팅 비용을 아끼면 일석이조입니다.
Lazy Load 말고 다른 대안도 있는데 이건 마지막에 남겨보도록 하겠습니다.
1. HTML 이미지 태그 수정
<div class="wrap">
<img data-src="https://cdn.pixabay.com/photo/2021/01/19/21/16/cat-5932474_960_720.jpg" alt="1" id='target'>
<img data-src="https://cdn.pixabay.com/photo/2021/11/13/14/55/mountains-6791530_960_720.jpg" alt="" id='target2'>
<img data-src="https://cdn.pixabay.com/photo/2021/12/23/19/14/waterfall-6889855_960_720.jpg" alt="" id='target3'>
<img data-src="https://cdn.pixabay.com/photo/2021/12/19/19/35/dried-up-6881798_960_720.jpg" alt="" id='target4'>
<img data-src="https://cdn.pixabay.com/photo/2021/11/28/15/31/cathedral-6830531_960_720.jpg" alt="" id='target5'>
</div>
Lazy Load 기능을 추가할 이미지 태그에 src 속성 이름을 data-src로 변경해주고 뷰포트에서 이미지가 보이면 data-src 값을 src 값으로 변경해 줄 예정.
2. fade 효과 CSS 작성
img{
opacity: 0;
transition: opacity 1s ease;
}
.fade{
opacity: 1 !important;
}
자연스러운 로딩 효과를 주기 위해 fade 효과를 주기 위한 CSS 작성.
3. Intersection Observer API
구현 방법으로는 여러가지가 있는데, 옛날 방식은 스크롤 이벤트에 이미지가 뷰포트로 넘어오면 로딩을 시작하는 방법이 있고 모든 이미지가 로드되어도 스크롤 이벤트가 작동해 성능면에서 좋지 않다는 평이 있습니다.
다음 방법으로는 직접 구현해볼 Intersection Observer API를 활용하는 방법입니다.
- 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩.
- 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링 되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll을 구현.
- 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.
- 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할지 여부를 결정.
MDN 문서에서 말하는 Intersection Observer의 활용 용도입니다. 지연 로딩 이외에도 무한 스크롤(infinite-scroll) 구현에도 활용되므로 알아두면 좋을 것 같네요.
Intersection Observer는 아래의 2가지 상황에서 콜백을 호출하는 기능을 제공합니다.
- 대상(target)으로 칭하는 요소가 기기 뷰포트나 특정 요소(이 API에서 이를 root 요소 혹은 root로 칭함)와 교차함.
- observer가 최초로 타겟을 관측하도록 요청받을 때마다.
Lazy Load는 뷰포트 즉, 화면에 이미지가 들어오는 경우에 로딩을 시작할 것이기 때문에 1번의 경우가 적합합니다.
4. Intersection Observer 생성 및 옵션 설정
const options = {
root: null,
rootMargin: '0',
threshold: 0
}
const observer = new IntersectionObserver(callback, options);
root는 이미지의 가시성을 확인하기 위한 뷰포트 요소로, 로딩할 이미지의 부모 요소로 지정해야 합니다. null 값이나 지정하지 않을 경우 브라우저 뷰포트로 지정되며, 대부분 브라우저 뷰포틀를 기준으로 사용해서 null 값으로 해두면 되겠습니다.
rootMargin은 root가 가진 여백을 설정한다. 기본값은 0
threshold는 대상 요소의 가시성을 얼마만큼 확보했을 때, 콜백함수를 실행할지를 결정합니다. 뷰포트에서 100% 보일 경우 콜백 함수를 실행하려면 1.0, 50%는 0.5를 입력, 기본값은 0
5. Lazy Load Callback 함수
const lazyLoad = (entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
// 뷰포트 침범
const img = entry.target;
img.setAttribute('src', img.dataset.src);
img.classList.add('fade');
observer.unobserve(entry.target);
// 감지 종료
}
});
};
LazyLoad 라는 콜백 함수를 작성합니다. 생성한 observer에서 화면에 이미지가 감지되면 로딩을 수행할 중요한 함수입니다. isIntersecting 함수는 해당 타겟이 뷰포트에서 감지됐을 경우 true를 반환해 화면상에서 표시되었는지 알 수 있습니다.
뷰포트 화면에 넘어온 대상 엘리먼트는 entry.target으로 data-src 속성 값을 src 값으로 변경해주면 이미지 로딩을 시작하고 fade 클래스를 추가해주면 페이드 CSS 효과까지 적용됩니다.
const target = document.querySelectorAll('img');
target.forEach(el=> {
observer.observe(el);
})
마지막으로 모든 이미지에 옵저버를 추가해줍니다.
6. 예제
See the Pen Untitled by junheeleeme (@junheeleeme) on CodePen.
7. 문제점 보완
현재 코드의 문제점으로 사이트 첫 페이지부터 큰 이미지나 로딩 시간이 필요한 경우 이미지가 로드되기 전 Fade 애니메이션이 끝나버리는 이슈가 있습니다. 그로 인해 초반 로드 시 부자연스럽게 보이는데요. 아래 코드로 수정하시면 좀 더 자연스럽게 보일 수 있습니다.
const lazyLoad = (entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
const img = entry.target;
img.setAttribute('src', img.dataset.src);
// 로딩 완료 후 Fade 효과 추가
img.onload = () => {
img.classList.add('fade');
}
observer.unobserve(entry.target);
}
});
};
Lazyload 함수에 위 코드 처럼 onload 이벤트 핸들러를 사용해 로드가 완료된 후 페이드 효과를 추가해주면 자연스럽게 이미지를 보여줄 수 있습니다.
티스토리에 적용할 경우
const lazyLoad = (entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
const img = entry.target;
const contentImg = img.getAttribute('srcset');
if(contentImg){
img.setAttribute('src', contentImg);
}else{
img.setAttribute('src', img.dataset.src);
}
img.onload = () => {
img.classList.add('fade');
}
}else if(!entry.isIntersecting){
const img = entry.target;
img.classList.remove('fade');
}
});
};
.lb-image{
opacity: 1 !important;
}
티스토리 본문에 포함된 이미지를 클릭해 확대할 경우 srcset 속성에 이미지 태그가 표시된다. srcset 내용을 구분해주는 코드로 구별해주면 된다.
MDN 문서
함께 사용하면 좋은 것들
Lazy Load 뿐만 아니라 이미지를 사용할 때 압축해서 사용한다면 리소스도 줄이며 더욱 효율적으로 로딩 속도를 줄 일 수 있습니다. 그리고 단순 압축보다 더 좋은 방법은 webp 이미지 포맷 형식을 사용하는 방법입니다. 예로 들어 유튜브는 동영상 썸네일 이미지를 webp 포맷으로 사용합니다. 구글에서 만들기도 했고 기존 이미지 형식보다 압축률이 뛰어나기 때문입니다.
크롬 브라우저에서 Webp 포맷 형식으로 변환 가능하며, 이미지 압축도 가능한 Tiny IMG 웹사이트를 이용해보세요.