← Blog
빌드 & 번들러

Rollup에서 Rolldown으로 — Vite가 번들러를 Rust로 다시 쓰는 이유

한 줄 요약

번들러를 Rust로 다시 쓰면 빨라지는데, 그건 "Rust"라는 언어가 마법이어서가 아니라 Rust가 네이티브 컴파일 + 안전한 멀티스레딩 + GC 없는 메모리 제어를 가능하게 해주기 때문이며, Rolldown은 그 원리로 Rollup을 재구현해 Vite 8의 단일 통합 번들러가 됐다.


왜 알아야 했나

  • "Rust 기반 번들러가 빠르다"는 말을 자주 듣는데, 빠른지 모르면 그냥 유행어로만 받아들이게 된다.
  • Vite 8에서 내부 번들러가 Rollup → Rolldown으로 바뀐 배경(이중 엔진 문제 해결)을 알아야 버전 업그레이드의 의미를 안다.
  • 언어 선택이 도구 성능에 어떻게 연결되는지 이해하면, 다른 도구(esbuild=Go, SWC=Rust 등)를 볼 때도 같은 원리로 판단할 수 있다.

핵심 개념

1. Rollup

  • ESM 기반으로 설계된 번들러. 강점은 트리셰이킹(미사용 export를 정적 분석으로 제거), 깔끔한 출력(라이브러리 번들링에 적합), 풍부한 플러그인 생태계.
  • Vite는 프로덕션 빌드에서 Rollup을 써왔다. Vite 플러그인 인터페이스는 Rollup 플러그인과 호환되도록 설계됨.
  • JavaScript로 작성됨 → 빠르지만 네이티브 번들러보다는 느림.

2. Rollup의 빌드 훅 흐름 (Vite prod 빌드의 뼈대)

  • 빌드 페이즈는 options로 시작해 항상 buildEnd로 끝남.
  • 핵심 훅: resolveId(경로→모듈 id) → load(코드 로드) → transform(코드 변환) → moduleParsed(파싱 완료).
  • 훅 종류: first(값 반환하는 첫 플러그인에서 멈춤), sequential(순차 누적), parallel(병렬).
  • 출력 생성 페이즈: renderChunk(압축·분할) → generateBundle(파일 목록·해싱) → writeBundle(디스크 기록).

3. Rolldown

  • Rust로 작성된 Rollup 재구현. "Rollup의 인터페이스 + esbuild의 속도"를 동시에 노림.
  • Rollup과 호환되는 API·플러그인 인터페이스를 제공하되, 범위(scope)는 esbuild에 더 가까움.
  • 내부 컴파일러로 Oxc(Rust)를 사용.

4. Rolldown을 만든 이유 — 이중 번들러 문제

  • 기존 Vite는 dev=esbuild, prod=Rollup의 이중 구조였다.
  • 두 변환 파이프라인 + 두 플러그인 시스템 + 둘을 동기화하는 glue 코드가 계속 늘어남.
  • 그 결과 "dev에서는 되는데 prod에서는 깨진다"는 미묘한 불일치 버그가 누적.
  • Rolldown은 dev/prod에서 동일한 모듈 해석·변환 경로를 쓰게 해 이 문제를 근본 제거.

5. Rolldown의 성능

  • 벤치마크상 Rollup보다 10~30배 빠르고, esbuild 수준에 도달.
  • 실제 사례: Linear 빌드 46초→6초(87%↓), GitLab 2.5분→22초(기존 webpack 대비 43배↑).
  • 단, dev HMR 속도는 큰 변화 없음(이미 esbuild로 충분히 빨랐음). 가장 크게 개선된 건 프로덕션 빌드 시간.
  • 비용: 모듈 그래프를 메모리에 더 많이 유지해 dev 메모리 사용량이 Vite 7 대비 약 7배.

6. Full Bundle Mode (Vite의 정체성 전복 가능성)

  • 지금까지 Vite의 정체성은 "dev에서 번들 안 함"이었는데, 충분히 빠른 Rust 번들러가 생기자 이 전제가 흔들림.
  • Rolldown이 dev에서도 prod처럼 앱 전체를 번들링하는 모드. 예비 수치로 dev 시작 3배↑, 전체 리로드 40%↑, 네트워크 요청 10배↓.
  • 중요: 아직 실험적(experimental) 기능. Vite 8을 그냥 설치하면 dev 기본 동작은 여전히 "파일별 on-demand ESM 서빙"이다. (그래서 파일 수정 시 여전히 그 파일만 컴파일되어 옴 — 달라진 건 변환 엔진이 esbuild→Oxc인 것뿐.)

7. Rust가 JavaScript보다 빠른 진짜 이유 (3가지)

핵심: Rust라는 언어 자체가 빠른 게 아니다. 세 조건이 겹쳐서 빠르고, Rust는 그걸 안전하게 제공하는 언어라서 선택됐을 뿐.

(a) 네이티브 컴파일 vs JIT

  • JS는 Node.js 위에서 실행 시점에 해석(JIT)됨 → 오버헤드 존재.
  • Rust는 미리 기계어로 컴파일됨 → 통역사 없이 원어민이 직접 말하는 격.
  • 단독 효과는 보통 2~5배 수준. 이것만으론 큰 차이 안 남.

(b) 멀티스레딩 (가장 큰 요인)

  • JavaScript는 기본 싱글 스레드 → 모듈 수백 개를 본질적으로 하나씩 처리.
  • Rust는 메모리 안전을 보장하며 여러 스레드를 자유롭게 사용 → 8코어면 8개씩 동시 변환.
  • 여기서 배수의 차이가 남. Rust가 중요한 진짜 이유 = "빠른 언어"가 아니라 "멀티스레딩을 안전하게 쓰게 해주는 언어".

(c) GC 없는 메모리 제어

  • JS는 가비지 컬렉터가 주기적으로 멈칫하며 메모리 청소 → 객체를 대량 생성·폐기하는 번들링에서 오버헤드 누적.
  • Rust는 GC 없이 컴파일 시점 규칙으로 메모리 관리 → 일시정지 없음.

8. 결정적 증거: 빠름의 비결은 언어 이름이 아니다

  • esbuild는 Rust가 아니라 Go로 작성됐는데도 매우 빠르다. Go도 네이티브 컴파일 + 멀티스레딩(고루틴)을 지원하기 때문.
  • 즉 빠름의 비결은 "Rust"가 아니라 네이티브 + 병렬 + 메모리 제어라는 성질.
  • Rolldown=Rust, esbuild=Go, Oxc=Rust, SWC=Rust로 언어는 제각각이지만 모두 빠른 이유가 이 공통 성질.

실측 — 이 원리를 내 프로젝트에서 확인해보기

벤치마크의 "10~30배"를 그대로 믿지 말고 직접 재보자는 취지로, 같은 스택의 사내 프로젝트 두 개(A·B)를 놓고 B를 Vite 7 → 8로 올리며 빌드 시간을 측정했다.

결과만 요약하면:

  • 10~30배가 아니라 33% 줄었다. 벤치마크 배수는 번들링 코어의 배수지 빌드 전체의 배수가 아니다 — 플러그인 훅은 여전히 JS로 돌기 때문에(「핵심 개념 3」의 Rollup 호환 인터페이스가 그 대가) 플러그인 바운드 프로젝트에는 상한이 생긴다.
  • 그 격차를 쫓아가다 진짜 병목을 찾았다. 의존성의 배럴 import 하나가 모듈 그래프를 10배로 부풀리고 있었다. 걷어내니 최종 54%↓.
  • 번들러 교체보다 import 형태를 바꾼 쪽이 효과가 컸다.

측정 과정과 수치 전체는 트러블슈팅 글로 분리했다:

빌드가 3배 느린 프로젝트 — 번들러를 바꿔도 안 줄던 5초의 정체


예시 / 코드

같은 작업, 처리 방식 차이 (개념적 비유):

[ JavaScript 번들러 (webpack/Rollup) ]
모듈1 → 모듈2 → 모듈3 → ... (싱글 스레드, 한 번에 하나씩)
+ 실행 중 JIT 해석 오버헤드
+ 주기적 GC 일시정지

[ Rust/Go 번들러 (Rolldown/esbuild) ]
모듈1 ┐
모듈2 ┼→ 동시 처리 (멀티 스레드, 8코어면 8개씩)
모듈3 ┘
+ 미리 컴파일된 기계어 (해석 오버헤드 없음)
+ GC 없음 (일시정지 없음)

Vite 8에서 파일 수정 시 (여전히 파일별로 옴):

GET /src/lib/Card.svelte?t=1719120000000
→ Full Bundle Mode가 기본이 아니므로 그 파일만 컴파일되어 응답
→ 변환 엔진만 esbuild → Oxc(Rust)로 바뀜, 결과물 형태는 동일

언어별 도구 매핑:

Rollup  (JavaScript) ──Rust 재구현──> Rolldown (Rust, 내부 Oxc)
webpack (JavaScript) ──Rust 재구현──> Rspack   (Rust, 내부 SWC)
esbuild (Go)         ── Vite 7까지 dev 변환 담당

헷갈렸던 점

  • "Rust 쓰면 빨라진다"는 오해: 언어가 마법이 아니라, 네이티브 컴파일 + 안전한 멀티스레딩 + GC 없음이라는 성질 덕분. Rust로 다시 써도 그 성질을 잘 활용해 재설계해야 빨라진다.
  • Vite 8인데 왜 여전히 파일별로 받아오나: Full Bundle Mode가 아직 실험 기능이라 기본값이 아님. Vite 8에서 바뀐 건 주로 (1) prod 번들러 Rollup→Rolldown, (2) dev 변환 엔진 esbuild→Oxc이고, dev의 서빙 전략(파일별 ESM)은 기본적으로 그대로.
  • dev HMR이 별로 안 빨라진 이유: 원래 esbuild로 이미 빨랐기 때문. Rolldown의 큰 이득은 프로덕션 빌드 쪽.
  • "10~30배 빠르다"를 내 빌드 시간에 그대로 대입한 것: 벤치마크 배수는 번들링 코어의 배수지 빌드 전체의 배수가 아니다. 플러그인 훅은 여전히 JS로 돌기 때문에(「핵심 개념 3」), 플러그인 바운드 프로젝트에서는 코어를 아무리 빠르게 해도 상한이 생긴다(암달의 법칙). 실측에서 33%에 그친 이유가 이것이었다.
  • 번들러만 바꾸면 되는 문제라고 본 것: 정작 더 큰 효과는 의존성 import 형태를 바꾼 쪽에서 나왔다. 도구를 최신으로 올리는 것과, 내 코드가 그 도구에게 일을 얼마나 주고 있는지는 다른 축이다. → 「실측」의 트러블슈팅 글

실무 적용

  • Vite 8 프로젝트는 자동으로 Rolldown을 씀 → 대부분 버전 한 번 올리면 프로덕션 빌드가 빨라짐. 단 체감 폭은 프로젝트마다 다름.
  • 업그레이드 후 [PLUGIN_TIMINGS] 경고가 뜨는지 확인 → 뜬다면 다음 병목은 번들러가 아니라 JS 플러그인 / 모듈 수다. 번들러를 더 손대는 대신 의존성 구조를 봐야 한다. (진단·해결 절차는 「실측」의 트러블슈팅 글에 정리)
  • "왜 이 도구가 빠른가"를 평가할 때 언어 이름이 아니라 네이티브/병렬/메모리 제어 여부로 판단.
  • dev 메모리 사용량 증가(약 7배)를 인지하고, 메모리 제약 환경에서는 모니터링.
  • Full Bundle Mode는 큰 코드베이스에서 dev 시작·리로드가 느릴 때 실험적으로 켜볼 수 있음(공식 문서로 현재 상태 확인 필요).
  • 관련 작업: [[Vite & Webpack & Rspack]]

더 공부할 것

  • Oxc(Rust 파서/트랜스포머/미니파이어)의 구조와 SWC와의 차이
  • Rolldown의 lazy barrel optimization(미사용 re-export 모듈 빌드 생략) — 실측에서 배럴 병목을 직접 만났는데 Vite 8 기본값에서는 안 걷혔다. 어떤 조건에서 작동하는지
  • 플러그인 훅을 Rust 쪽으로 옮기려는 시도가 있는지 (JS 플러그인이 남는 한 상한은 그대로다)
  • Rust의 소유권(ownership) 모델이 어떻게 GC 없이 메모리 안전을 보장하는지
  • WebAssembly와 번들러의 관계
  • Vite 8 Full Bundle Mode를 직접 켜고 dev 동작 변화 관찰

🔗 참고

⚠️ 이 분야는 매우 빠르게 변함(불과 몇 달 새 Vite 8·Rolldown 1.0 출시, Cloudflare의 VoidZero 인수 등). 실제 도입 시 각 프로젝트 공식 문서에서 최신 버전·기능 상태를 반드시 확인할 것.