--- url: /guide/introduction.md --- # Introduction ## What is a Bundler In JavaScript development, a bundler is responsible for compiling small pieces of code (ESM or CommonJS modules) into something larger and more complex, such as a library or application. For web applications, this makes your application load and run significantly faster (even with HTTP/2). For libraries, this can avoid your consuming application having to bundle the source again, and can also improve runtime execution performance. For those interested in the details, we have written a deeper analysis on [why bundlers are still needed](/in-depth/why-bundlers). ## Why Rolldown Rolldown is primarily designed to serve as the underlying bundler in [Vite](https://vite.dev/), with the goal to replace [esbuild](https://esbuild.github.io/) and [Rollup](https://rollupjs.org/) (which are currently used in Vite as dependencies) with one unified build tool. Here's why we are implementing a new bundler from the ground up: * **Performance**: Rolldown is written in Rust. It is on the same performance level with esbuild and [10~30 times faster than Rollup](https://github.com/rolldown/benchmarks). Its WASM build is also [significantly faster than esbuild's](https://x.com/youyuxi/status/1869608132386922720) (due to Go's sub-optimal WASM compilation). * **Ecosystem Compatibility**: Rolldown supports the same plugin API with Rollup / Vite, ensuring compatibility with Vite's existing ecosystem. * **Additional Features**: Rolldown provides some important features needed in Vite but unlikely to be implemented by esbuild and Rollup (details below). Although designed for Vite, Rolldown is also fully capable of being used as a standalone, general-purpose bundler. It can serve as a drop-in replacement for Rollup in most cases, and can also be used as an esbuild alternative when better chunking control is needed. ## Rolldown's Feature Scope Rolldown provides largely compatible APIs (especially the plugin interface) with Rollup, and has similar treeshaking capabilities for bundle size optimization. However, Rolldown's feature scope is more similar to esbuild, offering these [additional features](./notable-features) as built-in: * Platform presets * TypeScript / JSX / syntax lowering transforms * Node.js compatible module resolution * ESM / CJS module interop * `define` * `inject` * Minification (WIP) Rolldown also has a few concepts that have close equivalents in esbuild, but do not exist in Rollup: * [Module Types](./notable-features#module-types) (Experimental) * [Plugin hook filters](/apis/plugin-api/hook-filters) Finally, Rolldown provides some features that esbuild and Rollup do not (and may not intend to) implement: * [Manual code splitting](./notable-features#manual-code-splitting) * HMR support (WIP) ## Credits Rolldown wouldn't exist without all the lessons we learned from other bundlers like [esbuild](https://esbuild.github.io/), [Rollup](https://rollupjs.org/), [webpack](https://webpack.js.org/), and [Parcel](https://parceljs.org/). We have the utmost respect and appreciation towards the authors and maintainers of these important projects. --- --- url: /guide/getting-started.md --- # Getting Started :::tip Looking for specific use cases? For most applications, using [Rolldown through Vite](https://vite.dev/guide/rolldown.html#how-to-try-rolldown) is the recommended approach, as it provides a complete development experience with dev server, HMR, and optimized production builds. For library bundling, check out [tsdown](https://tsdown.dev/). ::: ## Installation ::: code-group ```sh [vp] $ vp add -D rolldown ``` ```sh [npm] $ npm install -D rolldown ``` ```sh [pnpm] $ pnpm add -D rolldown ``` ```sh [yarn] $ yarn add -D rolldown ``` ```sh [bun] $ bun add -D rolldown ``` ::: ::: details Using a minor platform (CPU architecture, OS) ? Prebuilt binaries are distributed for the following platforms (grouped by [Node.js v24 platform support tier](https://github.com/nodejs/node/blob/v24.x/BUILDING.md#platform-list)): * Tier 1 * Linux x64 glibc (`x86_64-unknown-linux-gnu`) * Linux arm64 glibc (`aarch64-unknown-linux-gnu`) * Windows x64 (`x86_64-pc-windows-msvc`) * Apple x64 (`x86_64-apple-darwin`) * Apple arm64 (`aarch64-apple-darwin`) * Tier 2 * Windows arm64 (`aarch64-pc-windows-msvc`) * Linux s390x glibc (`s390x-unknown-linux-gnu`) * Linux ppc64le glibc (`powerpc64le-unknown-linux-gnu`) * Experimental * Linux x64 musl (`x86_64-unknown-linux-musl`) * Linux armv7 (`armv7-unknown-linux-gnueabihf`) * FreeBSD x64 (`x86_64-unknown-freebsd`) * OpenHarmony arm64 (`aarch64-unknown-linux-ohos`) * Other * Linux arm64 musl (`aarch64-unknown-linux-musl`) * Android arm64 (`aarch64-linux-android`) * Wasm + Wasi (`wasm32-wasip1-threads`) If you are using a platform that a prebuilt binary is not distributed, you have the following options: * Use the Wasm build 1. Download the Wasm build. * For npm, you can run `npm install --cpu wasm32 --os wasip1-threads`. * For yarn or pnpm, you need to add the following content to your `.yarnrc.yaml` or `pnpm-workspace.yaml`: ```yaml supportedArchitectures: os: - wasip1-threads cpu: - wasm32 ``` 2. Make Rolldown load the Wasm build. * If the prebuilt binary is not available, Rolldown will fallback to the Wasm binary automatically. * In case you need to force Rolldown to use the Wasm build, you can set `NAPI_RS_FORCE_WASI=error` environment variable. * Build from source 1. Clone the repository. 2. Setup the project by following [the setup instructions](/development-guide/setup-the-project). 3. Build the project by following [the build instructions](/development-guide/building-and-running). 4. Set the `NAPI_RS_NATIVE_LIBRARY_PATH` environment variable to the path of `packages/rolldown` in the cloned repository. ::: ### Release Channels * [latest](https://npmx.dev/package/rolldown#versions): currently `1.x.x`. * [pkg.pr.new](https://pkg.pr.new/~/rolldown/rolldown): continuously released from the `main` branch. Install with `npm i https://pkg.pr.new/rolldown@sha` where `sha` is a successful build listed on [pkg.pr.new](https://pkg.pr.new/~/rolldown/rolldown). ## Using the CLI To verify Rolldown is installed correctly, run the following in the directory where you installed it: ```sh $ ./node_modules/.bin/rolldown --version ``` You can also check out the CLI options and examples with: ```sh $ ./node_modules/.bin/rolldown --help ``` ### Your first bundle Let's create two source JavaScript files: ```js [src/main.js] import { hello } from './hello.js'; hello(); ``` ```js [src/hello.js] export function hello() { console.log('Hello Rolldown!'); } ``` Then run the following in the command line: ```sh $ ./node_modules/.bin/rolldown src/main.js --file bundle.js ``` You should see the content written to `bundle.js` in your current directory. Let's run it to verify it's working: ```sh $ node bundle.js ``` You should see `Hello Rolldown!` printed. ### Adding a package.json build script To avoid typing the long command, we can move it inside a `package.json` script: ```json{5} [package.json] { "name": "my-rolldown-project", "type": "module", "scripts": { "build": "rolldown src/main.js --file bundle.js" }, "devDependencies": { "rolldown": "^1.0.0" } } ``` Now we can run the build with just: ::: code-group ```sh [vp] $ vp run build ``` ```sh [npm] $ npm run build ``` ```sh [pnpm] $ pnpm run build ``` ```sh [yarn] $ yarn build ``` ```sh [bun] $ bun run build ``` ::: ## Using the Config File When more options are needed, it is recommended to use a config file for more flexibility. A config file can be written in `.js`, `.cjs`, `.mjs`, `.ts`, `.mts`, or `.cts` formats. Let's create the following config file: ```js [rolldown.config.js] import { defineConfig } from 'rolldown'; export default defineConfig({ input: 'src/main.js', output: { file: 'bundle.js', }, }); ``` Rolldown supports most of the [Rollup config options](https://rollupjs.org/configuration-options), with some [notable additional features](./notable-features). See the [reference](/reference/) for the full list of options. While exporting a plain object also works, it is recommended to utilize the [`defineConfig`](/reference/Function.defineConfig) helper method to get options intellisense and auto-completion. This helper is provided purely for the types and returns the options as-is. Next, in the npm script, we can instruct Rolldown to use the config file with the `--config` CLI option (`-c` for short): ```json{5} [package.json] { "name": "my-rolldown-project", "type": "module", "scripts": { "build": "rolldown -c" }, "devDependencies": { "rolldown": "^1.0.0" } } ``` ### Multiple builds in the same config You can also specify multiple configurations as an array, and Rolldown will bundle them in parallel. ```js [rolldown.config.js] import { defineConfig } from 'rolldown'; export default defineConfig([ { input: 'src/main.js', output: { format: 'esm', }, }, { input: 'src/worker.js', output: { format: 'iife', dir: 'dist/worker', }, }, ]); ``` ## Using Plugins Rolldown's plugin API is identical to that of Rollup's, so you can reuse most of the existing Rollup plugins when using Rolldown. That said, Rolldown provides many [built-in features](./notable-features) that make it unnecessary to use plugins. Also Rolldown provides some builtin plugins that can be used for some use cases. See [Builtin Plugins](/builtin-plugins/) for more information. Community plugins that are published to npm are listed in [Vite Plugin Registry](https://registry.vite.dev/plugins). ## Using the API Rolldown provides a JavaScript API that is compatible with [Rollup's](https://rollupjs.org/javascript-api/), which separates `input` and `output` options: ```js import { rolldown } from 'rolldown'; const bundle = await rolldown({ // input options input: 'src/main.js', }); // generate bundles in memory with different output options await bundle.generate({ // output options format: 'esm', }); await bundle.generate({ // output options format: 'cjs', }); // or directly write to disk await bundle.write({ file: 'bundle.js', }); ``` Alternatively, you can also use the more concise `build` API, which accepts the exact same options as the config file export: ```js import { build } from 'rolldown'; // build writes to disk by default await build({ input: 'src/main.js', output: { file: 'bundle.js', }, }); ``` ## Using the Watcher The rolldown watcher api is compatible with rollup [watch](https://rollupjs.org/javascript-api/#rollup-watch). ```js import { watch } from 'rolldown'; const watcher = watch({/* option */}); // or watch([/* multiple option */] ) watcher.on('event', () => {}); await watcher.close(); // This is different than rollup: rolldown returns a promise here. ``` --- --- url: /guide/notable-features.md --- # Notable Features This page documents some notable features in Rolldown that do not have built-in equivalents in Rollup. ## Platform presets * Configurable via the [`platform`](/reference/InputOptions.platform) option. * Default: `'node'` for `cjs` output, `'browser'` otherwise * Possible values: `browser | node | neutral` Similar to [esbuild's `platform` option](https://esbuild.github.io/api/#platform), this option provides some sensible defaults regarding module resolution and how to handle `process.env.NODE_ENV`. **Notable differences from esbuild:** * The default output format is always `esm` regardless of platform. :::tip Rolldown does not polyfill Node built-ins when targeting the browser. You can opt-in to it with [rolldown-plugin-node-polyfills](https://github.com/rolldown/rolldown-plugin-node-polyfills). ::: ## Built-in transforms Rolldown supports the following transforms out of the box, powered by [Oxc](https://oxc.rs/docs/guide/usage/transformer). The transform is configurable via the [`transform`](/reference/InputOptions.transform) option. The following transforms are supported: * TypeScript * Sets configurations based on the `tsconfig.json` when the [`tsconfig`](/reference/InputOptions.tsconfig) option is provided. * Supported legacy decorators and decorator metadata. * JSX * Syntax lowering * Automatically transforms modern syntax to be compatible with your defined target. * Supports [down to ES2015](https://oxc.rs/docs/guide/usage/transformer/lowering#transformations). ## CJS support Rolldown supports mixed ESM / CJS module graphs out of the box, without the need for `@rollup/plugin-commonjs`. It largely follows esbuild's semantics and [passes all esbuild ESM / CJS interop tests](https://github.com/evanw/bundler-esm-cjs-tests). See [Bundling CJS](/in-depth/bundling-cjs) for more details. ## Module resolution * Configurable via the [`resolve`](/reference/InputOptions.resolve) option * Powered by [oxc-resolver](https://github.com/oxc-project/oxc-resolver), aligned with webpack's [enhanced-resolve](https://github.com/webpack/enhanced-resolve) Rolldown resolves modules based on TypeScript and Node.js' behavior by default, without the need for `@rollup/plugin-node-resolve`. When top-level [`tsconfig`](/reference/InputOptions.tsconfig) option is provided, Rolldown will respect `compilerOptions.paths` in the specified `tsconfig.json`. ## Define * Configurable via the [`transform.define`](/reference/InputOptions.transform#define) option. This feature provides a way to replace global identifiers with constant expressions. Aligns with the respective options in [Vite](https://vite.dev/config/shared-options.html#define) and [esbuild](https://esbuild.github.io/api/#define). ::: tip `@rollup/plugin-replace` behaves differently Note it behaves differently from [`@rollup/plugin-replace`](https://github.com/rollup/plugins/tree/master/packages/replace) as the replacement is AST-based, so the value to be replaced must be a valid identifier or member expression. Use the built-in [`replacePlugin`](/builtin-plugins/replace) for that purpose. ::: ## Inject * Configurable via the [`transform.inject`](/reference/InputOptions.transform#inject) option. This feature provides a way to shim global variables with a specific value exported from a module. This feature is equivalent of [`@rollup/plugin-inject`](https://github.com/rollup/plugins/tree/master/packages/inject) and conceptually similar to [esbuild's `inject` option](https://esbuild.github.io/api/#inject). ## Manual Code Splitting * Configurable via [`output.codeSplitting`](/reference/OutputOptions.codeSplitting) option. Rolldown allows controlling the chunking behavior granularly, similar to webpack's [`optimization.splitChunks`](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks) feature. See [Manual Code Splitting](/in-depth/manual-code-splitting) for more details. ## Module types * ⚠️ Experimental This is conceptually similar to [esbuild's `loader` option](https://esbuild.github.io/api/#loader), allowing users to globally associate file extensions to built-in module types via the [`moduleTypes`](/reference/InputOptions.moduleTypes) option, or specify module type of a specific module in plugin hooks. It is discussed in more details [here](/in-depth/module-types). ## Minification * Configurable via the [`output.minify`](/reference/OutputOptions.minify) option. The minification is powered by [Oxc Minifier](https://oxc.rs/docs/guide/usage/minifier). See its documentation for more details. --- --- url: /guide/troubleshooting.md --- # Troubleshooting ## Performance Performance is a primary goal for Rolldown. However, build performance isn't solely determined by Rolldown itself. It's also significantly affected by the environment it runs in and the plugins used. While we continuously strive to improve Rolldown to minimize these external factors, there are inherent limitations and areas where optimizations are still ongoing. This guide provides insights into potential bottlenecks and how you can mitigate them. ### Environment The operating system and its configuration can impact build times, particularly file system operations. #### Windows File system access on Windows is generally slower compared to other operating systems like macOS or Linux. Especially, antivirus software can make this much worse. But even without interference from antivirus programs, baseline file system performance tends to be slower. It is 3 times slower than macOS and 10 times slower than Linux. This becomes a bottleneck when most of the transforms are done without a plugin. To improve performance on Windows, consider using alternative file system environments: 1. [**Dev Drive**](https://learn.microsoft.com/en-us/windows/dev-drive/): A newer Windows feature designed for developer workloads, using the Resilient File System (ReFS). Using a Dev Drive can lead to a **2x to 3x speedup** compared to the standard Windows NTFS file system for file system operations. 2. [**Windows Subsystem for Linux (WSL)**](https://learn.microsoft.com/en-us/windows/wsl/): WSL lets Linux environment to run on Windows easily, which offers significantly better file system performance. Placing your project files and running the build process within WSL can result in speedups of around **10x** compared to the standard Windows NTFS file system for file system operations. :::details Benchmark Reference The benchmark script used is described in this blog post ([How fast can you open 1000 files?](https://lemire.me/blog/2025/03/01/how-fast-can-you-open-1000-files/)). The results were: | File System / Threads | 1 | 2 | 4 | 8 | 16 | | -----------------------: | ----: | ----: | ----: | ----: | ----: | | Windows NTFS | 286ms | 153ms | 85ms | 106ms | 110ms | | Windows Dev Drive (ReFS) | 124ms | 67ms | 35ms | 48ms | 55ms | | WSL (ext4) | 24ms | 13ms | 7.8ms | 9.0ms | 13ms | The benchmark was ran on the following environment: * OS: Windows 11 Pro 23H2 22631.5189 * CPU: AMD Ryzen 9 5900X * Memory: DDR4-3600 32GB * SSD: Western Digital Black SN850X 1TB ::: ### Plugins Plugins extend Rolldown's functionality, but can also introduce performance overhead. #### Plugin Hook Filters Rolldown provides a feature called **Plugin Hook Filters**. This allows you to specify precisely which modules a plugin hook should process, reducing the communication overhead between JavaScript and Rust. For detailed information on how filters work internally, refer to the [Hook Filters](/apis/plugin-api/hook-filters) page. If you are a plugin user and the plugin you use does not have hook filters specified, you can apply them by using the `withFilter` utility function exported by Rolldown. ```js import yaml from '@rollup/plugin-yaml'; import { defineConfig } from 'rolldown'; import { withFilter } from 'rolldown/filter'; export default defineConfig({ plugins: [ // Run the transform hook of the `yaml` plugin only for modules which end in `.yaml` withFilter(yaml({/*...*/}), { transform: { id: /\.yaml$/ } }), ], }); ``` #### Leverage Built-in Features Rolldown includes several built-in features designed for efficiency. Where possible, prefer using these native capabilities over external Rollup plugins that perform similar tasks. Relying on built-in functionality often means the processing happens entirely within Rust, allowing to process in parallel. Check the [Rolldown Features](/guide/notable-features) page for capabilities that does not exist in Rollup. For example, the following common Rollup plugins may be replaced with Rolldown's built-in features: * `@rollup/plugin-alias`: [`resolve.alias`](/reference/InputOptions.resolve#alias) option * `@rollup/plugin-commonjs`: supported out of the box * `@rollup/plugin-inject`: [`inject`](/guide/notable-features#inject) option * `@rollup/plugin-replace`: [`replacePlugin`](/builtin-plugins/replace) * `@rollup/plugin-node-resolve`: supported out of the box * `@rollup/plugin-json`: supported out of the box * `@rollup/plugin-swc`, `@rollup/plugin-babel`, `@rollup/plugin-sucrase`: supported out of the box via Oxc (complex configurations might still require the plugin) * `@rollup/plugin-terser`: `output.minify` option ## Avoiding Direct `eval` The `eval()` function evaluates a string of JavaScript code. `eval()` calls have two modes: direct eval and indirect eval. Direct eval refers to the case where the global `eval` function is called directly. Differently from indirect eval, direct eval allows the passed string to access the local scope variables of the caller. Direct eval is problematic when bundling the code for many reasons: * Rolldown applies an optimization called "scope hoisting" that puts multiple files into a single scope. However, this means code evaluated by direct `eval` can read and write variables in a different file in the bundle! This is a correctness issue because the evaluated code may try to access a global variable but may accidentally access a private variable with the same name from another file instead. **It can potentially even be a security issue** if a private variable in another file has sensitive data. * Rolldown may rename some variables in the bundle to avoid name collisions. While this is not a problem when not using direct eval, it is a problem for direct eval because the code evaluated by direct eval may try to reference the renamed variables by the original name. * Minifiers avoid mangling variable names that may be referenced from the direct eval code for correctness. There are also other optimizations prevented by direct eval. This means the output code would not be reduced efficiently. Luckily, it is usually easy to avoid using direct eval. There are two commonly-used alternatives that avoid all of the drawbacks mentioned above: * `(0, eval)('x')` This is most common way to use indirect eval. There are also other ways to trigger indirect eval. For example, `var eval2 = eval; eval2('x')` and `[eval][0]('x')` and `window.eval('x')` are all indirect eval calls. When you use indirect eval, the code is evaluated in the global scope instead of in the inline scope of the caller. * `new Function('x')` This constructs a new function object at run-time. It is as if you wrote `function() { x }` in the global scope except that `x` can be an arbitrary string of code. This form is sometimes convenient because you can add arguments to the function, and use those arguments to expose variables to the evaluated code. For example, `(new Function('env', 'x'))(someEnv)` is as if you wrote `(function(env) { x })(someEnv)`. This is often a sufficient alternative for direct `eval` when the evaluated code needs to access local variables because you can pass the local variables in as arguments. ## Avoid relying on `this` in exported functions In JavaScript, `this` is a special variable that is bound to a different value normally depending on how the function is called. For example, when the function is called as a method on an object, the `this` variable is bound to the object. ```js const obj = { method() { console.log(this); // `this` is `obj` here }, }; obj.method(); ``` Similar to this, when a function is exported from a module and is called via a module namespace object, based on the ECMAScript spec, the `this` variable is bound to the module namespace object. ```js // imported.js export function method() { console.log(this); // `this` is the module namespace object of `imported.js` here } // main.js import * as namespace from './imported.js'; namespace.method(); ``` However, **Rolldown does not necessarily preserve the value of `this`** for this case. For this reason, it is recommended to avoid relying on `this` in exported functions. That said, this behavior is common across most bundlers and would not be a problem in practice. The reason for this behavior is because preserving the value of `this` limits the possibilities of tree-shaking. For example, if the `this` variable needs to be bound to the module namespace object, all the exports in that module cannot be tree-shaken even if they are not used through the `import`s. ::: tip A similar issue when outputting your code as CJS Similar to the issue described above, Rolldown does not necessarily preserve the value of `this` of exported functions when outputting your code as CJS. In this case, `this` that should be `undefined` may be bound to the `module.exports` object instead. ::: ## Avoid relying on Temporal Dead Zone (TDZ) errors In ECMAScript, `let`, `const`, and `class` declarations create a binding that exists from the start of its scope but is uninitialized until the declaration itself is evaluated. Reading the binding during this window, even via `typeof`, throws a `ReferenceError`. This window is known as the "Temporal Dead Zone (TDZ)". ```js typeof x; // ReferenceError: Cannot access 'x' before initialization let x = 1; ``` However, **Rolldown does not necessarily preserve TDZ semantics**, for a mix of correctness and performance reasons. Code that relies on a TDZ access throwing may behave differently in the bundled output, and should be avoided. For example, Rolldown always rewrites a module top-level `class X {}` to `var X = class {}` so that the binding can be hoisted alongside other top-level declarations. As a result, the binding is observable as `undefined` (rather than throwing) before the declaration is reached. Setting [`output.topLevelVar`](/reference/OutputOptions.topLevelVar) to `true` extends the same rewriting to top-level `let` and `const`. ```js // In ESM, this throws ReferenceError. // In Rolldown's bundled output, `typeof X` evaluates to `"undefined"`. console.log(typeof X); class X {} ``` As another example, Rolldown may inline exported `const` values at their use sites, even across an import cycle. When the cycle causes the constant to be read before its declaration runs, ESM would throw, but Rolldown returns the inlined value instead. ::: code-group ```js [entry.js] import './constants.js'; ``` ```js [constants.js] export const foo = 123; export function bar() { return foo; } import './cycle.js'; ``` ```js [cycle.js] import { bar } from './constants.js'; // In ESM, `bar()` throws ReferenceError because `foo` is in TDZ. // In Rolldown's bundled output, `bar()` returns `123`. console.log(bar()); ``` ::: ## Warning: "Sourcemap is likely to be incorrect" You'll see this warning if you generate a sourcemap with your bundle ([`sourcemap: true`](/reference/OutputOptions.sourcemap) or `sourcemap: 'inline'`) but you're using one or more plugins that transformed code without generating a sourcemap for the transformation. Usually, a plugin will only omit the sourcemap if it (the plugin, not the bundle) was configured with `sourcemap: false` - so all you need to do is change that. If the plugin doesn't generate a sourcemap, consider raising an issue with the plugin author. ## Error: "Cannot find module '@rolldown/binding-...'" This error means Node.js found the `rolldown` package but not the platform-specific native package. It is usually caused by a known npm bug with optional dependencies ([npm/cli#4828](https://github.com/npm/cli/issues/4828)); if you installed with npm, removing `node_modules` and `package-lock.json` and reinstalling fixes it. It can also happen when the config file lives in a symlinked directory that points into another project, for example one shared between Windows and WSL ([#9854](https://github.com/rolldown/rolldown/issues/9854)). Node.js resolves the config to its real path before resolving its imports, so `import ... from 'rolldown'` can pick up a `node_modules` installed for a different platform. Keep the config outside the symlinked directory, or run with the `NODE_OPTIONS=--preserve-symlinks` environment variable set (not compatible with pnpm, whose `node_modules` layout relies on symlinks). --- --- url: /in-depth/why-bundlers.md --- # Why do we still need bundlers? ## Skipping the build step is impractical With the general availability of native ES modules and HTTP/2 in modern browsers, some developers are advocating for an unbundled approach for shipping web applications, even in production. While this approach works for smaller applications, in our opinion bundling is still very much necessary if you are shipping anything non-trivial and care about performance (which translates to better user experience). Even in a polished unbundled deployment model, a build step is still often unavoidable. Take Rails 8's default import-map-based approach for example: all JavaScript assets still go through a build step in order to fingerprint the assets and generate the import map and modulepreload directives. It's just handled via `importmap-rails` and Propshaft instead of a JavaScript bundler. Moreover, the unbundled approach will hit its limits if you have any of the following requirements: * Require modern JavaScript features like ES6+, TypeScript, or JSX. * Need to leverage bundler-specific optimizations like tree-shaking, code splitting, or minification. * Utilize libraries or frameworks that depend on a build step. * Utilize NPM dependencies that ship unbundled source code (results in too many requests). Going with unbundled means locking yourself out of a big part of the JS ecosystem and giving up on many possible performance optimizations that could benefit your end users. The main argument of avoiding JavaScript bundlers is added complexity and slowing down the dev feedback loop. However, modern JS tooling has improved a lot on this front over the past few years. Our goal with Vite / Rolldown is to improve these aspects further and make the build step feel invisible. ## The case for bundlers Fundamentally, bundlers exist because of the unique constraints of web applications: they need to be delivered over the network on-demand. Bundlers can make web applications more performant in three ways: 1. Reduce the amount of network requests and waterfalls. 2. Reduce total bytes sent over the network. 3. Improve JavaScript execution performance. ## Reduce network requests and waterfalls The first important thing we need to acknowledge is that **HTTP/2 does not mean you can stop caring about number of HTTP requests**. Although HTTP/2 theoretically supports unlimited multiplexing, most browsers / servers have a default limit of around 100 on the maximum number of concurrent streams per connection. Every network request also comes with fixed overhead (header processing, TLS encryption, multiplexing, etc.) on both the server and the client. More requests means more server load, and the actual concurrency is limited by how fast your server can serve the module files. Applications that contain thousands of unbundled modules will still create serious network bottlenecks even under HTTP/2. Deep import chains also result in network waterfalls - i.e. the browser needs to make multiple network roundtrips to fetch the entire module graph. This can be mitigated to some extent with `modulepreload` directives, but generating these requires tooling support, and bloating the HTML with thousands of `modulepreload` directives in `` is also a performance issue in itself. Bundling can drastically reduce such overhead by combining thousands of modules into an optimal number of chunks that both the server and the browser can handle with ease. Bundling also flattens the import chain depth to reduce waterfalls, and can provide the data needed to generate `modulepreload` directives. In its essence, bundling moves the work of combining the module graph to the build phase, instead of incurring it as a runtime cost for every visitor. This makes large applications load significantly faster on initial visit, especially in poor network conditions. ### Trade-offs in caching strategy One argument supporting the unbundled approach is that it allows each module to be cached individually, reducing the amount of cache invalidation when the application is updated. However, this comes with the trade-off of a much slower initial load as explained above. Sub-optimal bundling configurations can cause cascading chunk hash validations, causing users to have to re-download a large part of the app when the app is updated. But this is a solvable problem: bundlers can also leverage import maps and advanced chunking control to limit hash invalidation and improve cache hit rate. We do intend to provide an improved, more caching-friendly default chunking strategy in Vite / Rolldown in the future. ## Reduce total bytes sent over the network Bundling can also greatly reduce overall size of JavaScript sent over the wire. First, bundles can hoist multiple modules into the same scope, removing all the import / export statements between them. Second, treeshaking / dead code elimination is an optimization that can only be performed by statically analyzing the source code at build time. Native ESM loads and evaluates everything eagerly, so even if you only use a single export from a big module, the entire module has to be downloaded and evaluated. With a smart bundler, exports that are not used can be completely removed from the final bundle, saving lots of bytes. Finally, minification and gzip / brotli compression are considerably more efficient when performed on bundled code compared to individual modules. With these factors combined, users download less code, and your servers use less outbound bandwidth. ## Improve JavaScript execution performance JavaScript is an interpreted language, and modern JavaScript engines often employ advanced JIT compilation to make it run faster. However, there is also non-trivial cost involved in parsing and compiling JavaScript. Sending less JavaScript code not only saves bandwidth - it also means less JavaScript needs to be compiled and evaluated in the browser, leading to faster application startup time. Some bundlers / minifiers also can perform optimizations like constant folding / ahead-of-time evaluation to varying extent, making the bundled code more efficient than their hand-written source. *** In conclusion, bundling is still a beneficial, and in many cases necessary step in web development, and will continue to be so in the foreseeable future. --- --- url: /in-depth/why-plugin-hook-filter.md --- # Why Plugin Hook Filters? ## The Problem Even though Rolldown's core is written in Rust with parallel processing capabilities, **adding JavaScript plugins can significantly slow down your builds**. Why? Because each plugin hook gets called for *every* module, even when the plugin doesn't care about most of them. For example, if you have a CSS plugin that only transforms `.css` files, it still gets called for every `.js`, `.ts`, `.jsx`, and other file in your project. With 10 plugins, this overhead multiplies, causing build times to increase by **3-4x**. Plugin hook filters solve this by letting Rolldown skip unnecessary plugin calls at the Rust level, keeping your builds fast even with many plugins. ## Real-World Impact Let's see the actual performance difference with a benchmark using [apps/10000](https://github.com/rolldown/benchmarks/tree/main/apps/10000): branch: https://github.com/rolldown/benchmarks/pull/3 ```diff diff --git a/apps/10000/rolldown.config.mjs b/apps/10000/rolldown.config.mjs --- a/apps/10000/rolldown.config.mjs +++ b/apps/10000/rolldown.config.mjs @@ -1,8 +1,25 @@ import { defineConfig } from "rolldown"; -import { minify } from "rollup-plugin-esbuild"; +// import { minify } from "rollup-plugin-esbuild"; const sourceMap = !!process.env.SOURCE_MAP; const m = !!process.env.MINIFY; +const transformPluginCount = process.env.PLUGIN_COUNT || 0; +let transformCssPlugin = Array.from({ length: transformPluginCount }, (_, i) => { + let index = i + 1; + return { + name: `transform-css-${index}`, + transform(code, id) { + if (id.endsWith(`foo${index}.css`)) { + return { + code: `.index-${index} { + color: red; +}`, + map: null, + }; + } + } + } +}) export default defineConfig({ input: { main: "./src/index.jsx", @@ -11,13 +28,7 @@ export default defineConfig({ "process.env.NODE_ENV": JSON.stringify("production"), }, plugins: [ - m - ? minify({ - minify: true, - legalComments: "none", - target: "es2022", - }) - : null, + ...transformCssPlugin, ].filter(Boolean), profilerNames: !m, output: { diff --git a/apps/10000/src/index.css b/apps/10000/src/index.css deleted file mode 100644 diff --git a/apps/10000/src/index.jsx b/apps/10000/src/index.jsx --- a/apps/10000/src/index.jsx +++ b/apps/10000/src/index.jsx @@ -1,7 +1,16 @@ import React from "react"; import ReactDom from "react-dom/client"; import App1 from "./f0"; -import './index.css' +import './foo1.css' +import './foo2.css' +import './foo3.css' +import './foo4.css' +import './foo5.css' +import './foo6.css' +import './foo7.css' +import './foo8.css' +import './foo9.css' +import './foo10.css' ReactDom.createRoot(document.getElementById("root")).render( ``` **Setup:** * 10 CSS files (`foo1.css` to `foo10.css`) * Each plugin transforms only one specific CSS file (e.g., plugin 1 only cares about `foo1.css`) * Variable number of plugins controlled via `PLUGIN_COUNT` * Plugins use standard pattern: check if file matches, return early if not ### Without Filter (Traditional Approach) ```bash Benchmark 1: PLUGIN_COUNT=0 node --run build:rolldown Time (mean ± σ): 745.6 ms ± 11.8 ms [User: 2298.0 ms, System: 1161.3 ms] Range (min … max): 732.1 ms … 753.6 ms 3 runs Benchmark 2: PLUGIN_COUNT=1 node --run build:rolldown Time (mean ± σ): 862.6 ms ± 61.3 ms [User: 2714.1 ms, System: 1192.6 ms] Range (min … max): 808.3 ms … 929.2 ms 3 runs Benchmark 3: PLUGIN_COUNT=2 node --run build:rolldown Time (mean ± σ): 1.106 s ± 0.020 s [User: 3.287 s, System: 1.382 s] Range (min … max): 1.091 s … 1.130 s 3 runs Benchmark 4: PLUGIN_COUNT=5 node --run build:rolldown Time (mean ± σ): 1.848 s ± 0.022 s [User: 4.398 s, System: 1.728 s] Range (min … max): 1.825 s … 1.869 s 3 runs Benchmark 5: PLUGIN_COUNT=10 node --run build:rolldown Time (mean ± σ): 2.792 s ± 0.065 s [User: 6.013 s, System: 2.198 s] Range (min … max): 2.722 s … 2.850 s 3 runs Summary 'PLUGIN_COUNT=0 node --run build:rolldown' ran 1.16 ± 0.08 times faster than 'PLUGIN_COUNT=1 node --run build:rolldown' 1.48 ± 0.04 times faster than 'PLUGIN_COUNT=2 node --run build:rolldown' 2.48 ± 0.05 times faster than 'PLUGIN_COUNT=5 node --run build:rolldown' 3.74 ± 0.10 times faster than 'PLUGIN_COUNT=10 node --run build:rolldown' ``` **Key Takeaway:** Build time scales linearly with plugin count - 10 plugins = **3.74x slower** (2.8s vs 745ms). ## The Solution: Plugin Hook Filters Instead of calling every plugin for every module, use `filter` to tell Rolldown which files each plugin cares about. Here's how: ```diff diff --git a/apps/10000/rolldown.config.mjs b/apps/10000/rolldown.config.mjs index 822af995..dee07e68 100644 --- a/apps/10000/rolldown.config.mjs +++ b/apps/10000/rolldown.config.mjs @@ -8,14 +8,21 @@ let transformCssPlugin = Array.from({ length: transformPluginCount }, (_, i) => let index = i + 1; return { name: `transform-css-${index}`, - transform(code, id) { - if (id.endsWith(`foo${index}.css`)) { - return { - code: `.index-${index} { + transform: { + filter: { + id: { + include: new RegExp(`foo${index}.css$`), + } + }, + handler(code, id) { + if (id.endsWith(`foo${index}.css`)) { + return { + code: `.index-${index} { color: red; }`, - map: null, - }; + map: null, + }; + } } } } ``` **What changed:** * Wrapped the `transform` function in an object with `handler` and `filter` properties * Added `filter.id.include` with a regex pattern matching only the files this plugin cares about * Rolldown now checks the filter in Rust *before* calling into JavaScript ### With Filter (Optimized) ```bash Benchmark 1: PLUGIN_COUNT=0 node --run build:rolldown Time (mean ± σ): 739.1 ms ± 6.8 ms [User: 2312.5 ms, System: 1153.0 ms] Range (min … max): 733.0 ms … 746.5 ms 3 runs Benchmark 2: PLUGIN_COUNT=1 node --run build:rolldown Time (mean ± σ): 760.6 ms ± 18.3 ms [User: 2422.1 ms, System: 1107.4 ms] Range (min … max): 739.7 ms … 773.6 ms 3 runs Benchmark 3: PLUGIN_COUNT=2 node --run build:rolldown Time (mean ± σ): 731.2 ms ± 11.1 ms [User: 2461.3 ms, System: 1141.4 ms] Range (min … max): 723.9 ms … 744.0 ms 3 runs Benchmark 4: PLUGIN_COUNT=5 node --run build:rolldown Time (mean ± σ): 741.5 ms ± 9.3 ms [User: 2621.6 ms, System: 1111.3 ms] Range (min … max): 734.0 ms … 751.9 ms 3 runs Benchmark 5: PLUGIN_COUNT=10 node --run build:rolldown Time (mean ± σ): 747.3 ms ± 2.1 ms [User: 2900.9 ms, System: 1120.0 ms] Range (min … max): 745.0 ms … 749.2 ms 3 runs Summary 'PLUGIN_COUNT=2 node --run build:rolldown' ran 1.01 ± 0.02 times faster than 'PLUGIN_COUNT=0 node --run build:rolldown' 1.01 ± 0.02 times faster than 'PLUGIN_COUNT=5 node --run build:rolldown' 1.02 ± 0.02 times faster than 'PLUGIN_COUNT=10 node --run build:rolldown' 1.04 ± 0.03 times faster than 'PLUGIN_COUNT=1 node --run build:rolldown' ``` **Key Takeaway:** With filters, all plugin counts perform nearly identically (~740ms). The overhead has been **eliminated**. ### Performance Comparison | Plugin Count | Without Filter | With Filter | Speedup | | ------------ | -------------- | ----------- | --------- | | 0 plugins | 745ms | 739ms | 1.0x | | 1 plugin | 863ms | 761ms | 1.13x | | 2 plugins | 1,106ms | 731ms | 1.51x | | 5 plugins | 1,848ms | 742ms | 2.49x | | 10 plugins | 2,792ms | 747ms | **3.74x** | **Bottom line:** When you have plugins that only care about specific files, use filters to maintain fast build times regardless of how many plugins you add. ## How It Works Under the Hood To understand why filters are so effective, you need to understand how Rolldown processes modules with JavaScript plugins. Rolldown uses parallel processing (like the [producer-consumer problem](https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem)) to build the module graph efficiently. Here's a simple dependency graph to illustrate: **Dependency Graph** ```dot [Dependency Graph] digraph { bgcolor="transparent"; rankdir=TB; node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; a [label="a.js", fillcolor="${#fff0e0|#4a2a0a}"]; b [label="b.js", fillcolor="${#dbeafe|#1e3a5f}"]; c [label="c.js", fillcolor="${#dbeafe|#1e3a5f}"]; d [label="d.js", fillcolor="${#dbeafe|#1e3a5f}"]; e [label="e.js", fillcolor="${#dbeafe|#1e3a5f}"]; f [label="f.js", fillcolor="${#dbeafe|#1e3a5f}"]; a -> b; a -> c; b -> d; b -> e; c -> f; } ``` ### Without JavaScript Plugins ![Bundling without JavaScript plugins](https://github.com/user-attachments/assets/ad071cf9-6a34-4a7d-a669-02efec342d45) Everything runs in parallel in Rust. Multiple CPU cores process modules simultaneously, maximizing throughput. > \[!NOTE] > These diagrams show the conceptual algorithm, not exact implementation details. Some time slices are exaggerated for clarity—`fetch_module` actually runs at macrosecond speeds. ### With JavaScript Plugins (No Filter) ![Bundling with JavaScript plugins](https://github.com/user-attachments/assets/7e95fb60-d345-4d23-a35e-c7d062fa2b70) Here's the bottleneck: **JavaScript plugins run in a single thread**. Even though Rolldown's Rust core is parallel, every module must: 1. Stop at the "diamond" (hook call phase) 2. Cross the FFI boundary from Rust → JavaScript 3. Wait for *all* plugins to run serially 4. Cross back from JavaScript → Rust This serialization point becomes a major bottleneck. Notice how the diamond section grows wider as more plugins are added, while CPU cores sit idle waiting for JavaScript. ### With Filters (Optimized) When you add filters, Rolldown evaluates them **in Rust** before crossing into JavaScript: ``` For each module: For each plugin: ✓ Check filter in Rust (macrosecond) ✗ Skip if no match → Only call JavaScript for matching plugins ``` This eliminates the majority of FFI overhead and JavaScript execution time. In the benchmark, most plugins don't match most files, so nearly all calls are skipped. The diamond shrinks back down, CPU utilization stays high, and build times remain fast. ## When to Use Filters **Use filters when:** * ✅ Your plugin only processes specific file types (e.g., `.css`, `.svg`, `.md`) * ✅ Your plugin targets specific directories (e.g., `src/**`, `node_modules/**`) * ✅ You have multiple plugins in your build * ✅ You care about build performance ## Quick Reference ```js // ❌ Without filter - called for every module export default { name: 'my-plugin', transform(code, id) { if (!id.endsWith('.css')) return; // ... transform CSS }, }; // ✅ With filter - only called for CSS files export default { name: 'my-plugin', transform: { filter: { id: { include: /\.css$/ }, }, handler(code, id) { // ... transform CSS }, }, }; ``` See the [plugin hook filter usage](/apis/plugin-api/hook-filters) for complete filter api and options. --- --- url: /in-depth/module-types.md --- # Module Types As a web bundler, JavaScript is not the only file type with built-in support in Rolldown. For example, Rolldown can handle TypeScript and JSX files directly, parsing and transforming them to JavaScript before bundling them. We refer to these file types with first-class support in Rolldown as **Module Types**. ## How module types affect users End users usually do not need to concern themselves with Module Types, since Rolldown automatically recognizes and handles known Module Types. By default, Rolldown determines the module type of a module based on its file extension. However, in some cases this may not be sufficient. For example, imagine a file containing JSON data, but its extension is `.data`. Rolldown can't recognize it as a JSON file because the extension is not `.json`. In this case, users need to explicitly tell Rolldown that files with the `.data` extension should be treated as the JSON module type. This can be done via the `moduleTypes` option in the config: ```js [rolldown.config.js] export default { moduleTypes: { '.data': 'json', }, }; ``` ## Module types and plugins Plugins can specify the module type of a specific file via the `load` hook and the `transform` hook: ```js const myPlugin = { load(id) { if (id.endsWith('.data')) { return { code: '...', moduleType: 'json', }; } }, }; ``` The main significance of module types is that it provides a central convention for supported types, making it easier to chain multiple plugins that need to operate on the same module type. For example, `@vitejs/plugin-vue` currently creates virtual css modules for the style blocks in `.vue` files and append `?lang=css` to the id of a virtual module, allowing these modules to be recognized as css by the vue plugin. However, this is only a convention of the vue plugin - other plugins may ignore the query string and thus not recognize the convention. With module types, `@vitejs/plugin-vue` can explicitly specify the module type of virtual css modules as `css`, and other plugins like the postcss plugin can process these css modules without being aware of the vue plugin. Another example: to add support for `.jsonc` files, a plugin could simply strip comments of `.jsonc` files in the `load` hook and return `moduleType: 'json'`. Rolldown will handle the rest. --- --- url: /in-depth/external-modules.md --- # External Modules When a module is marked as external, Rolldown will not bundle it. Instead, the `import` or `require` statement is preserved in the output, and the module is expected to be available at runtime. ```js // input import lodash from 'lodash'; console.log(lodash); // output (lodash is external) import lodash from 'lodash'; console.log(lodash); ``` This page explains how externals work end-to-end: how a module becomes external, how its import path is determined in the output, and how the relevant options and plugin hooks interact. ## How a Module Becomes External There are three ways a module can be marked as external: 1. **The [`external`](/reference/InputOptions.external) option** — a config-level pattern (string, regex, array, or function) that tests each import specifier. See the [option reference](/reference/InputOptions.external) for pattern syntax, examples, and caveats. 2. **A plugin's `resolveId` hook** — a plugin can return `{ id, external: true }` (or `"relative"` / `"absolute"`) to explicitly mark a module as external. A plugin can also `return false` to mark the raw specifier as external with the same normalization as the `external` option. 3. **Unresolved modules** — if no plugin or the internal resolver can find a module and the `external` option matches the specifier, Rolldown treats it as external rather than throwing an error. ## The Full Resolution Flow Here is the step-by-step process Rolldown follows when it encounters an import: ### 1. First `external` check The raw import specifier (e.g. `'./utils'`, `'lodash'`) is tested against the [`external`](/reference/InputOptions.external) option with `isResolved: false`. If it matches, the module is marked as external immediately — **plugins and the internal resolver are skipped entirely**. ### 2. Plugin `resolveId` If the first check did not match, plugins get a chance to resolve the import: | Plugin return value | Effect | | ------------------------------------- | --------------------------------------------------------------------------------- | | `return false` | External. Uses the raw specifier as the module ID (same normalization as step 1). | | `return { id, external: true }` | External. Uses `id` as the module ID. | | `return { id, external: "relative" }` | External. Path is **always** relativized (overrides config). | | `return { id, external: "absolute" }` | External. Path is **always** kept verbatim (overrides config). | | `return { id }` (no `external`) | Resolved, continue to step 3 with the resolved ID. | | `return null` | No plugin handled it, fall through to step 3. | ### 3. Internal resolver Rolldown's built-in resolver tries to find the module on disk. ### 4. Second `external` check The resolved ID (e.g. `'/project/node_modules/vue/dist/vue.runtime.esm-bundler.js'`) is tested against the [`external`](/reference/InputOptions.external) option with `isResolved: true`. If it matches, the specifier is marked as external. ### 5. Output path determination Regardless of which step marked the module as external (first check, plugin, or second check), [`makeAbsoluteExternalsRelative`](/reference/InputOptions.makeAbsoluteExternalsRelative) applies uniformly to determine the import path in the output: * **Bare specifiers** (e.g. `'lodash'`, `'node:fs'`) — appear as-is when matched on the first check. If matched on the second check (resolved path), the full resolved path appears instead (see the [caveat about `/node_modules/`](/reference/InputOptions.external#avoid-node-modules-for-npm-packages)). * **Relative and absolute specifiers** — two things happen: 1. **Resolve-time normalization** — for the first check and `return false`, when `makeAbsoluteExternalsRelative` is enabled (which it is by default), relative specifiers (the **original import specifier**) are normalized to absolute paths by resolving against the importer's directory. This ensures that `'./utils'` imported from different directories correctly maps to different external modules. For the second check and `return { id, external: true }`, the **resolved module ID** is already absolute. 2. **Render-time output** — absolute resolved module IDs may be converted back to relative paths from the output chunk's location (e.g. `'/project/src/utils.js'` → `'./utils.js'`). Whether this happens depends on the `makeAbsoluteExternalsRelative` value and whether the original import specifier was relative. Plugin overrides (`external: "relative"` / `"absolute"`) bypass this logic entirely. See the [`makeAbsoluteExternalsRelative` reference](/reference/InputOptions.makeAbsoluteExternalsRelative) for how each value controls this behavior, with examples. ## Special Cases ### Data URLs Specifiers with a valid `data:` URL (e.g. `data:text/javascript,export default 42`) with a supported file format are handled by Rolldown's internal dataurl plugin which **bundles the inline content**. They are not automatically treated as external. However, other `data:` URLs are treated as external automatically unless it's handled by a custom plugin. ### HTTP URLs Specifiers starting with `http://`, `https://`, or `//` are **automatically treated as external** regardless of the `external` option, unless it's handled by a custom plugin. These IDs are emitted as-is and not affected by `makeAbsoluteExternalsRelative`. ```js import lib from 'https://cdn.example.com/lib.js'; // Always external, emitted as-is ``` ## Unused Imports Are Removed If nothing uses an import from an external module, Rolldown removes it. ```js // input import { used, unused } from 'ext-pkg'; console.log(used); // output import { used } from 'ext-pkg'; console.log(used); ``` Note that even if every import is removed, the statement itself usually stays. External modules are assumed to have side effects, so it becomes a bare `import 'ext-pkg';`. The statement goes away completely only when the external module is also marked side-effect-free. ::: warning Difference from bundled modules If a bundled module does not actually export `unused`, Rolldown emits a `MISSING_EXPORT` error at build time, whether or not the import is used. For external modules, Rolldown does not know what exports exist, so it cannot check. If `unused` does not exist, importing it would throw at runtime, and removing the import removes that error along with it. Causing a semantic change without any message is normally a bad idea, but Rolldown makes an exception here. An unused import usually comes from dead code elimination, either Rolldown's own or a plugin's, rather than being written by hand, so the error is rarely the one you intended to see. ::: --- --- url: /in-depth/directives.md --- # Directive JavaScript has a feature called directive, which is used to annotate a part of the code. Rolldown may not be able to preserve the semantics related to directives, here are the strategies when handling directives. ## `"use strict"` The `"use strict"` directive is a directive that tells the JavaScript engine to enforce strict mode. Because keeping the top-level `"use strict"` directive semantics is complicated and requires a bigger output size, Rolldown may not keep them. Since ES modules are always in strict mode, Rolldown does not output any `"use strict"` directive for `output.format: 'es'`. As a side note, this means code that is not in strict mode are forced to be in strict mode for ES module format output. You can control the `"use strict"` directive emission with the [`output.strict`](/reference/OutputOptions.strict) option: * `true` - Always emit `"use strict"` at the top of the output (not applicable for ESM format since ESM is always strict). * `false` - Never emit `"use strict"` in the output. * `'auto'` (default) - Respect the `"use strict"` directive from the source code. ```ts import { defineConfig } from 'rolldown'; export default defineConfig({ output: { format: 'cjs', strict: true, }, }); ``` When `output.format` is not `'es'` and `output.strict` is `'auto'`, Rolldown will output the `"use strict"` directive for any of the following cases: * The directive is not in the top-level scope and not inside strict mode scope ([REPL](https://repl.rolldown.rs/#eNptjk0KAyEMha8SsrGF4gGE3mQ24mhxsMmgsR0YvHu1pT+LbpJ8L+G97BjQ7Bhp9pteypgJzZdP6Dr6beUsRQdmOEOo5CQygT0cYZ8IQNXioUiOTtTg7KVmAtXvO7eJuo9HI7n6dsLMKc18J+2YQrxo+cT+2fw+ALMPtibpoXnEcJW1inkjQOB8tV1QbinqJbbRngVbz751s2TFF8H2AIc5VRY=)) * The directive is in the top-level scope and the module is an entry module ([REPL](https://repl.rolldown.rs/#eNptUEtuhDAMvYqVDVCN6Kobuuw12FBwpqmCQx2nTYVy9zGD5qPRbJK8Z7+PshprutU4mjC333F7k+lu+GBGhVWKCFHYjVK99+TmJbDA1/+C/JE+ESyHGar2Nf6kgVF121ZPY6AYPLY+HOvrcv3WNDpVZzSdcMJyMFfdJf9GPC3QE+ZzhQntkLyATTSKCwS7sM4NrD0BMEpiggwvkFVXNFfjOHg/hT9qtaB1x7vcJ5O9wEPe2vNmH5IsSboLBLCB50GJatQ/2MmyXefDFM3+VTM/CEYx5QSMo4I7)) * The directive is in the top-level scope and `output.preserveModules` is enabled ([REPL](https://repl.rolldown.rs/#eNptkE1uhDAMha9iZQNUiK66ocuuewM2FJwpVYip40ypUO5eZxAz1Wg2Sfzz/L14M9a0m5n8iGvzFfLbm/YW12bQsIgBIQhPgxSvnZ/mhVjg83dBfosfCJZphqJ5Dt+xZ1Rd7ur8QD6Qw8bRqbw2ly9VpVWdjKYVjphqc9Ud/FvioQFcLwZGtH10Ajb6QSbysMvKtYKt8wCMEtnDCk+wqiopVWFMzo304xu1Z6fTP+qDyo6/420d5/EUZYnSHiGAJZ57TRSDbqA+sgtjQD7jO43RYWghf3ovpnxdDpPU2VlRrhcMYtIfQpqMFA==)) ## Other directives The ECMAScript specification allows implementations to define additional directives. Since those additional directives are not part of the specification, Rolldown does not know the semantics of them. Rolldown assumes that they follow the similar semantics as `"use strict"`. But for the same reason as above, Rolldown may not preserve the top-level directives. Rolldown will output the directive for any of the following cases: * The directive is not in the top-level scope ([REPL](https://repl.rolldown.rs/#eNptjt0KwyAMhV8l5MYNig8g7E16I1ZHi02Kxq1QfPfpxn4udpPkOwnn5MCA5sCZJr/rJfeZ0Hx5QNfQ7xsnyTowwwVCISczE9jTGY6RAFTJHlzJwqvqnLyURKDafeM6UvPxaCQVXwdMHOPEd9KOKcxXLZ/YP5vfB2DywZYoLTT1GC6yFTFvBAicVtsE5ZasXmLt7VmwtuxbM4tWfBasD4hqVRg=)) * The directive is in the top-level scope and the module is a entry module ([REPL](https://repl.rolldown.rs/#eNptUM1OwzAMfhUrl7ZolBOXcuQ1eimtM4pSuzgOdKr67riLtiHYJYn9/Sqr865Z3UgDLvVH3N/kmtt8cL2NRYoIfYrK0yOSyql4aWmcZhaF99OM8preELzwBEX9FD9TJ2jqndVSzxQ5YB34WF7J5XNVGWr+6BqVhNvBXXWXFrfF/xoZywm4nJsM6LsUFHyiXkcmyJxyqWBtCUBQkxAs8ACL6TaLt1ThEAb+ptp6+vH4K/4Oknv8yVtb2e056Zy0uYwAnmXqbFH09hV5ue3X+XCbZX+ZWegUo7rtB/Gqh1w=)) * The directive is in the top-level scope and `output.preserveModules` is enabled ([REPL](https://repl.rolldown.rs/#eNptkM9ShDAMxl8l0wvgIJ684NGzb8AFIV1xSoNps7LD8O6mMOw6upe2+fPl+zWLsaZezOB7nKvPkN7e1Le4NJ2GmQSETkKk8RF95Ev20vhhnIgjfFwm5Fd5R7BMI2TVU/iSllHVqavxHflADitHp/zanD8XhVZ1Ppo6suBamqvuoLgl/mOM1IvD5IDzxtGjbcVFsOK7OJCHXZ3PBSyNB2CMwh5meIBZVauaqyeTcz19+0op7XD6ZX6nslP88VsaTuNJ4iSxPkIASzy2msg6XUR5ZCfGgHzGtw0/1JD+vhfXdG2HWZXsrFaujRiiWX8ALR2RKg==)) If you want to append custom directive to all files, you can use the `output.banner` option: ```ts import { defineConfig } from 'rolldown'; export default defineConfig({ output: { banner: "'use client';", }, }); ``` --- --- url: /in-depth/automatic-code-splitting.md --- # Automatic Code Splitting Automatic code splitting is the process of creating chunks from modules. This chapter describes its behavior and the principles behind it. Automatic code splitting is not controllable. It runs following certain rules. Thus, we will also refer to it as automatic code splitting against manual code splitting done by [manual code splitting](./manual-code-splitting.md). Two types of chunks are generated by automatic code splitting. ## **Entry chunks** **Entry chunks** are generated by combining modules connected statically into a chunk. "statically" means static `import ... from '...'` or `require(...)`. There are two types of **entry chunks**. The first one is **initial chunks**. **Initial chunks** are generated due to users' configuration. For example, `input: ['./a.js', './b.js']` defines two **initial chunks**. The second one is **dynamic chunks**. **Dynamic chunks** are generated due to dynamic imports. Dynamic imports are used to load code on demand, so we don't put imported code together with the importers. For the following code, two chunks will be generated: ```js // entry.js (included in `input` option) import foo from './foo.js'; import('./dyn-entry.js'); // dyn-entry.js require('./bar.js'); // foo.js export default 'foo'; // bar.js module.exports = 'bar'; ``` In this case, there are two groups of statically connected modules. ```dot digraph { bgcolor="transparent"; rankdir=LR; node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; subgraph cluster_group1 { label="Group 1 (initial chunk)"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; entry [label="entry.js", fillcolor="${#fff0e0|#4a2a0a}"]; foo [label="foo.js", fillcolor="${#fff0e0|#4a2a0a}"]; entry -> foo [label="static import"]; } subgraph cluster_group2 { label="Group 2 (dynamic chunk)"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#0366d6|#58a6ff}"; dyn [label="dyn-entry.js", fillcolor="${#dbeafe|#1e3a5f}"]; bar [label="bar.js", fillcolor="${#dbeafe|#1e3a5f}"]; dyn -> bar [label="require()"]; } entry -> dyn [label="import()", style=dashed]; } ``` Since there are two groups, in the end, automatic code splitting will generate two chunks. ## **Common chunks** **Common chunks** are generated when a module gets statically imported by at least two different entries. Those modules are put into a separate chunk. The purpose of this behavior is: * Ensure every JavaScript module is singleton in the final bundle output. * When a entry gets executed, only imported modules should get executed. It is important to note that whether a module could be put into the same common chunk is determined by if it is imported by the same entries. For the following code, six chunks will be generated: ```js // entry-a.js (included in `input` option) import 'shared-by-ab.js'; import 'shared-by-abc.js'; console.log(globalThis.value); // entry-b.js (included in `input` option) import 'shared-by-ab.js'; import 'shared-by-bc.js'; import 'shared-by-abc.js'; console.log(globalThis.value); // entry-c.js (included in `input` option) import 'shared-by-bc.js'; import 'shared-by-abc.js'; console.log(globalThis.value); // shared-by-ab.js globalThis.value = globalThis.value || []; globalThis.value.push('ab'); // shared-by-bc.js globalThis.value = globalThis.value || []; globalThis.value.push('bc'); // shared-by-abc.js globalThis.value = globalThis.value || []; globalThis.value.push('abc'); ``` The chunks will be generated as follows: ::: code-group ```js [entry-a.js] import './common-ab.js'; import './common-abc.js'; ``` ```js [entry-b.js] import './common-ab.js'; import './common-bc.js'; import './common-abc.js'; ``` ```js [entry-c.js] import './common-bc.js'; import './common-abc.js'; ``` ```js [common-ab.js] globalThis.value = globalThis.value || []; globalThis.value.push('ab'); ``` ```js [common-bc.js] globalThis.value = globalThis.value || []; globalThis.value.push('bc'); ``` ```js [common-abc.js] globalThis.value = globalThis.value || []; globalThis.value.push('abc'); ``` ::: The following diagram shows how entries share dependencies and how modules are grouped into chunks: ```dot digraph { bgcolor="transparent"; rankdir=TB; node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; newrank=true; // Entry nodes entry_a [label="entry-a.js", fillcolor="${#fff0e0|#4a2a0a}"]; entry_b [label="entry-b.js", fillcolor="${#fff0e0|#4a2a0a}"]; entry_c [label="entry-c.js", fillcolor="${#fff0e0|#4a2a0a}"]; // Shared module nodes shared_ab [label="shared-by-ab.js", fillcolor="${#dbeafe|#1e3a5f}"]; shared_bc [label="shared-by-bc.js", fillcolor="${#dbeafe|#1e3a5f}"]; shared_abc [label="shared-by-abc.js", fillcolor="${#e0e7ff|#2e1065}"]; // Edges entry_a -> shared_ab; entry_a -> shared_abc; entry_b -> shared_ab; entry_b -> shared_bc; entry_b -> shared_abc; entry_c -> shared_bc; entry_c -> shared_abc; // Chunk grouping subgraph cluster_chunk_a { label="entry-a.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; entry_a; } subgraph cluster_chunk_b { label="entry-b.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; entry_b; } subgraph cluster_chunk_c { label="entry-c.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; entry_c; } subgraph cluster_common_ab { label="common-ab.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#0366d6|#58a6ff}"; shared_ab; } subgraph cluster_common_bc { label="common-bc.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#0366d6|#58a6ff}"; shared_bc; } subgraph cluster_common_abc { label="common-abc.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#0366d6|#58a6ff}"; shared_abc; } } ``` `entry-*.js` chunks are generated by the reason discussed above. `common-*.js` chunks are the **common chunks**. These are created because: * `common-ab.js`: `shared-by-ab.js` is imported by both `entry-a.js` and `entry-b.js`. * `common-bc.js`: `shared-by-bc.js` is imported by both `entry-b.js` and `entry-c.js`. * `common-abc.js`: `shared-by-abc.js` is imported by all 3 entries. You may ask why automatic code splitting doesn't place `shared-by-*.js` files into a single common chunk. The reason is that doing so would violate the original code's intention. For the example above, if a single common chunk were created, it will be like: ```js [common-all.js] globalThis.value = globalThis.value || []; globalThis.value.push('ab'); globalThis.value = globalThis.value || []; globalThis.value.push('bc'); globalThis.value = globalThis.value || []; globalThis.value.push('abc'); ``` For this output, executing each entry will output `['ab', 'bc', 'abc']`. However, the original code outputs a different result for each entry: * `entry-a.js`: `['ab', 'abc']` * `entry-b.js`: `['ab', 'bc', 'abc']` * `entry-c.js`: `['bc', 'abc']` ## Module Placing Order Rolldown tries to place your modules in the order declared in the original code. For the following code: ```js // entry.js import { foo } from './foo.js'; console.log(foo); // foo.js export var foo = 'foo'; ``` Rolldown will try to calculate the order by emulating the execution, starting from entries. In this case, the execution order is `[foo.js, entry.js]`. So the bundle output will be like: ```js [output.js] // foo.js var foo = 'foo'; // entry.js console.log(foo); ``` ### Respecting Execution Order doesn't take precedence However, Rolldown sometimes places modules without respecting their original order. This is because ensuring that modules are singletons takes precedence over placing them in the declared order. For the following code: ```js // entry.js (included in `input` option) import './setup.js'; import './execution.js'; import('./dyn-entry.js'); // setup.js globalThis.value = 'hello, world'; // execution.js console.log(globalThis.value); // dyn-entry.js import './execution.js'; ``` The bundle output will be: ::: code-group ```js [entry.js] import './common-execution.js'; // setup.js globalThis.value = 'hello, world'; ``` ```js [dyn-entry.js] import './common-execution.js'; ``` ```js [common-execution.js] console.log(globalThis.value); ``` ::: `common-execution.js` is a common chunk. It is generated because `execution.js` is imported by both `entry.js` and `dyn-entry.js`. ```dot digraph { bgcolor="transparent"; rankdir=TB; node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"]; compound=true; subgraph cluster_entry { label="entry.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; entry [label="entry.js", fillcolor="${#fff0e0|#4a2a0a}"]; setup [label="setup.js", fillcolor="${#fff0e0|#4a2a0a}"]; } subgraph cluster_dyn { label="dyn-entry.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#d44803|#ff712a}"; dyn [label="dyn-entry.js", fillcolor="${#fff0e0|#4a2a0a}"]; } subgraph cluster_common { label="common-execution.js chunk"; labeljust="l"; fontname="Arial"; fontsize=11; fontcolor="${#3c3c43|#dfdfd6}"; style="dashed,rounded"; color="${#0366d6|#58a6ff}"; execution [label="execution.js", fillcolor="${#dbeafe|#1e3a5f}"]; } entry -> setup [label="import"]; entry -> execution [label="import"]; entry -> dyn [label="import()", style=dashed]; dyn -> execution [label="import"]; } ``` This example shows the problem, before bundling, the code outputs `hello, world`, but after bundling, it outputs `undefined`. Currently, there's no easy way to solve this problem, as well for other bundlers that output ESM. ::: info Related issues for other bundlers * [evanw/esbuild#399](https://github.com/evanw/esbuild/issues/399) * [rollup/rollup#4539](https://github.com/rollup/rollup/issues/4539) ::: There are some discussions on how to solve this problem. One way is to move modules into additional common chunks whenever their original order would be violated, but that can fragment the output. Rolldown instead offers [`strictExecutionOrder`](/reference/OutputOptions.strictExecutionOrder), which wraps ESM module bodies so they can run in source order while keeping ESM output. When a wrapped dynamic entry ends up sharing its implementation chunk with other code, the rewritten `import()` triggers the implementation itself. Strict mode emits a small entry facade only where a real file is still needed — for example when another chunk statically loads the entry's chunk, or for chunks a plugin emits — so the output chunk shape can still change. By default strict mode wraps every eligible module; the experimental `onDemandWrapping` mode derives a conservative subset from predicted chunk execution hazards. --- --- url: /in-depth/manual-code-splitting.md --- # Manual Code Splitting Manual code splitting is a powerful feature that allows you to do manual code splitting to complement the [automatic code splitting](./automatic-code-splitting.md). This is useful when you want to optimize the loading performance of your application by splitting it into smaller, more manageable pieces. Before reading this guide, you should first understand the [automatic code splitting](./automatic-code-splitting.md) feature of Rolldown. This guide will explain how manual code splitting works and how to use it effectively. Before we dive into the details, let's clarify some things first. * Automatic code splitting and manual code splitting are not contradictory. Using manual code splitting does not mean disabling automatic code splitting. A module will be either captured by automatic code splitting or manual code splitting depending on your configuration, but not both. If a module is not captured by manual code splitting, it will still be put into a chunk which is created by automatic code splitting while respecting the rules we explained in the [automatic code splitting](./automatic-code-splitting.md) guide. ## Why use manual code splitting? The automatic code splitting doesn't take loading performance or cache invalidation into account. It simply groups modules based on their static imports. This can lead to suboptimal chunking, where large chunks are created that may not be performant for loading or cause cache invalidation for every deployment. ## How to use manual code splitting? Let's take a look at the following example: ```jsx // index.jsx import * as ReactDom from 'react-dom'; import App from './App.jsx'; ReactDom.createRoot(document.getElementById('root')).render(); // App.jsx import * as React from 'react'; import { Button } from 'ui-lib'; export default function App() { return