라이브러리 없이 이미지 지연 로드하기

sejin kim·2022년 7월 31일
0
post-thumbnail

Lazy loading

첫 화면/뷰포트에 포함되지 않는 오프스크린 이미지를 지연 로드하는 전략은, 웹 페이지 성능 최적화에 있어 기본적이면서도 중요한 부분으로 작용합니다.

Critical Rendering Path(CRP)에서 중요하게 다뤄지는 HTML, CSS, JavaScript와는 달리 이미지는 페이지 렌더링을 차단하지는 않지만, 페이지를 구성하는 주요 리소스이고 DOMContentLoaded 이벤트 이후 load 이벤트 시점까지의 시간을 결정한다는 점에서 사용자 경험과 밀접하게 연관되어 있습니다.

또한 이미지는 대개 웹 페이지에서 가장 높은 비중으로 트래픽을 점유하는 리소스이기도 한데, 실제로 HTTPArchive의 리포트에 의하면 백분위수에 따라 차이는 있지만 전체 페이지 사이즈에서 약 50% 가량을 차지하고 있음을 알 수 있습니다.



어렵지 않게 페이지 성능을 개선하고 사용자의 데이터도 절약할 수 있는 아이디어인 만큼, 그동안은 (지난 글에서도 언급했던 바 있는) Intersection Observer API 또는 scroll, resize, orientationchange 이벤트 핸들러 등을 활용하는 방법으로 직접 구현한다거나, 이미 잘 만들어진 라이브러리들(lazysizes, Vanilla-lazyload, yall.js, react-lazyload)을 사용하는 방법으로 Lazy loading을 적용해 왔습니다.

만약 Next.js를 사용하고 있다면, <Image /> 컴포넌트로 제공되는 이미지 최적화 기능을 통해 조금 더 간편하게 구현할 수 있는 선택지도 존재했을 것입니다.

이러한 상황에서 2020년 즈음부터는 브라우저 레벨에서 Lazy loading을 지원하기 시작했고, 이제는 대부분의 브라우저에서 속성 하나만 추가하면 구현할 수 있게끔 단순화되기에 이르렀습니다.






HTML img 'loading' attribute

아래와 같이 <img> 엘리먼트에서 loading 속성을 사용하면, 해당 이미지에 대해 Lazy loading을 적용할 수 있습니다.


<img src="image.png" loading="lazy" alt="" width="250" height="150">

  • lazy: 브라우저에서 정의한 수치만큼 사용자가 특정 위치로 스크롤할 때까지 이미지의 로딩을 지연시킵니다.
  • eager: 이미지가 뷰포트 내에 위치하는지 여부와 무관하게 이미지를 즉시 로드합니다. (default)

브라우저 호환성은 Chrome의 경우 77 버전, Firefox는 75 버전, Safari의 경우엔 한동안 구현에 대한 논의가 이어지다가 15.4 버전부터 제한적으로 지원하기 시작했습니다. 경우에 따라 구체적인 구현 범위나 조건은 다를 수 있지만, 이미지에 대해서는 공통적으로 지원됩니다.

만약 브라우저가 지원하지 않더라도, 별다른 부작용 없이 그냥 무시되므로 마음 편히 적용해볼 수 있습니다. 아래는 caniuse를 인용한 내용입니다.



이때 브라우저의 지원 여부는 아래와 같이 확인해볼 수 있으며, 당연하게도(?) polyfill 또한 존재하기 때문에 사실상 거의 모든 케이스를 대응할 수 있습니다.

if ('loading' in HTMLImageElement.prototype) {
    // ...
} else {
    // polyfill ...
}





threshold

그런데 이미 라이브러리 등으로 Lazy loading을 적용해본 경험이 있다면, 무언가 하나가 빠진 듯한 느낌이 들 수도 있습니다. 바로 임계값(threshold) 설정입니다.

이미지를 지연 로드하기는 하지만, 구체적으로 어느 시점에서 미리 로드할 것인지를 조정해야 할 수 있습니다. 예를 들어 threshold 값을 '1000' 만큼 준다면, 뷰포트에 표시되기 1000px 이내 지점에서 이미지가 로드되는 식입니다. Intersection Observer에서의 rootMargin을 의미한다고도 볼 수 있을 것입니다.

미리 로드하지 않으면 뷰포트에 진입한 순간에야 이미지가 로드될 것이고, 이미지가 지연 로드되는 모습이 그대로 보이면서 사용자 경험을 해칠 수 있으므로 이러한 margin 설정은 반드시 필요한 부분입니다.

실제로 필자의 경우에도, 리스트 형태의 아이템 이미지들에 대해 지연 로딩을 적용할 때 정책 협의 과정에서 "Desktop은 700px, Mobile은 1400px로 적용해 주세요." 같은 구체적인 요구사항을 접수해본 적이 있습니다. 플랫폼, 디바이스, 네트워크 등 사용자 환경에 따라 너무 빠르게 로드해도, 너무 늦게 로드해도 바람직하지 않기 때문입니다.

하지만 위에서 언급했던 대로, loading 속성으로 지연 로드하는 경우 뷰포트로부터의 임계값은 브라우저에 의해 결정됩니다. 정확히는 브라우저 엔진에 하드코딩되어 있으며, 이를 변경할 수 있는 API는 제공되지 않고 있습니다. Chromium 같은 경우에는 일반적인 라이브러리들보다는 보수적인 값으로 설정하고 있다고 알려져 있습니다.

다만 값이 아주 고정적인 것은 아니고, 여러 변수를 고려하여 휴리스틱으로 최적의 사용자 경험을 제공할 수 있도록 적절한 수치로 설정하고 있다고 Google(Chrome)은 설명합니다.



아래 Chromium 소스를 직접 확인해보면, 네트워크 연결 상태에 따라 다른 수치가 적용됨을 알 수 있습니다.


//
// Lazy image loading distance-from-viewport thresholds for different effective connection types.
//
{
    name: "lazyImageLoadingDistanceThresholdPxUnknown",
    initial: 5000,
    type: "int",
},
{
    name: "lazyImageLoadingDistanceThresholdPxOffline",
    initial: 8000,
    type: "int",
},
{
    name: "lazyImageLoadingDistanceThresholdPxSlow2G",
    initial: 8000,
    type: "int",
},
{
    name: "lazyImageLoadingDistanceThresholdPx2G",
    initial: 6000,
    type: "int",
},
{
    name: "lazyImageLoadingDistanceThresholdPx3G",
    initial: 4000,
    type: "int",
},
{
    name: "lazyImageLoadingDistanceThresholdPx4G",
    initial: 3000,
    type: "int",
},

2020년 7월에는 이 임계값 수치를 한 차례 조정했다고 하며, 고속 연결(4G)에서는 3000px -> 1250px, 저속 연결(3G)에서는 4000px -> 2500px으로 줄이는 등 최적화를 진행하고 있음을 알 수 있습니다. 이는 DevTools에서 네트워크를 스로틀링하여 직접 확인해볼 수도 있습니다.







유의할 점

이미지 크기 지정하기 (width, height)

<img> 태그에는 기본적으로 width, height 속성이 포함되어야 바람직합니다. 브라우저가 이미지의 크기를 미리 파악하고 배치할 공간을 확보해야 하기 때문입니다. 그렇지 않으면 이미지가 로드될 때 Cumulative Layout Shift(CLS)가 발생하게 되며, 사용자 경험에 막대한 악영향을 미칠 수 있습니다. 이때 이미지 주변 모든 엘리먼트의 위치가 다시 계산되고 배치되면서 발생하는 reflow는 말할 것도 없습니다.

width, height 속성이 없으면 이미지가 로드되기 전에는 0x0 픽셀로 취급됩니다. 때문에 브라우저는 엘리먼트가 뷰포트에 이미 포함되어 있는 것으로 판단하게 될 수 있으며, 이 경우 이미지가 지연 로드되지 못하고 그냥 즉시 로드될 수도 있습니다.




첫 화면에 포함되는 이미지는 지연 로드하지 않기

첫 화면에서부터 뷰포트 내에 포함되어 있는 이미지는 loading="lazy"를 적용하지 않는 편이 좋습니다. 이는 폴리필이나 라이브러리를 사용하는 경우에도 해당되는 부분입니다. 불필요하게 이미지가 지연 로드되면서 사용자 경험이 저해되는 문제가 발생할 수 있기 때문입니다.

예를 들어 오리지널 이미지가 로드되기 전까지 빈 공간이나 어떤 placeholder 이미지 같은 것을 대신 보여주는 처리가 되어 있다면, 즉시 노출될 수 있음에도 굳이 뷰포트 진입 여부를 계산한 다음 이미지가 교체되는 모습을 보여주면서 느리게 로드되는 듯한 느낌을 줄 수 있습니다.

이외에도 중요도가 높거나, SEO에 영향을 줄 수 있는 이미지인 경우에도 지연 로드하지 않는 편이 바람직할 수 있습니다.


<!-- visible in the viewport -->
<img src="product-1.jpg" alt="..." width="200" height="200">
<img src="product-2.jpg" alt="..." width="200" height="200">
<img src="product-3.jpg" alt="..." width="200" height="200">

<!-- offscreen images -->
<img src="product-4.jpg" loading="lazy" alt="..." width="200" height="200">
<img src="product-5.jpg" loading="lazy" alt="..." width="200" height="200">
<img src="product-6.jpg" loading="lazy" alt="..." width="200" height="200">





그 외의 특징

  • slider, carousel 같이 즉시 노출되지는 않는 영역에 위치하고 있는 이미지인 경우 뷰포트 내에 노출되는지(오프스크린 여부)와 관계없이 즉시 로드됩니다. 오직 임계값(threshold) 만큼 뷰포트 아래에 위치한 이미지만 지연 로드됩니다.
  • CSS background image에는 loading 속성을 적용할 수 없습니다. <img> 엘리먼트에서만 사용 가능합니다.
  • 기존 폴리필이나 라이브러리와도 서로 영향 없이 동시에 동작할 수 있도록 설계되어 있으므로, 함께 사용해도 무방합니다.





참고 링크

profile
퇴고를 좋아하는 주니어 웹 개발자입니다.

0개의 댓글