블로그 목록

async · defer · preload · prefetch — 스크립트 로딩과 리소스 힌트

2026-06-268 min read
브라우저성능scriptpreload리소스 힌트

async/defer와 preload/prefetch는 자주 같이 묶이지만 사실 다른 그룹이다. 앞 둘은 스크립트 실행 타이밍, 뒤 둘은 리소스 사전 다운로드를 다룬다.

1. 일반 <script>의 문제

<script src="app.js"></script>

브라우저가 HTML을 파싱하다 이걸 만나면 파싱을 멈추고 JS를 다운로드·실행한 뒤 재개한다. 즉 파싱을 막는다(parser-blocking). 스크립트가 크면 화면이 늦게 뜬다. 이걸 푸는 게 async와 defer다.

2. 타임라인으로 보기

일반<script>파싱🔴 다운로드+실행 (멈춤)파싱async다운로드(병렬)파싱🟡 실행파싱받는 즉시 · 순서 Xdefer다운로드(병렬)파싱🟢 실행파싱 후 · 순서대로

async와 defer 모두 다운로드는 파싱과 병렬로 진행한다(파싱을 안 막음). 차이는 언제 실행하느냐다.

3. async — 받는 즉시 실행 (순서 보장 X)

<script async src="analytics.js"></script>
  • 다운로드는 병렬, 다운로드가 끝나는 즉시 실행 → 이 순간만 파싱이 잠깐 멈춤
  • 여러 개면 먼저 받아진 게 먼저 실행 → 순서 보장 안 됨
  • 용도: 다른 코드와 독립적인 스크립트 (애널리틱스, 광고 등)

4. defer — 파싱 후 순서대로 실행

<script defer src="app.js"></script>
  • 다운로드는 병렬, 실행은 HTML 파싱이 끝난 뒤 문서 순서대로
  • DOMContentLoaded 이전에 실행됨
  • 용도: DOM을 다루거나 순서·의존성이 있는 일반 스크립트 → 대부분 defer가 안전

5. 비교

다운로드실행 시점순서
일반파싱 멈춤즉시 (블로킹)순서대로
async병렬받는 즉시❌ 보장 안 됨
defer병렬파싱 끝난 후✅ 순서대로

type="module" 스크립트는 기본이 defer처럼 동작한다.

6. 리소스 힌트 — preload vs prefetch

이건 <script>가 아니라 <link rel>로, "이 리소스를 미리 받아둬"라고 브라우저에 힌트를 준다. 받기만 하고 실행/적용은 별개다.

preload — 지금 페이지에 곧 필요한 것 (높은 우선순위)

<link rel="preload" href="hero.woff2" as="font" crossorigin />
  • 현재 페이지에서 곧 필요한 중요 리소스를 일찍, 높은 우선순위로 받음
  • 브라우저가 늦게 발견하는 자원에 특히 유용 (CSS 안에서 참조되는 폰트, 히어로 이미지, 동적 import될 중요 JS)
  • as 속성 필수 (종류를 알려줘야 우선순위·CORS가 맞음)

prefetch — 다음 페이지에 쓸 것 (낮은 우선순위)

<link rel="prefetch" href="/next-page.js" />
  • 미래의 내비게이션에서 쓸 자원을 한가할 때(idle), 낮은 우선순위로 받아 캐시에 저장
  • 다음 페이지로 이동하면 캐시에서 즉시 → 체감 속도 ↑
  • 예측이 빗나가면 낭비 → 확신이 있을 때만

비교

대상시점·우선순위용도
preload현재 페이지일찍, 높음곧 쓸 중요 자원
prefetch다음 페이지idle, 낮음다음 내비게이션 대비

7. 친척 힌트들

<link rel="preconnect" href="https://api.example.com" />   <!-- 미리 연결 (DNS+TCP+TLS) -->
<link rel="dns-prefetch" href="https://api.example.com" />  <!-- DNS만 미리 -->
<link rel="modulepreload" href="module.js" />               <!-- ESM 모듈 preload -->
  • preconnect: 곧 요청할 도메인과 연결을 미리 맺어 핸드셰이크 지연 제거
  • dns-prefetch: DNS 조회만 미리 (preconnect의 가벼운 버전)
  • modulepreload: ESM 모듈 그래프를 미리 받기

8. React와의 연결

이 개념들은 React 앱에서 번들 로딩·코드 스플리팅·라우트 prefetch로 그대로 이어진다.

defer ↔ 번들 로딩

Vite·Next가 주입하는 스크립트는 type="module"이라 기본이 defer다. 그래서 React 번들은 HTML 파싱을 막지 않고, 파싱 후 실행돼 앱을 마운트/hydrate한다. "일반 스크립트엔 defer"가 번들러의 기본값인 셈이다.

preload/prefetch ↔ 코드 스플리팅 (React.lazy)

import()로 쪼갠 청크에 그대로 힌트를 붙인다.

// 곧 필요 → preload (높은 우선순위)
const Modal = lazy(() => import(/* webpackPreload: true */ "./Modal"));

// 나중에 갈 수도 → prefetch (idle에 미리)
const Settings = lazy(() => import(/* webpackPrefetch: true */ "./Settings"));

prefetch ↔ Next.js <Link> (가장 실무적)

<Link href="/products">제품</Link>
// 링크가 뷰포트에 들어오면 다음 페이지 JS·데이터를 idle에 prefetch
// → 클릭 시 캐시에서 즉시 전환

SPA 라우팅이 빠르게 느껴지는 핵심이 바로 이 prefetch다.

async/defer 전략 ↔ next/script

<Script src="..." strategy="beforeInteractive" /> // hydrate 전
<Script src="..." strategy="afterInteractive" />  // hydrate 후 (≈ defer/async)
<Script src="..." strategy="lazyOnload" />        // idle에

리소스 힌트 ↔ React 19 API

<link rel="preload">를 HTML에 쓰던 걸 컴포넌트 안에서 호출하면 React가 <head>로 호이스팅한다.

import { preload, preinit, preconnect } from "react-dom";

preinit("https://.../analytics.js", { as: "script" });
preload("https://.../font.woff2", { as: "font" });
preconnect("https://api.example.com");

next/font(폰트 preload)·next/image의 priority(LCP 이미지 preload)도 같은 원리다.

한눈에 보기

브라우저 개념React / 프레임워크
defer번들러의 type="module" (기본 defer)
코드 스플리팅 + preload/prefetchReact.lazy + import() 청크 (매직 코멘트)
prefetchNext.js <Link> 자동 prefetch
async/defer 전략next/script의 strategy
리소스 힌트React 19 preload/preinit/preconnect

9. 정리

async/defer 는 스크립트를 언제 실행할까(다운로드는 둘 다 병렬, 차이는 실행 타이밍·순서)를, preload/prefetch 는 리소스를 언제·어떤 우선순위로 받아둘까(실행은 별개)를 다룬다. 일반 스크립트엔 defer, 독립 스크립트엔 async, 현재 페이지 중요 자원엔 preload, 다음 페이지 대비엔 prefetch. React 세계에선 이게 번들 로딩(defer) → 코드 스플리팅 청크 → Next.js <Link> prefetch → React 19 힌트 API로 한 단계 추상화돼 자동·선언적으로 처리된다.


스크립트가 파싱을 막는 맥락과 렌더링 파이프라인은 브라우저 렌더링 과정 글에서 다룬다.