vite-mastery

2.3 · difficulty 3/4 · 15 min read

Rolldown Architecture Deep Dive

How does Rolldown work internally? What do the three stages — OXC parsing, module graph construction, and chunk generation — each do? Where do the architectural differences with Rollup lie?

Vite 8.1Stable

Comparing the Two Architectures

Before diving into Rolldown's internals, take a moment to feel the architectural difference between Vite 7 and Vite 8 at a glance:

Bundler architecture comparisondev
V7Dual engine (esbuild + Rollup)
dev
build
Browser / Node output

Two engines had different designs, so plugin behavior often needed separate adaptation.

V8Rolldown unified engine
OXC parser
Module graph
Chunking
Browser / Node output

One Rust core keeps dev/build behavior aligned, while the compatibility layer preserves Rollup APIs.

Click or hover a node to highlight data flow

On the left is Vite 7's dual engine — esbuild handles dev transforms, Rollup handles build bundling, and the two are completely independent. On the right is Vite 8's unified Rolldown architecture.

Rolldown's Three Core Stages

Rolldown's execution can be broken down into three main stages:

Stage 1: Parsing and Module Graph Construction

Input: entry file path

What Rolldown does:

  1. Calls OXC Parser to parse the entry file, producing an AST
  2. Extracts all import declarations from the AST
  3. Runs the resolve flow for each import path (triggering the resolveId hook)
  4. For each successfully resolved module, runs load (triggering the load hook) + parse
  5. Recursively processes dependencies until all modules have been parsed

This process builds a Module Graph — a directed graph of all modules in the project and their dependency relationships.

text
Entry: src/main.tsx

  ├── resolve: "react" → node_modules/react/index.js
  ├── resolve: "./App" → src/App.tsx
  │     ├── resolve: "./Button" → src/Button.tsx
  │     └── resolve: "./styles.css" → src/styles.css
  └── resolve: "virtual:env" → \0virtual:env (handled by plugin)

Stage 2: Transform

Once the module graph is built, Rolldown runs the transform pipeline on each module:

  1. Triggers all plugins' transform(code, id) hooks (executed sequentially)
  2. OXC Transformer handles TypeScript type-stripping
  3. For JSX, calls the corresponding transform (e.g., @vitejs/plugin-react)
  4. Handles import.meta.* injections (e.g., import.meta.env, import.meta.hot)

In dev mode, Rolldown executes transforms on demand — only modules actually requested by the browser are transformed; unrequested modules are left alone. This is the fundamental reason cold starts are so fast.

In build mode, Rolldown fully transforms all modules, preparing them for tree-shaking and chunk generation.

Stage 3: Chunk Generation and Optimization (build only)

This stage only runs during vite build:

  1. Tree-shaking — based on the module graph and export / import analysis, marks and removes unused code
  2. Chunk splitting — based on dynamic import() boundaries and rolldownOptions.output.manualChunks configuration, decides how to pack modules into different chunk files
  3. Minification — OXC Minifier compresses code (removes whitespace, shortens variable names, etc.)
  4. Asset processing — CSS extraction, image hash fingerprinting, static asset copying

Hook execution order:

text
buildStart → resolveId/load/transform (per module) → buildEnd
→ renderStart → renderChunk (per chunk) → generateBundle → writeBundle → closeBundle

OXC's Place in the Architecture

OXC is not an external dependency of Rolldown — it is Rolldown's internal core:

text
Rolldown process

  ├── OXC Parser      — handles all JS/TS AST parsing
  ├── OXC Transformer — handles TS type-stripping, basic JSX support
  ├── OXC Minifier    — handles code minification in the build stage
  └── Rolldown Core   — module graph, resolve logic, chunk algorithm, plugin hooks

OXC is implemented in Rust and runs in the same process as Rolldown, with no need for cross-process communication. This is fundamentally different from Vite 7, where esbuild ran as a separate process (via child_process or WASM).

Architectural Differences from Rollup

Rolldown deliberately mirrors Rollup's plugin API, but the underlying architecture is fundamentally different:

DimensionRollupRolldown
ImplementationJavaScriptRust
ParserAcorn (JS)OXC Parser (Rust)
ConcurrencySingle-threaded + PromiseMulti-threaded (Rayon)
Tree-shakingBased on Acorn ASTBased on OXC AST
Plugin APIRollup Plugin nativeCompatibility layer (translates to Rust calls)

Rolldown uses a compatibility layer to translate JavaScript plugin hook calls into internal Rust operations. This means:

  • The plugin API is transparent to users (the syntax is identical)
  • Performance overhead is concentrated at the JS ↔ Rust boundary, not in parsing and analysis itself
  • As Rolldown iterates, certain hook behavior details may have minor differences from Rollup

Self-check

  1. Across Rolldown's three stages, what is the main difference between dev mode and build mode, and in which stage does it occur? Why?
  2. OXC Parser is integrated inside the Rolldown process. What practical advantages does this have over Vite 7's external esbuild process architecture?
  3. Is Rolldown's module graph construction depth-first or breadth-first? How does this affect the call order of the resolveId hook?
  4. Why is Rolldown's plugin API described as a "compatibility layer"? What potential risks does this layer introduce?
ts
// Analyze the following code. In the build stage, which hooks will Rolldown trigger and in what order?
// (Assume resolveId and load use default behavior; transform has one user plugin)

// vite.config.ts
export default defineConfig({
  build: {
    rolldownOptions: {
      input: "src/main.ts",
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
        },
      },
    },
  },
  plugins: [myTransformPlugin()],
})

// src/main.ts
import React from "react"
import { App } from "./App"

// Questions:
// 1. How many times will renderChunk be called? (Hint: there is a manualChunks config)
// 2. Does generateBundle come before or after renderChunk?
// 3. At which stage is myTransformPlugin's transform hook called?