async/defer, preload, 반응형 이미지 등 HTML 단에서 제어하는 로딩 성능과 Core Web Vitals 최적화를 다룬다.
리소스 로딩과 렌더링 성능 최적화
브라우저가 HTML을 파싱하면서 외부 리소스를 만나는 순간, 어떤 속성이 붙어 있느냐에 따라 페이지 로딩 속도가 수백 밀리초씩 달라집니다. 입문편에서 <script src="...">, <img src="..."> 같은 기본 삽입 방법을 배웠다면, 이번 레슨에서는 그 태그들에 붙이는 성능 제어 속성이 렌더링 파이프라인에 어떤 영향을 주는지 원리부터 실전 패턴까지 다룹니다. Core Web Vitals(LCP·CLS·INP)를 JavaScript 없이 HTML 마크업 수준에서 개선할 수 있는 영역이 생각보다 넓습니다.
학습 목표
async/defer/type="module"의 스크립트 로딩 타이밍 차이와 실행 순서를 이해하고 올바르게 선택한다.rel=preload·preconnect·dns-prefetch·prefetch리소스 힌트를 상황에 맞게 적용한다.srcset·sizes·<picture>와fetchpriority로 반응형 이미지 로딩을 제어한다.loading=lazy·decoding=async속성이 CLS(Cumulative Layout Shift) 방지와 어떻게 연관되는지 파악한다.- Critical Rendering Path 에서 렌더 차단 리소스를 제거하여 LCP와 INP를 개선한다.
1. script의 async / defer / module 로딩 타이밍
세 가지 속성의 동작 원리
<script> 태그를 </body> 직전에 두는 것은 고전적인 최적화입니다. 그러나 속성 하나로 <head> 안에서도 HTML 파싱을 막지 않을 수 있습니다.
HTML 파싱 ──────┬───────────────────────────────────────────▶
│
기본(없음) ▼ fetch + execute (파싱 완전 중단)
async fetch(병렬) + execute(도착 즉시, 파싱 일시중단)
defer fetch(병렬) + execute(DOMContentLoaded 직전, 순서 보장)
module defer와 동일 타이밍 + strict mode + 모듈 스코프
<!-- ❌ 파싱 차단: 스크립트가 크거나 느릴수록 흰 화면이 길어진다 -->
<head>
<script src="heavy-analytics.js"></script>
</head>
<!-- ✅ async: 광고·분석처럼 순서·DOM 불필요한 독립 스크립트 -->
<head>
<script async src="analytics.js"></script>
</head>
<!-- ✅ defer: DOM이 필요하고 실행 순서가 중요한 앱 코드 -->
<head>
<script defer src="vendor.js"></script>
<script defer src="app.js"></script> <!-- vendor.js 다음에 실행 보장 -->
</head>
<!-- ✅ module: ES Module 문법 사용, defer 기본 내장 -->
<head>
<script type="module" src="main.js"></script>
</head>
실행 순서 비교 표
| 속성 | 파싱 차단 | 실행 시점 | 순서 보장 | DOM 접근 가능 |
|---|---|---|---|---|
| 없음 | 예 | 즉시 | 예 | 불확실 |
async | 부분(실행 시) | 다운로드 완료 즉시 | 아니오 | 불확실 |
defer | 아니오 | DOMContentLoaded 직전 | 예 | 예 |
type="module" | 아니오 | DOMContentLoaded 직전 | 예 | 예 |
⚠️ 주의
async스크립트가 여러 개일 때 실행 순서는 네트워크 응답 속도에 따라 달라집니다. 서로 의존 관계가 있는 스크립트에는 절대async를 쓰지 마세요.
💡 TIP
type="module"스크립트는 같은 URL을 두 번import해도 한 번만 평가됩니다. 또한 크로스 오리진 모듈에는 CORS 헤더가 필요합니다.
2. 리소스 힌트: preload · preconnect · dns-prefetch · prefetch
브라우저는 HTML을 위에서 아래로 읽으며 필요한 리소스를 발견합니다. 리소스 힌트는 브라우저가 실제 리소스를 발견하기 전에 미리 행동하도록 지시하는 메커니즘입니다.
preload — 현재 페이지에 곧 필요한 리소스
preload는 브라우저의 프리로드 스캐너가 발견하기 어려운 늦게 로딩되는 중요 리소스를 선점합니다. LCP 이미지나 웹 폰트에 가장 효과적입니다.
<head>
<!-- LCP 히어로 이미지 선점 로딩 -->
<link rel="preload" as="image" href="/hero.webp" />
<!-- 폰트: crossorigin 필수 (same-origin도 포함) -->
<link rel="preload" as="font" type="font/woff2"
href="/fonts/pretendard.woff2" crossorigin />
<!-- CSS 안에서만 참조되는 critical 스크립트 -->
<link rel="preload" as="script" href="/critical-sdk.js" />
</head>
⚠️ 주의
as속성을 빠뜨리면 리소스가 두 번 다운로드됩니다.as값은image,font,script,style,fetch등 리소스 유형에 맞게 정확히 지정하세요.
preconnect / dns-prefetch — 외부 오리진 연결 선점
외부 CDN·API 서버에 대한 DNS 조회, TCP 핸드셰이크, TLS 협상을 미리 완료합니다.
<head>
<!-- 곧 리소스를 요청할 외부 오리진: DNS + TCP + TLS 모두 선점 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- 나중에 사용할 오리진 또는 여러 도메인: DNS만 선점 (비용 저렴) -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://api.example.com" />
</head>
prefetch — 다음 페이지에서 필요한 리소스
현재 페이지에는 필요 없지만 사용자가 다음에 방문할 가능성이 높은 리소스를 유휴 시간에 미리 캐싱합니다.
<!-- 장바구니 버튼이 있는 페이지라면, 결제 페이지 스크립트를 미리 캐싱 -->
<link rel="prefetch" href="/checkout/checkout.js" as="script" />
힌트 선택 가이드
| 힌트 | 언제 사용 | 우선순위 |
|---|---|---|
preload | 현재 페이지, 즉시 필요, LCP 리소스 | 높음 |
preconnect | 외부 오리진, 곧 요청 예정 | 중간 |
dns-prefetch | 외부 오리진이 많을 때 저비용 대안 | 낮음 |
prefetch | 다음 페이지, 유휴 시 캐싱 | 매우 낮음 |
💡 TIP
preload를 과도하게 사용하면 오히려 대역폭 경쟁으로 성능이 나빠집니다. 한 페이지에서preload는 3~5개 이내, LCP에 직접 영향을 주는 리소스만 사용하는 것이 원칙입니다.
3. 반응형 이미지: srcset · sizes · picture · fetchpriority
srcset와 sizes — 브라우저에게 후보를 제시하라
<!-- ❌ 모든 디바이스에 동일한 2000px 이미지 전송 -->
<img src="hero-2000.jpg" alt="히어로 이미지" />
<!-- ✅ 브라우저가 뷰포트·DPR에 맞는 최적 이미지를 선택 -->
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-2000.jpg 2000w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 80vw,
1200px
"
alt="히어로 이미지"
width="1200"
height="630"
/>
sizes는 CSS 미디어 쿼리 문법으로 이미지가 실제 렌더링될 크기를 브라우저에게 알립니다. 브라우저는 이 정보와 화면 DPR(Device Pixel Ratio)을 조합해 srcset 후보 중 가장 적합한 파일을 선택합니다.
⚠️ 주의
width와height속성을 반드시 함께 지정하세요. 브라우저는 이 값으로 이미지 공간을 미리 확보해 CLS를 방지합니다. CSS로width: 100%를 지정해도 aspect-ratio가 유지됩니다.
picture — 아트 디렉션과 포맷 분기
<picture>
<!-- WebP를 지원하면 WebP 사용, 세로 뷰포트에서는 세로 구도 이미지 -->
<source
media="(orientation: portrait)"
srcset="hero-portrait.webp 800w, hero-portrait-hd.webp 1600w"
type="image/webp"
sizes="(max-width: 800px) 100vw, 800px"
/>
<!-- 가로 뷰포트 WebP -->
<source
srcset="hero-landscape.webp 1200w, hero-landscape-hd.webp 2400w"
type="image/webp"
sizes="(max-width: 1200px) 100vw, 1200px"
/>
<!-- WebP 미지원 폴백 -->
<img
src="hero-landscape.jpg"
alt="캠페인 히어로"
width="1200"
height="630"
/>
</picture>
fetchpriority — 로딩 우선순위 명시적 제어
브라우저의 기본 우선순위 휴리스틱을 덮어쓸 수 있습니다.
<!-- LCP 대상 이미지: 높은 우선순위로 선점 -->
<img
src="hero.webp"
fetchpriority="high"
alt="메인 히어로"
width="1200"
height="630"
/>
<!-- 뷰포트 밖 슬라이드, 배너: 우선순위 낮춤 -->
<img
src="slide-3.webp"
fetchpriority="low"
loading="lazy"
alt="세 번째 슬라이드"
width="800"
height="400"
/>
<!-- preload와 조합: 폰트처럼 브라우저가 늦게 발견하는 리소스 -->
<link
rel="preload"
as="image"
href="hero.webp"
fetchpriority="high"
/>
4. loading=lazy · decoding=async와 CLS 방지
loading=lazy — 뷰포트 진입 전까지 로딩 지연
<!-- ✅ 스크롤 없이 보이지 않는 이미지는 지연 로딩 -->
<img
src="article-thumbnail.webp"
loading="lazy"
alt="아티클 썸네일"
width="600"
height="400"
/>
<!-- ❌ LCP 대상(뷰포트 내 첫 화면) 이미지에는 절대 사용 금지 -->
<img
src="hero.webp"
loading="lazy"
alt="히어로"
/> <!-- LCP를 지연시켜 점수를 악화시킴 -->
<!-- iframe도 지원 -->
<iframe
src="https://www.youtube.com/embed/abc123"
loading="lazy"
title="소개 영상"
width="560"
height="315"
></iframe>
decoding=async — 이미지 디코딩 비동기 처리
<!-- 메인 스레드를 차단하지 않고 백그라운드에서 이미지 디코딩 -->
<img
src="large-illustration.webp"
decoding="async"
alt="일러스트레이션"
width="800"
height="600"
/>
decoding=async는 이미지 디코딩을 메인 스레드 외부에서 처리하므로, 복잡한 레이아웃 계산이나 스크립트 실행과 경쟁하지 않아 INP(Interaction to Next Paint) 개선에 도움이 됩니다.
CLS 방지 체크리스트
CLS는 사용자가 읽고 있는 중 레이아웃이 갑자기 밀리는 현상으로, Google이 Core Web Vitals 핵심 지표로 삼고 있습니다.
<!-- ✅ 모든 img/video에 width+height 명시 → 브라우저가 공간 선점 -->
<img src="photo.webp" alt="사진" width="800" height="600" />
<!-- ✅ CSS aspect-ratio로도 공간 확보 가능 -->
<style>
.responsive-img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
</style>
<img class="responsive-img" src="photo.webp" alt="사진" />
<!-- ✅ 폰트 로딩 중 텍스트 이동 방지: font-display: swap + 사전 크기 확보 -->
<link rel="preload" as="font" href="pretendard.woff2"
type="font/woff2" crossorigin />
💡 TIP 동적으로 삽입되는 광고 배너가 CLS의 가장 큰 원인입니다. 광고 슬롯에
min-height를 CSS로 미리 지정하거나, 배너 컨테이너에 고정 크기를 확보해두면 CLS를 크게 줄일 수 있습니다.
5. 렌더 차단 리소스 제거와 Critical Rendering Path
Critical Rendering Path란
브라우저가 첫 픽셀을 화면에 그리려면 HTML 파싱 → CSSOM 생성 → 렌더 트리 결합 순서를 거칩니다. 이 경로에서 대기 시간을 일으키는 리소스가 렌더 차단 리소스입니다.
HTML 파싱
│
├─ <link rel="stylesheet"> 발견 → CSS 다운로드 완료까지 렌더 차단
│
├─ <script>(속성 없음) 발견 → 다운로드 + 실행 완료까지 파싱 중단
│
└─ CSSOM + DOM 완성 → Render Tree → Layout → Paint → Composite
CSS 렌더 차단 완화 전략
<!-- ❌ 모든 CSS를 하나의 큰 파일로 제공 -->
<link rel="stylesheet" href="all-styles.css" />
<!-- ✅ 전략 1: media 속성으로 조건부 로딩 (비해당 미디어 = 비차단) -->
<link rel="stylesheet" href="base.css" />
<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="tablet.css" media="(min-width: 768px)" />
<!-- ✅ 전략 2: 중요 CSS는 <style> 인라인, 나머지는 비차단 로딩 -->
<style>
/* Critical CSS: 첫 화면에 필요한 최소한의 스타일만 */
body { margin: 0; font-family: sans-serif; }
.hero { min-height: 100svh; background: #000; }
</style>
<!-- 비차단 방식으로 전체 CSS 로딩 (onload 후 rel을 stylesheet로 변경) -->
<link rel="preload" as="style" href="full.css"
onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="full.css" /></noscript>
서드파티 스크립트 렌더 차단 제거
<!-- ❌ 태그 매니저, 채팅 위젯 등 서드파티를 동기 로딩 -->
<script src="https://thirdparty.com/widget.js"></script>
<!-- ✅ defer로 렌더 차단 제거, DOM 로드 후 실행 -->
<script defer src="https://thirdparty.com/widget.js"></script>
<!-- ✅ 또는 동적 삽입 (사용자 인터랙션 후 로딩) -->
<script>
document.querySelector('#chat-button').addEventListener('click', () => {
const s = document.createElement('script');
s.src = 'https://chat.example.com/widget.js';
document.head.appendChild(s);
});
</script>
💡 TIP Chrome DevTools의 Performance 탭에서 "Render blocking resources" 항목을 확인하거나, Lighthouse 보고서의 "Eliminate render-blocking resources" 감사 항목으로 차단 리소스를 빠르게 파악할 수 있습니다.
6. Core Web Vitals를 HTML 구조로 개선하기
LCP(Largest Contentful Paint) — 가장 큰 콘텐츠의 첫 렌더 시간
LCP는 주로 히어로 이미지 또는 대형 텍스트 블록이 화면에 표시되는 시점입니다. 목표: 2.5초 이내.
<head>
<!-- 1. LCP 이미지 preload -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- 2. 외부 폰트 오리진 preconnect -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head>
<body>
<!-- 3. LCP 이미지: loading=eager(기본값), fetchpriority=high, width/height 명시 -->
<img
src="/hero.webp"
alt="메인 히어로"
width="1440"
height="720"
fetchpriority="high"
decoding="async"
/>
</body>
CLS(Cumulative Layout Shift) — 예상치 못한 레이아웃 이동 합산
목표: 0.1 이하. 주요 원인과 HTML 수준 해결책입니다.
<!-- 원인 1: 크기 없는 이미지 → 해결: width/height 항상 명시 -->
<img src="thumbnail.webp" alt="썸네일" width="300" height="200" />
<!-- 원인 2: 나중에 삽입되는 광고/배너 → 해결: 컨테이너 크기 예약 -->
<div style="min-height: 90px; width: 728px;">
<!-- 배너 로딩 후 여기에 삽입 -->
</div>
<!-- 원인 3: 웹 폰트 로딩 중 폰트 교체 → 해결: preload + font-display: swap -->
<link rel="preload" as="font" href="/fonts/pretendard-regular.woff2"
type="font/woff2" crossorigin />
<style>
@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard-regular.woff2') format('woff2');
font-display: swap;
}
</style>
INP(Interaction to Next Paint) — 인터랙션 응답성
INP는 사용자 입력에서 다음 화면 업데이트까지의 시간입니다. 목표: 200ms 이하. HTML 구조 차원에서 개선할 수 있는 부분을 살펴봅니다.
<!-- ✅ 스크립트 defer/async로 파싱 중 메인 스레드 독점 방지 -->
<script defer src="app.js"></script>
<!-- ✅ 이미지 decoding=async로 디코딩이 메인 스레드 차단 방지 -->
<img src="gallery-item.webp" decoding="async" alt="갤러리" width="400" height="300" />
<!-- ✅ 무거운 iframe은 lazy 로딩으로 초기 파싱 부담 분산 -->
<iframe
src="https://maps.example.com/embed"
loading="lazy"
title="위치 지도"
width="600"
height="450"
></iframe>
💡 TIP
<script type="module">은 기본적으로 defer처럼 동작하므로, ES Module 기반 앱에서는 별도의defer없이도 렌더 차단이 발생하지 않습니다. 단,<script type="module" async>처럼 명시적으로async를 추가하면 defer 동작이 overriding됩니다.
요약
async는 독립 스크립트,defer는 DOM 의존·순서 중요 스크립트에 사용하고,type="module"은defer를 내장한다.preload는 현재 페이지의 중요 리소스 선점에,preconnect는 외부 오리진 연결 선점에,prefetch는 다음 페이지 캐싱에 사용한다.srcset·sizes·<picture>로 디바이스에 맞는 이미지를 제공하고,fetchpriority="high"로 LCP 이미지를 명시적으로 우선순위 지정한다.loading=lazy는 뷰포트 밖 이미지에만 사용하고,width·height속성을 항상 명시해 CLS를 방지한다.- CSS
media분리와 Critical CSS 인라인으로 렌더 차단 CSS를 최소화하고, 서드파티 스크립트는defer또는 지연 삽입으로 처리한다. - Core Web Vitals는 LCP·CLS·INP 세 지표로 구성되며, 각각 HTML 속성·구조 변경만으로도 의미 있는 개선이 가능하다.
연습문제
- 다음 코드에서 성능 문제를 찾고,
async와defer중 어떤 속성을 붙여야 하는지 이유와 함께 설명하세요.
<head>
<script src="jquery.js"></script>
<script src="app.js"></script> <!-- app.js는 jQuery에 의존함 -->
<script src="analytics.js"></script> <!-- analytics.js는 DOM·다른 스크립트와 무관 -->
</head>
힌트 의존 관계가 있으면 실행 순서가 보장되어야 합니다.
- 아래 이미지 태그가 CLS를 유발하는 이유를 설명하고, CLS를 방지하도록 수정하세요.
<img src="product.webp" loading="lazy" alt="제품 사진" />
힌트 브라우저가 공간을 미리 확보하려면 어떤 정보가 필요한가요?
- 뉴스 사이트의
<head>안에 다음 리소스들이 있습니다. 각 리소스에 맞는 리소스 힌트를 골라 코드를 작성하세요./fonts/noto-sans-kr.woff2— 본문 폰트, 즉시 필요https://ad-server.example.com— 광고 서버 도메인, 곧 요청 예정/next-article.js— 사용자가 다음 아티클을 클릭할 가능성이 높음
힌트 즉시 필요하면 preload, 외부 오리진 연결 선점은 preconnect, 다음 페이지 캐싱은 prefetch입니다.
- 다음 LCP 이미지에 최적의 성능 속성들을 추가하여 완성된 태그를 작성하세요. (이미지 크기: 1440×720, WebP 포맷,
srcset에 400w·800w·1440w 후보 포함)
<img src="hero-1440.webp" alt="히어로 이미지" />
힌트 LCP 이미지는 높은 우선순위, lazy 로딩 금지, width/height 명시, 비동기 디코딩을 고려하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“HTML 심화” 강좌에 대한 댓글입니다.