브라우저는 HTML 을 위에서 아래로 parse 하면서 필요한 리소스를 하나씩 발견한다. font 는 CSS 를 받아 parse 한 뒤에야, LCP image 는 그걸 참조하는 마크업을 만난 뒤에야 요청된다. resource hint 는 이 발견을 앞당기는 도구 — <link rel> 한 줄로 "이건 곧 필요하니 미리 받아둬" 를 브라우저에게 알린다. 단, 잘못 쓰면 대역폭만 낭비하고 정작 critical 리소스의 우선순위를 깎는다. 이 가이드는 hint family 각각이 무엇을 하는지, 언제 써야 하는지, 그리고 흔한 실수를 정리한다.
왜 hint 가 필요한가 — 발견의 지연
<!-- 브라우저가 보는 순서 -->
1. HTML 받음 → parse 시작
2. <link rel="stylesheet" href="app.css"> 발견 → CSS 요청
3. CSS 받음 → parse → @font-face url(...) 발견 → font 요청 ← 여기!
4. <img src="hero.jpg"> 발견 → LCP image 요청
font 와 LCP image 는 chain 끝에서야 발견된다.
preload 로 1번 직후 병렬 요청하면 수백 ms 단축.핵심은 critical chain 단축. 브라우저가 스스로 발견하기 전에 우리가 먼저 알려주는 것. 자세한 critical path 는 how-browsers-render-pages 가이드 참조.
dns-prefetch — DNS 만 미리
<link rel="dns-prefetch" href="https://cdn.example.com">- 해당 origin 의 DNS 해석만 미리 수행. TCP / TLS 는 안 함.
- 가장 싼 hint — 거의 비용 없음. 실제 연결을 약속하지 않음.
- 언제 — 곧 쓸지도 모르는 덜 critical한 3rd-party origin (analytics, 광고, 폰트 CDN 후보). 연결까지 보장하긴 아까울 때.
- DNS lookup 은 보통 20-120ms. 모바일·느린 네트워크에서 체감 ↑.
preconnect — DNS + TCP + TLS 까지
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>- DNS 해석 + TCP handshake + (HTTPS 면) TLS negotiation 까지 미리. 연결을 "데워둔다(warm)".
- 언제 — 곧 확실히 쓸 critical 3rd-party origin. font host, image CDN, API origin.
- crossorigin 주의 — font 는 anonymous CORS 모드로 받기 때문에
crossorigin속성이 없으면 preconnect 한 연결이 재사용되지 않고 새 연결이 또 열린다. font origin 엔 거의 항상crossorigin필요. - 너무 많이 쓰지 말 것 — origin 당 연결 자원을 잡는다. 4-6 개를 넘기면 오히려 critical 연결과 경쟁. 정말 쓰는 origin 에만.
<!-- 흔한 Google Fonts 패턴 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- googleapis = CSS (same-origin fetch), gstatic = font (crossorigin) -->font 가 실제로 어떻게 로드되고 FOUT / FOIT 가 왜 생기는지는 how-web-fonts-load 가이드에서 다룬다. CDN 연결 비용의 내부는 how-cdns-actually-work 참조.
preload — 이 페이지에 필요한 걸 높은 우선순위로
<link rel="preload" href="/fonts/inter.woff2" as="font"
type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image"
fetchpriority="high">
<link rel="preload" href="/critical.css" as="style">- 지금 이 페이지에 곧 필요한 리소스를 즉시, 높은 우선순위로 fetch. dns-prefetch / preconnect 가 "연결" 을 데우는 것과 달리 preload 는 실제 리소스를 받는다.
- as 필수 —
as="font"/as="image"/as="style"/as="script".as가 없으면 브라우저가 우선순위·CORS 모드·Accept 헤더를 정하지 못해 hint 가 무시되거나 리소스를 두 번 받는다. - font 는 crossorigin — font 는 same-origin 이라도 CORS 모드로 받으므로
crossorigin없으면 double-fetch. - 언제 — late-discovered critical 리소스: CSS 가 참조하는 폰트, LCP image, JS 가 동적으로 부르는 critical chunk. 처음부터 HTML 에 보이는 리소스는 보통 preload 불필요.
- unused 경고 — preload 한 리소스를 몇 초 안에 실제로 쓰지 않으면 콘솔에 "was preloaded but not used" 경고. 대역폭만 낭비한 신호.
fetchpriority — 우선순위 힌트
<img src="hero.avif" fetchpriority="high"> <!-- LCP image -->
<img src="below-fold.jpg" fetchpriority="low"> <!-- 화면 밖 -->
<script src="analytics.js" fetchpriority="low"></script>high/low/auto— 브라우저의 기본 우선순위를 위/아래로 조정.img,script,link, fetch() 에 적용 가능.- 브라우저는 기본적으로 첫 image 를 low priority 로 받기 시작한다 (LCP 여부를 아직 모름). LCP image 에
fetchpriority="high"를 주면 즉시 끌어올린다 — 가장 효과 큰 단일 변경 중 하나. - 반대로 화면 밖 image, 비핵심 script 는
low로 내려 critical 리소스에 대역폭을 양보. - preload 와 결합 —
<link rel="preload" as="image" fetchpriority="high">로 발견과 우선순위를 동시에 처리.
prefetch — 다음 navigation 용 저우선순위 캐시
<link rel="prefetch" href="/dashboard" as="document">
<link rel="prefetch" href="/next-page.js" as="script">- 다음에 갈 가능성이 높은 페이지·리소스를 가장 낮은 우선순위로 미리 받아 cache 에 저장. 현재 페이지가 한가할 때(idle) fetch.
- preload 와의 차이 — preload = 이 페이지 지금, prefetch = 다음 페이지 나중. 우선순위와 사용 시점이 정반대.
- 언제 — pagination "다음", 마법사의 다음 단계, hover 시 상세 페이지 등 사용자 다음 행동이 거의 확실할 때.
- 틀린 추측이면 받은 데이터가 버려져 대역폭 낭비. 확실한 경로에만.
modulepreload — ES module 을 받고 parse 까지
<link rel="modulepreload" href="/app.js">
<link rel="modulepreload" href="/router.js">
<link rel="modulepreload" href="/store.js">- ES module 전용 preload. 단순히 받기만 하는 게 아니라 parse + module map 등록 + dependency graph 해석까지 미리 한다.
as="script"가 암시되어 적지 않아도 된다. 일반preload as="script"와 달리 import 그래프를 따라 내려간다.- 언제 — 깊은 import chain 을 가진 entry module. bundler(Vite 등)가 동적 import 의 chunk 들에 대해 자동 삽입하는 경우가 많다.
103 Early Hints — 응답 전에 hint 부터
-- 서버 흐름
1. 요청 도착 → 서버가 페이지 렌더 시작 (DB 조회 등, 느림)
2. 본문이 준비되기 전에 먼저:
HTTP/1.1 103 Early Hints
Link: </app.css>; rel=preload; as=style
Link: <https://cdn.example.com>; rel=preconnect
3. 본문 준비 완료:
HTTP/1.1 200 OK
...실제 HTML...- 서버가 최종 200 응답을 만드는 동안(렌더·DB 대기) 먼저
103으로 preconnect / preload 힌트만 보낸다. 브라우저는 그 시간에 연결을 데우고 리소스를 받기 시작. - HTML 안의
<link>보다 더 빠르다 — HTML 의 첫 바이트(TTFB)를 기다리지 않기 때문. - Cloudflare, Fastly 등 CDN 과 Chrome 이 지원. server-side 렌더링이 느린 페이지에서 특히 효과.
Speculation Rules API — 다음 페이지를 미리 받거나 렌더
<script type="speculationrules">
{
"prefetch": [
{ "where": { "href_matches": "/articles/*" }, "eagerness": "moderate" }
],
"prerender": [
{ "where": { "selector_matches": "a.next" }, "eagerness": "eager" }
]
}
</script>- JSON 규칙으로 브라우저에게 "이 링크들은 prefetch / prerender 해도 좋다" 를 선언. prerender 는 다음 페이지를 백그라운드에서 실제로 렌더까지 해둬, 클릭 시 즉시 표시.
- deprecated 된
<link rel="prerender">의 현대적 대체. URL 패턴 매칭, eagerness(conservative / moderate / eager), hover·pointerdown 트리거 등 세밀한 제어. - prerender 는 비용이 크다(전체 페이지 렌더). eagerness 를 낮춰 오발 prerender 를 줄이고, 분석·결제 같은 부작용 있는 페이지는 제외.
흔한 실수
1. over-preloading
"빠르면 좋으니" 모든 걸 preload 하면, 모두가 high priority 가 되어 결국 아무것도 우선순위가 없는 상태가 된다. 진짜 critical 한 LCP image·font 가 그다음 줄의 비핵심 리소스와 대역폭을 다툰다. preload 는 소수 정예로.
2. as / crossorigin 누락
<!-- Bad — as 없음 → 무시되거나 double-fetch -->
<link rel="preload" href="/inter.woff2">
<!-- Bad — font 인데 crossorigin 없음 → 연결 재사용 안 됨 -->
<link rel="preload" href="/inter.woff2" as="font">
<!-- Good -->
<link rel="preload" href="/inter.woff2" as="font"
type="font/woff2" crossorigin>3. unused preload 경고
preload 했지만 몇 초 안에 안 쓰면 "was preloaded but not used" 경고. 보통 오타난 URL(쿼리·해시·버전이 실제 사용 URL 과 다름) 또는 더 이상 안 쓰는 리소스가 원인. URL 이 1바이트라도 다르면 별개로 취급되어 double-fetch.
4. preconnect 남발
실제로 안 쓰는 origin 에 preconnect 하면 연결만 열고 버린다. TLS handshake 는 공짜가 아니다. 정말 첫 화면에 쓰는 critical origin 2-4 개만.
5. prefetch 추측 빗나감
사용자가 가지 않을 페이지를 prefetch 하면 그대로 낭비. data saver 모드·느린 연결에서는 특히 민폐. Speculation Rules 의 conservative eagerness 가 안전한 기본값.
어떤 hint 를 언제 쓰나 — 요약 표
hint 받는 것 우선순위 언제
──────────────────────────────────────────────────────────────
dns-prefetch DNS 만 - 덜 critical 3rd-party
preconnect DNS+TCP+TLS - 확실히 쓸 critical origin
preload 리소스 (이 페이지) 높음 late-discovered font/LCP/CSS
modulepreload ES module + graph 높음 깊은 import chain entry
prefetch 리소스 (다음 페이지) 가장 낮음 거의 확실한 다음 navigation
fetchpriority (속성, 새 fetch X) 조정 LCP=high, 화면밖=low
103 Early Hints 서버발 preconnect/preload TTFB 느린 SSR 페이지
Speculation 다음 페이지 prefetch/prerender SPA 류 navigation 예측Core Web Vitals 와의 연결
resource hint 의 실전 가치는 대부분 LCP 와 CLS 로 측정된다. LCP image 를 preload + fetchpriority="high" 로 끌어올리면 LCP 가 직접 줄고, font 를 preconnect / preload 로 일찍 받으면 텍스트가 늦게 swap 되며 생기는 layout shift(CLS)와 invisible text 구간이 줄어든다. 다만 측정 없이 hint 를 추가하면 역효과 — 반드시 Lighthouse·WebPageTest 으로 before/after 확인. 측정 지표 자체는 how-core-web-vitals-work 가이드에서 다룬다.
참고 자료
- MDN — Preloading content with rel=preload — developer.mozilla.org
- web.dev — Optimize resource loading with the Fetch Priority API — web.dev/fetch-priority
- Chrome — Speculation Rules API — developer.chrome.com
- RFC 8297 — 103 Early Hints — datatracker.ietf.org
요약
- resource hint = 브라우저의 리소스 발견을 앞당겨 critical chain 을 단축하는 도구.
- dns-prefetch(DNS) < preconnect(DNS+TCP+TLS) < preload(실제 리소스) 순으로 "데우는" 정도가 깊어진다.
- preload 는
as필수, font 는crossorigin필수. 안 그러면 무시되거나 double-fetch. - fetchpriority="high" 를 LCP image 에 — 가장 효과 큰 단일 변경.
- prefetch / Speculation Rules 는 다음 navigation 용. preload 는 이 페이지용.
- 103 Early Hints 는 TTFB 가 느린 SSR 에서 응답 전에 연결을 데운다.
- 가장 흔한 실수는 over-preloading 과 as/crossorigin 누락. 적게, 정확히, 그리고 측정.