Early Hints
실험적 기능: Early Hints는 실험적 기능이며 변경될 수 있습니다.
HTTP 103 Early Hints를 사용하면 최종 HTML 응답이 준비되기 전에 서버가 브라우저에 중요한 리소스를 알릴 수 있습니다. TanStack Start는 라우트 애셋과 라우트 head().links를 수집한 다음 서버 엔트리를 호출하여 런타임이 103 응답을 전송하도록 할 수 있습니다.
TanStack Start는 Early Hints를 자동으로 전송하지 않습니다. 각 배포 플랫폼은 정보성 응답을 작성하기 위한 서로 다른 API를 제공하므로, 이를 전송하는 방법은 서버 엔트리에서 결정합니다.
힌트 전송 방법 선택
대부분의 앱은 다음 패턴 중 하나를 선택해야 합니다.
| 목표 | 사용 | 절충점 |
|---|---|---|
| 가능한 한 일찍 힌트 전송 | phase === 'static'와 함께 links 사용 | 리디렉션되는 요청에 힌트를 전송할 수 있음 |
| 리디렉션에 안전한 힌트만 전송 | phase === 'dynamic'와 함께 allLinks 사용 | 라우트 로딩이 완료된 후 더 늦게 실행됨 |
| CDN에서 Early Hints 생성 | responseLinkHeader 사용 | 공개된 캐시 안정적 링크에만 안전함 |
| HTTP 103 미지원 런타임 지원 | 사전 로드 힌트 대체 수단으로 responseLinkHeader 사용 | 103처럼 서버 처리 시간을 숨기지 못함 |
브라우저는 일반적으로 탐색 시 첫 번째 103 응답만 처리합니다. 요청당 Early Hints 응답을 최대 하나만 작성하세요.
서버 엔트리에서 Early Hints 전송
src/server.ts에 onEarlyHints를 추가한 다음, 직렬화된 links를 런타임의 Early Hints API에 전달합니다.
다음 예시는 가장 이른 정적 힌트를 전송합니다:
// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
export default createServerEntry({
fetch(request) {
return handler.fetch(request, {
onEarlyHints: ({ phase, links }) => {
if (phase !== 'static' || !links.length) return
// Send `links` with your runtime-specific 103 API.
},
})
},
})
TanStack Start는 요청에 대해 onEarlyHints를 두 번 이상 호출할 수 있습니다. links에는 이전 단계에서 내보내지 않은 값만 포함됩니다. allLinks에는 지금까지 수집된 중복 제거된 모든 값이 포함됩니다.
힌트 전송 시점 선택
onEarlyHints는 두 단계로 실행될 수 있습니다.
| 단계 | 실행 시점 | 포함 내용 |
|---|---|---|
static | 라우트 일치 후, 라우터가 라우트를 로드하기 전 | 일치한 라우트에 대해 매니페스트에서 관리하는 에셋 |
dynamic | 요청이 리디렉션되지 않는 한 router.load() 완료 후 | 라우트 head() 함수가 반환한 지원되는 링크 또는 모든 힌트가 이미 전송된 경우 빈 배열 |
브라우저가 알려진 라우트 에셋을 최대한 빨리 로드하기 시작하도록 하려면 static을 사용합니다. 정적 힌트는 라우트 beforeLoad 함수보다 먼저 실행될 수 있으므로, 나중에 리디렉션되는 요청에도 전송될 수 있습니다.
힌트가 리디렉션에 안전하거나 로더를 인식해야 한다면 dynamic을 사용합니다. 정적 라우트 에셋과 동적 라우트 head() 링크를 모두 포함하는 하나의 103 응답을 원한다면, dynamic을 기다린 후 allLinks을 전송합니다.
onEarlyHints: ({ phase, allLinks }) => {
if (phase !== 'dynamic' || !allLinks.length) return
// Send one redirect-safe 103 with static and dynamic links.
// Use `allLinks` with your runtime-specific 103 API.
}
dynamic 단계는 빈 links로 실행될 수 있으므로, 로드 후 신호로도 사용할 수 있습니다.
라우트 Head에서 동적 힌트 추가
동적 Early Hints는 로더가 실행된 후 지원되는 라우트 head().links 항목에서 가져옵니다.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => getPost(params.postId),
head: ({ loaderData }) => ({
links: [
{
rel: 'preload',
href: loaderData.heroImageUrl,
as: 'image',
},
],
}),
})
router.load()이 리디렉션을 생성하면 dynamic 단계를 건너뜁니다.
rel: 'stylesheet'이 있는 라우트 head().links 항목은 Early Hints용 rel=preload; as=style로 변환됩니다. 여기에는 ?url을 사용하여 가져오고 라우트 head()에서 반환하는 스타일시트가 포함됩니다. CSS 가져오기 패턴이 Start가 스타일시트를 발견하는 시점에 어떤 영향을 주는지는 CSS 스타일링을 참조합니다.
응답 Link 헤더를 대체 수단으로 사용
수집된 힌트를 최종 HTML 응답의 HTTP Link 헤더에 추가할 수도 있습니다.
응답 Link 헤더는 103 응답처럼 서버 처리 시간을 숨기지는 않지만, 브라우저가 HTML 본문을 파싱하기 전에 이를 수신하므로 HTML만 사용할 때보다 지원되는 프리로드와 프리커넥트를 더 일찍 시작할 수 있습니다.
응답 Link 헤더는 다음과 같은 경우에 가장 유용합니다:
- 런타임에서
103응답을 작성할 수 없습니다. - CDN이 응답
Link헤더에서 Early Hints를 생성할 수 있습니다.
Start는 응답 Link 헤더를 자동으로 추가하지 않습니다. 이러한 헤더가 브라우저에서 현재 응답에만 사용될지, 공유 캐시에 저장될지, 또는 나중에 CDN에서 생성한 Early Hints로 재전송될지 알 수 없기 때문입니다.
이 예시는 수집된 모든 정적 및 동적 링크를 리디렉션이 아닌 HTML 응답에 추가합니다:
// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
export default createServerEntry({
fetch(request) {
return handler.fetch(request, {
responseLinkHeader: true,
})
},
})
CDN으로 보내기 전에 링크 필터링
일부 CDN은 응답 Link 헤더를 읽고 캐시한 후, 이후 요청에 자체 103 응답을 전송할 수 있습니다. 예를 들어 Cloudflare Early Hints는 HTML 응답의 Link 헤더를 사용할 수 있습니다.
공유 캐시 또는 CDN은 응답의 캐시 경계에서 공개되고 캐시 안정적인 링크만 재사용하도록 합니다.
포함하기에 적합한 링크는 다음과 같습니다:
- 정적 라우트 JavaScript 및 CSS 자산.
- 안정적인 URL을 사용하는 공개 글꼴, 이미지, 스타일 또는 fetch 프리로드.
- 공개 preconnect 오리진.
다음에 해당하는 링크는 제외하거나 필터링합니다:
- 인증되었거나 비공개이거나 사용자별로 다른 링크.
- 서명되었거나 만료되거나 그 밖에 수명이 짧은 링크.
- 캐시 키가 동일한 입력에 따라 달라지는 경우가 아니라면 쿠키, 헤더, 쿼리 문자열, A/B 테스트 또는 사용자 데이터에서 파생된 링크.
- 앱이 요청에 권한을 부여하기 전에 재사용하기에 안전하지 않은 링크.
Cloudflare는 몇 가지 중요한 주의 사항을 문서화합니다. Early Hints 캐시는 쿼리 문자열을 무시하고, 오리진 또는 Worker에 도달하기 전에 캐시된 힌트를 내보낼 수 있으며, 선택된 최종 응답 상태 코드와 Link 관계에서만 힌트를 생성합니다.
이러한 캐시 의미 체계 때문에 내보내는 모든 정적 또는 동적 링크가 요청 URI에 대해 공개되고 캐시 안정적인 경우에만 응답 Link 헤더를 사용합니다. 캐시 경계에 안전하지 않은 링크를 제거하려면 responseLinkHeader.filter을 사용합니다.
예를 들어 다음은 정적 매니페스트 자산만 유지합니다:
handler.fetch(request, {
responseLinkHeader: {
filter: ({ phase }) => phase === 'static',
},
})
CDN 자산 재작성 방식이 힌트에 미치는 영향
정적 Early Hints는 요청에 대해 확인된 최종 Start 매니페스트에서 수집됩니다. 즉, transformAssets의 결과를 따릅니다:
- CDN URL 재작성 내용이 Early Hints에 반영됩니다.
transformAssets에서 반환된crossOrigin이 Early Hints에 반영됩니다.- JavaScript 힌트는 클라이언트 출력 형식을 따릅니다. 모듈 출력에는
modulepreload을 사용하고 IIFE 출력에는preload; as=script을 사용합니다. cache: false을 사용한 요청별 변환이 해당 요청의 Early Hints에 반영됩니다.- Start의 CSS 인라인 처리 빌드 옵션이 CSS 자산을 HTML에 인라인하면 해당 자산을 건너뜁니다.
이벤트 형태
콜백은 EarlyHintsEvent을 받습니다:
type EarlyHintsEvent = {
phase: 'static' | 'dynamic'
hints: ReadonlyArray<EarlyHint>
links: Array<string>
allHints: ReadonlyArray<EarlyHint>
allLinks: Array<string>
}
hints은 현재 단계의 구조화된 형식입니다. links은 현재 단계의 직렬화된 HTTP Link 헤더 형식입니다. 둘 다 여러 단계에 걸쳐 중복이 제거되고 새 값만 포함하며 인덱스가 서로 일치합니다.
allHints과 allLinks에는 현재까지 요청에 대해 수집된 중복 제거 값이 모두 포함됩니다. 이 둘도 인덱스가 서로 일치하며, dynamic 단계에서 하나로 결합된 103 응답을 작성하려는 경우 유용합니다.
responseLinkHeader.filter 콜백은 다음 형태의 항목을 받습니다:
type ResponseLinkHeaderEntry = {
phase: 'static' | 'dynamic'
hint: EarlyHint
link: string
}
지원되는 링크
Start는 HTTP Link 헤더에 명확하게 매핑되는 링크 관계에 대해 Early Hints를 생성합니다:
preloadmodulepreloadpreconnectdns-prefetch
Start는 다음 속성이 있으면 이를 직렬화합니다:
hrefrelascrossOrigintypeintegrityreferrerPolicyfetchPriority
기타 head 태그, 인라인 스타일, 라우트 스크립트 및 메타데이터는 Early Hints로 변환되지 않습니다.
HTML Early Hints 처리는 최종 문서가 존재할 때까지 media, imageSrcSet 또는 imageSizes을 적용하지 않으므로, Start는 해당 속성을 103 링크로 직렬화하지 않습니다.
런타임 예시: Node
런타임에서 Node의 ServerResponse을 노출하는 경우, links을 사용하여 writeEarlyHints을 호출합니다. 이 예시는 가장 이른 시점에 static 힌트를 전송합니다:
// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
import type { ServerResponse } from 'node:http'
export default createServerEntry({
fetch(request) {
return handler.fetch(request, {
onEarlyHints: ({ phase, links }) => {
if (phase !== 'static' || !links.length) return
const response = getNodeResponseSomehow(request) as
| ServerResponse
| undefined
response?.writeEarlyHints({ link: links })
},
})
},
})
getNodeResponseSomehow을 어댑터에서 노출하는 API로 교체합니다.
런타임 예시: Node의 srvx / Nitro
Nitro는 Node 배포에 내부적으로 srvx를 사용합니다. srvx는 요청 런타임 컨텍스트에서 네이티브 Node 응답을 노출합니다. 이 예시는 정적 링크와 동적 링크를 모두 포함하는 리디렉션 안전 응답 하나를 전송하기 위해 dynamic을 기다립니다:
// src/server.ts
import handler from '@tanstack/react-start/server-entry'
import type { ServerRequest } from 'srvx'
export default {
fetch(request: Request) {
const serverRequest = request as ServerRequest
return handler.fetch(request, {
onEarlyHints: ({ phase, allLinks }) => {
if (phase !== 'dynamic') return
const response = serverRequest.runtime?.node?.res
if (response?.writeEarlyHints && allLinks.length) {
response.writeEarlyHints({ link: allLinks })
}
},
})
},
}
제한 사항
- Start 개발 서버에서는 Early Hints를 건너뜁니다.
- Start는
responseLinkHeader이 활성화된 경우에만 응답의Link헤더를 변경합니다. - 브라우저는 일반적으로 탐색에 대해 첫 번째
103응답만 처리합니다. - 정적 힌트는
beforeLoad리디렉션이 확인되기 전에 전송될 수 있습니다. - 앱 앞단의 런타임 또는 프록시가 HTTP
103응답을 지원해야 합니다.