노트

이미지 loading 속성

Lazy Loading

프론트엔드#html#performance · 연결된 개념 8개

쉽게 말하면

이미지 loading 속성을 lazy로 두면 화면 밖 이미지는 스크롤해서 가까워질 때까지 받지 않아요. 수도꼭지 하나로 물을 받을 때 지금 쓸 그릇부터 채우는 셈이라 중요한 파일이 먼저 도착해요.

비유의 한계 그렇다고 다 lazy로 두면 안 돼요. 첫 화면의 주인공 이미지(LCP 후보)에 걸면 다운로드가 늦게 시작돼 LCP가 나빠지고, width·height가 없으면 늦게 온 이미지가 레이아웃을 밀어요.

img·iframe의 loading 속성으로 리소스를 언제 내려받을지 정한다. eager(기본)는 태그를 만나자마자, lazy는 화면에 가까워질 때까지 미룬다. JS(JavaScript) 없이 동작하는 브라우저 기본 기능이라 IntersectionObserver로 직접 만들 필요가 없다.

  • lazy의 "가까움" 기준은 브라우저가 정한다(Chrome은 네트워크 속도에 따라 수백~수천 px 여유를 둔다)
  • 화면 밖 이미지는 아예 받지 않으므로 처음 내려받는 바이트와 요청 수가 크게 준다

진짜 효과: 중요한 것을 먼저 받게 하기

이미지는 CSS나 동기 스크립트와 달리 렌더링을 막지 않는다. 다 받지 못해도 페이지는 그려지고 자리만 비어 있다. 그런데도 eager 이미지가 많으면 간접적으로 느려진다.

  • 대역폭 경쟁: 수십 장이 동시에 받아지면 CSS·폰트·JS·LCP(Largest Contentful Paint) 이미지와 대역폭을 나눠 쓴다. HTTP/2여도 파이프는 하나다(HTTP/2 멀티플렉싱)
  • 디코드: 받은 이미지는 압축을 풀어야 그려진다. 큰 이미지 여럿을 동시에 풀면 스크롤이 버벅일 수 있다(decoding="async"로 완화)
  • 메모리: 풀린 비트맵이 RAM(Random Access Memory)을 차지해 저사양 기기에 부담이다

브라우저도 화면 밖 eager 이미지의 우선순위를 낮추긴 하지만, 결국 전부 받고 큐를 차지한다. 그리드 상품 이미지에 lazy를 걸면 이미지 바이트뿐 아니라 LCP도 좋아지는 이유가 이 경쟁이 풀려서다.

주의

  • 첫 화면의 주인공 이미지(LCP 후보)에는 lazy를 쓰지 않는다. 다운로드가 늦게 시작돼 LCP가 나빠진다. 오히려 우선순위를 올린다 → 이미지 로딩 우선순위(loading·fetchpriority), preload
  • width·height(또는 aspect-ratio)를 꼭 준다. 늦게 들어오는 이미지가 레이아웃을 밀면 CLS(Cumulative Layout Shift)가 나빠진다
  • display: none으로 숨긴 lazy 이미지는 Chrome·Safari·Firefox 모두 받지 않는다. 반면 opacity: 0처럼 다른 방식으로 숨기면 받는다
  • Next.js Image는 기본이 lazy이고, 우선 로드 옵션을 주면 eager + preload로 바뀐다. 옵션 이름은 버전에 따라 다르다 → Next.js 16 변경점

출처: MDN — Lazy loading: Loading attribute · web.dev — Browser-level image lazy loading for the web

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 프리로드 스캐너

    주 HTML 파서가 막혀 있는 동안 원본 HTML을 앞질러 훑어 이미지·CSS·스크립트를 미리 요청하는 브라우저의 보조 파서. HTML 마크업에 적힌 리소스만 찾는다.

  • 롱 태스크

    메인 스레드를 50ms 넘게 붙잡는 작업. W3C Long Tasks 명세는 이벤트 루프 태스크(뒤따르는 마이크로태스크 처리 포함), 렌더링 갱신 단계, 이벤트 루프 밖에서 브라우저가 UI 스레드로 하는 일 가운데 50ms를 넘는 것을 롱 태스크로 본다. 태스크는 도중에 끊기지 않으므로 그동안 클릭·키 입력 처리와 화면 갱신이 모두 밀린다(이벤트 루프).

  • 브라우저 렌더링 파이프라인

    브라우저가 받은 HTML·CSS·JS를 화면의 픽셀로 바꾸는 순서. 처음 로드할 때도, 이후 무언가 바뀔 때도 같은 단계를 거친다. 바뀐 내용에 따라 어느 단계부터 다시 시작하는지만 다르다.

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

  • 브라우저 캐시

    한 번 받은 리소스(HTML·CSS·JS·이미지)를 브라우저가 저장해 두었다가 다시 쓰는 기능. 서버가 응답 헤더로 얼마나, 어떻게 캐시할지 알려 주고 브라우저가 그대로 따른다.

보기 옵션