HTTP 아카이브 연구에 따르면 평균 웹사이트의 반 이상이 이미지로 이루어져 있으며, 이미지의 용량도 다른 콘텐츠에 비해 월등히 높다. 따라서 이미지의 사이즈를 적절히 줄이고, 렌더링 속도를 빠르게 하면, 웹 사이트의 렌더링 성능은 더욱더 빨라질 것이다.
하지만 무작정 이미지를 최적화하기 전에, 앞서 내가 만들고 있는 웹 사이트가 이미지 최적화가 필요한지 먼저 고민을 해보아야 한다. 이미지가 중요한 사이트가 아니라면 굳이 리소스와 비용을 투자할 필요가 없기 때문이다.
이미지 최적화 방법에는 여러 가지가 있다.
<img>
태그의 srcset 속성을 사용하는 방법<picture>
태그를 사용하는 방법올리브영 웹 사이트 최적화 방법 의 글을 참조하자.
이 글에서는 이미지 Lazy Load
기법을 통하여 이미지 최적화 하는 방법에 대하려 알아보려 한다.
사용자가 처음부터 보지 않는 부분을 초기 렌더링 시 로드하게 되면 정작 사용자가 보이는 화면의 로딩 시간이 지연되게 된다.
웹사이트의 이미지는 최대한 사용자가 보이는 부분부터 로드되도록 처리하며, 사용자가 보이지 않는 부분은 Lazy Loading
을 적용하여 사용자의 사용자 경험 저하를 막을 수 있도록 해야한다.
10개의 이미지를 사용하여 화면에 나타내주었다.
사용자들에게는 처음 2개의 이미지만 보일 것임에도 불구하고, 10장의 이미지가 초기에 다 로드가 된다.
네트워크 탭에서 속도를 Slow 3G 으로 변경하게 되면 사용자 경험 관점에서 이것이 얼마나 성능에 악영향을 끼치는지 확인할 수 있다. 이때 사용자들은 몇 초간은 아무 것도 보이지 않을 수 있다.
html 을 살펴보자.
먼저, img src
에 width=10, height
를 지정해두었고 data-src
에는 width=300
을 지정을 해두었다.
javascript 를 이용하여 src에 data-src를 대체하여 넣을 것이다.
<div class="container">
<img src="https://picsum.photos/10/301" data-src="https://picsum.photos/400/301" />
<img src="https://picsum.photos/10/302" data-src="https://picsum.photos/400/302" />
<img src="https://picsum.photos/10/303" data-src="https://picsum.photos/400/303" />
<img src="https://picsum.photos/10/304" data-src="https://picsum.photos/400/304" />
<img src="https://picsum.photos/10/305" data-src="https://picsum.photos/400/305" />
<img src="https://picsum.photos/10/306" data-src="https://picsum.photos/400/306" />
<img src="https://picsum.photos/10/307" data-src="https://picsum.photos/400/307" />
<img src="https://picsum.photos/10/308" data-src="https://picsum.photos/400/308" />
</div>
밑의 사진은 src에 적용된 사진이 브라우저에 로드된 모습이다. 현재 width를 10으로 지정해두었기 때문에 뿌옇게 흐려지는 사진이 보여진다.
이제, Javascript 를 통해서 src를 data-src로 대체 할 것이다.
forEach문을 사용하여 모든 image 들의 src를 data-src로 대체하였다.
const images = document.querySelectorAll('img');
images.forEach((image) => {
const newURL = image.getAttribute('data-src');
image.src = newURL
})
image태그에 data-src 의 url이 잘 적용되어 width가 400으로 지정되어진 확인할 수 있다. 그런데 아직까지는 초기 로딩 시에, 모든 이미지 리소스들을 다 다운로드 받아진 것이 network tab 에서 보여진다.
이제, Intersection Observer API
를 이용하여 이미지가 화면에 표시되는지 여부를 감지하여 스크롤을 할 때 화면에 보여지는 이미지만 로드되도록 개선해 볼 것이다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법이다.
페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩을 구현하기 위하여 InterSection Observer API을 사용해보자.
또한, IntersectionObserverEntry
의 속성을 활용하면 getBoundingClientRect()
를 호출한 것과 같은 결과를 알 수 있기 때문에 따로 getBoundingClientRect() 함수를 호출할 필요가 없어 리플로우 현상을 방지
할 수 있다.
리플로우(reflow)
: 리플로우는 브라우저가 웹 페이지의 일부 또는 전체를 다시 그려야하는 경우 발생한다.const images = document.querySelectorAll('img');
// IntersectionObserver 를 등록한다.
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
// 관찰 대상이 viewport 안에 들어오지 않은 경우 return
if (!entry.isIntersecting) return;
// 관찰 대상이 viewport 안에 들어온 경우 image 로드
const image = entry.target;
const newURL = image.getAttribute('data-src');
// data-src 정보를 타켓의 src 속성에 설정
image.src = newURL
// 이미지를 불러왔다면 타켓 엘리먼트에 대한 관찰을 멈춘다.
observer.unobserve(image)
});
}, imageOptions);
// 관찰할 대상을 선언하고, 해당 속성을 관찰시킨다.
images.forEach((image) => {
observer.observe(image);
})
스크롤을 내려보면, 스크롤이 해당 이미지의 위치에 도달했을 때 이미지를 로딩하고 있다. Network 탭을 보면 순차적으로 이미지를 불러오는 것을 확인할 수 있다.
또한 unobserve
를 이용하여 이미지를 불러왔다면 target element 에 대한 관찰을 멈추었기 때문에, 스크롤을 올렸을 때 다시 이미지가 로드되지 않는 것을 볼 수 있다
.
html에서 src와 data-src 두 가지를 선언하여, javascript를 이용하여 lazy load images 를 구현하였는 데, 코드가 길어져 지저분해 보일 수 있다.
코드를 좀 더 짧게 정리를 해보자.
우선 html에 data-src를 다 지우고, src 만 남겨준다.
javascript 에서 replace
를 이용해서 width 를 바꾸어줄 수 있다.
따라서 html 에는 src 만 남게 되어 코드가 간결해졌다.
<div class="container">
<img src="https://picsum.photos/10/301" />
<img src="https://picsum.photos/10/302" />
<img src="https://picsum.photos/10/303" />
<img src="https://picsum.photos/10/304" />
<img src="https://picsum.photos/10/305" />
<img src="https://picsum.photos/10/306" />
<img src="https://picsum.photos/10/307" />
<img src="https://picsum.photos/10/308" />
</div>
const images = document.querySelectorAll('img');
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
console.log(entry)
if (!entry.isIntersecting) return;
const image = entry.target;
image.src = image.src.replace('photos/10/', 'photos/400/')
observer.unobserve(image)
});
}, imageOptions);
images.forEach((image) => {
observer.observe(image);
})