rolldown
Rolldown is a JavaScript and TypeScript bundler written in Rust, built by VoidZero (Evan You's company) to become the bundler inside Vite. It keeps Rollup's config shape and plugin interface, so input, output, and plugin hooks look familiar, but the scope is closer to esbuild: it transpiles TypeScript and JSX, handles CommonJS without a plugin, inlines env values with define, and minifies through Oxc, all in native code. The pitch is Rollup compatibility at esbuild-class speed, one tool instead of a rollup-plus-esbuild-plus-terser stack, and its download numbers come largely from Vite adopting it under the hood.
The likely future default of the Vite ecosystem and already a strong pick for new library and service builds, with the caveat that it is young and plugin compatibility has edges. Migrating working Rollup or esbuild setups is only worth it when build time actually hurts.
Use it if
- You bundle libraries or Node services today with Rollup and want the same config style with much faster builds and built-in TS, JSX, and CommonJS handling
- You are on Vite and want your app and library builds to share one bundler family instead of mixing esbuild for transforms and Rollup for output
- You want minification, define replacement, and target lowering built in, so a production build needs no terser or babel plugins
- You maintain a monorepo where bundling time in CI is a real cost and a native-code bundler pays for itself
- You depend on Rollup plugins that reach deep into its JS internals: compatibility is broad but not total, and every JS plugin hook crosses the native boundary, which can erase the speed advantage on plugin-heavy configs
- You want a decade-old stable target: 1.0 shipped recently, minor releases still move quickly, and documentation for edge cases lags behind what Rollup has accumulated
- Your environment cannot take platform-specific native binaries: rolldown installs a per-platform binding package via optionalDependencies, which breaks with copied node_modules across OSes and with strict offline registries unless every binding is mirrored
- A framework CLI (Vite, tsdown, Next) already wraps your build: configure that layer instead of adopting the bundler directly, since you get Rolldown through Vite anyway
Setup reality
npm install rolldown is quick, but the real payload arrives through an optionalDependencies trick: a platform-specific @rolldown/binding-* native binary. Copying node_modules between macOS and Linux or running a registry proxy that strips optional deps produces a missing-binding error at startup, and pnpm setups need supportedArchitectures configured to prefetch other platforms. Config migration from Rollup is mostly rename-the-file, but plugin behavior differences show up at runtime, not install time, so budget a test pass over your output. Node 20.19+ or 22.12+ is required, and debugging the Rust core means reading GitHub issues rather than stepping through source.
Patterns
Bundle with a config fileconfig-file
// rolldown.config.ts
import { defineConfig } from 'rolldown'
export default defineConfig({
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
},
})
// run: npx rolldown -cTypeScript entry points work with zero extra config; rolldown -c picks up rolldown.config.ts or .js from the project root.
Bundle programmaticallyjs-api
import { rolldown } from 'rolldown'
const bundle = await rolldown({
input: 'src/index.ts',
})
await bundle.write({
dir: 'dist',
format: 'esm',
})
await bundle.close()The API mirrors Rollup: rolldown() resolves and builds the graph, write() emits files; call close() or the native side keeps resources alive.
Bundle a Node servicenode-target
import { defineConfig } from 'rolldown'
export default defineConfig({
input: 'src/server.ts',
platform: 'node',
external: [/^node:/],
output: {
dir: 'dist',
format: 'esm',
},
})platform: 'node' sets Node-friendly resolution and keeps builtins external; the regex form of external covers all node: imports at once.
Minify output for productionminify-production
export default defineConfig({
input: 'src/index.ts',
output: {
dir: 'dist',
minify: true,
sourcemap: true,
},
})Minification is built in via the Oxc minifier, so no terser plugin is needed; it is an output-level option, not a top-level one.
Inline environment valuesdefine-env
export default defineConfig({
input: 'src/index.ts',
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VERSION__: JSON.stringify('1.2.3'),
},
output: { dir: 'dist' },
})Values are substituted as expressions like esbuild's define, so strings must be JSON-stringified or they become identifiers.
Emit ESM and CommonJS from one buildmultiple-formats
import { defineConfig } from 'rolldown'
export default defineConfig([
{
input: 'src/index.ts',
output: { dir: 'dist/esm', format: 'esm' },
},
{
input: 'src/index.ts',
output: { dir: 'dist/cjs', format: 'cjs' },
},
])defineConfig accepts an array of configs like Rollup; CommonJS input is handled natively, no @rollup/plugin-commonjs required.
Rebuild on file changeswatch-mode
import { watch } from 'rolldown'
const watcher = watch({
input: 'src/index.ts',
output: { dir: 'dist' },
})
watcher.on('event', (event) => {
if (event.code === 'BUNDLE_END') console.log('rebuilt')
if (event.code === 'ERROR') console.error(event.error)
})
// later: await watcher.close()The watcher event codes follow Rollup's convention; the CLI equivalent is rolldown -c --watch.
Alias import pathspath-alias
export default defineConfig({
input: 'src/index.ts',
resolve: {
alias: {
'@': new URL('./src', import.meta.url).pathname,
},
},
output: { dir: 'dist' },
})Aliases live under resolve.alias (closer to Vite than to Rollup's plugin approach); they are not read from tsconfig paths automatically unless tsconfig support is configured.
Use an existing Rollup pluginuse-rollup-plugin
import { defineConfig } from 'rolldown'
import json from '@rollup/plugin-json'
export default defineConfig({
input: 'src/index.ts',
plugins: [json()],
output: { dir: 'dist' },
})Most standard Rollup plugins work as-is, but each JS hook call crosses into native code; check the compatibility notes before porting plugin-heavy configs.
Write a small transform pluginwrite-plugin
const replaceVersion = {
name: 'replace-version',
transform(code, id) {
if (!id.endsWith('.ts')) return null
return code.replace(/__BUILD_TIME__/g, JSON.stringify(Date.now()))
},
}
export default defineConfig({
input: 'src/index.ts',
plugins: [replaceVersion],
output: { dir: 'dist' },
})Plugin hooks use Rollup's names and semantics (transform, resolveId, load); returning null means the hook passed on the module.
Split dynamic imports into chunkscode-splitting
export default defineConfig({
input: ['src/main.ts', 'src/admin.ts'],
output: {
dir: 'dist',
format: 'esm',
chunkFileNames: 'chunks/[name]-[hash].js',
},
})Multiple entries and dynamic import() produce shared chunks automatically; naming patterns follow Rollup's entryFileNames and chunkFileNames conventions.
Target browsers with syntax loweringbrowser-target
export default defineConfig({
input: 'src/app.ts',
platform: 'browser',
transform: {
target: 'es2020',
},
output: { dir: 'dist', format: 'esm' },
})Syntax is lowered by Oxc to the given target, but like esbuild there are no runtime polyfills; missing APIs still need core-js or similar.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rollup | npm | You need the most battle-tested output control and full plugin ecosystem, and build speed is not your bottleneck |
| esbuild | npm | You want a proven fast bundler with a stable, minimal API and do not need Rollup plugin compatibility |
| vite | npm | You are building a browser app and want a dev server and framework plugins; recent Vite runs Rolldown internally |
| tsdown | npm | You are publishing a TypeScript library and want declaration files and sensible defaults on top of Rolldown |