V8 Explicit Compile Hints — 주석 한 줄로 JavaScript 시작 속도를 630ms 줄이는 법

10 min read
V8성능 최적화ChromeJavaScript컴파일
V8 Explicit Compile Hints — 주석 한 줄로 JavaScript 시작 속도를 630ms 줄이는 법

브라우저가 JavaScript 파일을 받으면 즉시 모든 코드를 컴파일하지 않는다. "이 함수는 나중에 호출될 수도 있고, 안 될 수도 있으니까 일단 넘어가자"는 전략을 쓴다. 이를 **지연 컴파일(Lazy Compilation)**이라고 한다. 대부분의 경우 합리적인 전략이지만, 페이지 로드 직후 반드시 실행되는 함수까지 뒤로 미루면 오히려 병목이 된다. V8 13.5(Chrome 136)에 도입된 Explicit Compile Hints는 개발자가 "이 파일의 함수들은 전부 바로 실행된다"고 V8에 알려주는 기능이다. 주석 한 줄이면 된다.


지연 컴파일이 문제가 되는 순간

V8의 기본 전략은 이렇다.

단계동작시점
1. 파싱함수 경계만 파악 (가벼운 파싱)스크립트 로드 시
2. 대기함수 본문 컴파일 보류
3. 호출함수가 실제 호출될 때 컴파일메인 스레드 블로킹

비유하면 식당에서 주문을 받은 뒤 "손님이 젓가락을 들면 그때 요리를 시작"하는 것과 같다. 주문이 들어왔으면 미리 조리를 시작하는 게 당연히 빠르다.

문제는 3단계다. 함수가 호출되는 시점에 메인 스레드에서 컴파일이 일어나면, 그 시간 동안 UI가 멈춘다. 페이지 로드 직후 실행되는 초기화 코드, 라우팅 로직, 이벤트 핸들러 등이 전부 이 패턴에 해당한다.

[💡 잠깐! 이 용어는?] 지연 컴파일(Lazy Compilation): JavaScript 엔진이 함수 정의를 만나도 즉시 컴파일하지 않고, 함수가 처음 호출될 때 컴파일하는 전략. 메모리를 절약하지만, 호출 시점에 지연이 발생할 수 있다.


Explicit Compile Hints의 동작 원리

사용법은 놀라울 정도로 단순하다. JavaScript 파일 최상단에 매직 코멘트를 추가하면 된다.

entry.js — Explicit Compile Hint 적용
//# allFunctionsCalledOnLoad
 
function initializeApp() {
  const root = document.getElementById('root')
  const config = loadConfig()
  renderMainView(root, config)
}
 
function loadConfig() {
  return JSON.parse(localStorage.getItem('app-config') || '{}')
}
 
function renderMainView(root, config) {
  root.innerHTML = buildTemplate(config)
  attachEventListeners(root)
}
 
function attachEventListeners(root) {
  root.addEventListener('click', handleClick)
  root.addEventListener('scroll', handleScroll)
}
 
initializeApp()

//# allFunctionsCalledOnLoad 한 줄이면 V8은 이 파일의 모든 함수를 즉시 컴파일한다. 지연 없이 백그라운드 스레드에서 미리 컴파일을 끝내기 때문에, 함수가 호출되는 시점에는 이미 실행 준비가 완료되어 있다.

즉시 컴파일이 빠른 두 가지 이유

이유설명
중복 작업 제거지연 컴파일에서는 가벼운 파싱(함수 경계 파악) + 본격 파싱(컴파일)을 두 번 한다. 즉시 컴파일은 한 번에 끝낸다
백그라운드 병렬화스크립트가 네트워크에서 로드되는 동안 백그라운드 스레드에서 컴파일을 진행한다. 메인 스레드가 블로킹되지 않는다

비유하면 택배가 오는 동안 포장을 뜯을 가위를 미리 준비해놓는 것과 같다. 택배가 도착하면 바로 개봉할 수 있다.


성능 벤치마크

V8 팀이 상위 20개 인기 웹사이트를 대상으로 테스트한 결과:

측정 항목결과
개선된 사이트20개 중 17개
평균 파싱+컴파일 시간 감소630ms
적용 방식코드 변경 없이 주석 한 줄 추가

630ms는 사용자가 체감할 수 있는 수준의 차이다. Core Web Vitals의 FCP(First Contentful Paint)가 1.8초 이내여야 "Good" 등급인 점을 감안하면, 파싱+컴파일에서만 630ms를 절약하는 건 상당한 개선이다.

[💡 잠깐! 이 용어는?] FCP(First Contentful Paint): 브라우저가 페이지의 첫 번째 콘텐츠(텍스트, 이미지 등)를 화면에 렌더링하는 시점. LCP(Largest Contentful Paint)와 함께 Core Web Vitals의 핵심 지표다.


언제 써야 하고, 언제 피해야 하나

모든 파일에 //# allFunctionsCalledOnLoad를 붙이는 건 좋은 전략이 아니다. 즉시 컴파일은 메모리와 CPU를 더 사용하기 때문이다.

시나리오Explicit Compile Hint이유
앱 진입점 (entry.js)O페이지 로드 직후 모든 함수가 실행됨
라우터 초기화 코드O첫 렌더링에 필수적인 코드
Critical Path의 유틸리티O초기 렌더링에 사용되는 헬퍼 함수들
서드파티 라이브러리X라이브러리 전체를 즉시 컴파일하면 메모리 낭비
코드 스플리팅된 청크X지연 로드 목적으로 분리한 코드를 즉시 컴파일하면 취지에 반함
관리자 페이지 전용 코드X대부분의 사용자가 접근하지 않는 코드

비유하면 레스토랑 오픈 전에 모든 메뉴를 미리 조리해두는 건 비효율적이다. 오늘의 추천 메뉴만 미리 준비하고, 나머지는 주문이 들어오면 조리하는 게 맞다.


적용 가이드

1단계: 크리티컬 패스 파일 식별

Chrome DevTools의 Performance 탭에서 "Compile Script" 항목을 확인한다. 파싱+컴파일에 시간이 많이 소요되면서 페이지 로드 직후 실행되는 파일이 후보다.

2단계: 매직 코멘트 추가

webpack.config.js — 번들러에서 자동 삽입
const webpack = require('webpack')
 
module.exports = {
  plugins: [
    new webpack.BannerPlugin({
      banner: '//# allFunctionsCalledOnLoad',
      raw: true,
      entryOnly: true
    })
  ]
}

Webpack의 BannerPlugin을 사용하면 엔트리 파일에만 자동으로 매직 코멘트를 삽입할 수 있다. 수동으로 파일마다 추가할 필요가 없다.

3단계: 효과 측정

Chrome DevTools에서 성능 측정
# Chrome을 --enable-benchmarking 플래그로 실행
# Performance 탭 → Record → 페이지 로드
# "Parse Script" + "Compile Script" 항목의 시간 변화를 비교

향후 발전 방향

현재는 파일 단위(allFunctionsCalledOnLoad)로만 힌트를 줄 수 있다. V8 팀은 향후 함수 단위의 세분화된 컴파일 힌트를 계획하고 있다. 특정 함수만 골라서 "이건 바로 실행된다"고 지정할 수 있게 되면, 파일 전체를 즉시 컴파일하는 것보다 더 정밀한 최적화가 가능해진다.


정리

  • V8 13.5(Chrome 136)에 Explicit Compile Hints 기능이 도입됐다
  • //# allFunctionsCalledOnLoad 매직 코멘트 한 줄로 파일 내 모든 함수를 즉시 컴파일한다
  • 상위 20개 웹사이트 중 17개에서 평균 630ms 파싱+컴파일 시간이 감소했다
  • 앱 진입점, 라우터, Critical Path 코드에 적용하면 효과적이다
  • 서드파티 라이브러리나 코드 스플리팅된 청크에는 적용하지 않는 것이 좋다
  • 향후 함수 단위 힌트로 확장될 예정이다

코드 한 줄 바꾸지 않고 주석 한 줄 추가만으로 수백 밀리초를 절약할 수 있다. 비용 대비 효과가 이렇게 높은 최적화는 드물다. 페이지 로드 성능이 중요한 프로젝트라면 엔트리 파일에 먼저 적용해보는 것을 권한다.


참고:

관심 있을 만한 포스트

V8 Mutable Heap Numbers — 숫자 하나 바꿀 때마다 새 객체를 만들던 비효율을 잡다

V8 엔진이 스크립트 컨텍스트의 숫자 변수를 매번 새 HeapNumber로 할당하던 방식을 제자리 수정(mutable)으로 바꿔 최대 2.5배 성능 향상을 달성했다.

V8JavaScript

V8의 JSON.stringify가 2배 빨라졌다 — 6가지 최적화 기법 해부

V8 13.8(Chrome 138)에서 적용된 JSON.stringify 성능 개선의 기술적 배경과 6가지 핵심 최적화 전략을 분석한다.

V8JSON

V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.

V8WebAssembly

V8의 Sea of Nodes 탈출기 — 왜 우아한 이론이 실전에서 무너졌는가

V8 팀이 10년간 사용한 Sea of Nodes IR을 포기하고 Turboshaft로 전환한 7가지 이유와 그 교훈을 정리한다.

V8컴파일러

Temporal API — JavaScript Date의 30년 묵은 저주가 풀린다

Chrome 144가 Temporal API를 정식 탑재하면서 JavaScript 날짜 처리의 새 시대가 열렸다.

TemporalJavaScript

Native JSON Modules — 번들러 없이 JSON을 import하는 시대

Import Attributes와 함께 표준이 된 native JSON module. 어떻게 동작하고, 기존 번들러 방식과 뭐가 다른지 정리했다.

JavaScriptESM

Babel 7.29.0 — 10년 역사의 마지막 마이너, 그리고 8 RC1

2026년 1월 31일, Babel 7의 마지막 마이너 릴리스가 공개됐다. 이 버전이 갖는 역사적 의미와 Babel 8 RC1의 핵심 변화를 정리한다.

BabelJavaScript

Error.isError() — realm을 넘나드는 안전한 에러 검사 API

instanceof Error가 iframe과 worker에서 실패하는 이유, 그리고 이를 근본적으로 해결하는 Error.isError()의 동작 원리를 정리한다.

JavaScriptError.isError