Skip to content

feat(babel): add parallel option to transform in worker threads - #139

Open
NullVoxPopuli-ai-agent wants to merge 4 commits into
rolldown:mainfrom
NullVoxPopuli-ai-agent:babel-parallel
Open

NullVoxPopuli-ai-agent wants to merge 4 commits into
rolldown:mainfrom
NullVoxPopuli-ai-agent:babel-parallel

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Sep 25, 2026 •

Copy link
Copy Markdown

Note

I am an AI bot: Claude Code, with the Claude Opus 5.5 model. @NullVoxPopuli asked me to open this PR. @NullVoxPopuli, please review.

Adds a parallel option to @rolldown/plugin-babel. With it, the plugin runs Babel in worker threads. This is a port of the same option from @rollup/plugin-babel (rollup/plugins#1956).

babel({
  parallel: true, // or a worker count
  plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]],
})

Behavior

  • true starts one worker per CPU core, with a maximum of 4. A number sets the worker count.
  • The pool starts on the first transform. It stops in closeBundle (not in watch mode) or closeWatcher.
  • Babel options must be structured-cloneable. If a plugin or preset is a function, the first transform fails with an error that names the option, for example overrides[0].presets[0].
  • The rolldown part of a Rolldown Babel preset stays in the main thread. So its filters and hooks can still be functions.
  • Babel errors from a worker keep loc, so Rolldown still reports foo.js:1:13. The structured clone of an error drops loc, so the worker sends the error as a plain object.

The other recent performance change in @rollup/plugin-babel (rollup/plugins#1954, hook filters) already exists in this plugin.

Benchmark

packages/babel/benchmark compares parallel: false and parallel: true on 100 generated modules with decorators. Run pnpm bench --reporter=verbose in that directory. On an 8-core machine, with 10 samples:

parallel mean build time
false 3.55 s
true (4 workers) 1.64 s

Pool packages

Build time with each pool package, 4 workers, 12 rounds with a rotated order:

pool 30 modules 300 modules
none (parallel: false) 1.29 s 9.81 s
workerpool 0.89 s 3.61 s
tinypool 0.90 s 3.57 s
artichokie 0.90 s 3.65 s

The differences between the pools are inside the noise (standard deviation 0.01 to 0.08 s), so the plugin uses artichokie, the smallest package.

Notes for review

  • New runtime dependency: artichokie@^0.4.5.
  • artichokie stop() calls unref() on the workers and does not terminate them. A process that runs many builds keeps the idle workers until it exits.
  • Errors from loadOptionsAsync now also get the [BabelError] prefix, because they are now inside the same try block as the transform.
  • The test build() helper now calls bundle.close(), so that worker pools stop.
  • Not tested: the Vite dev server. The Rolldown builds and the existing Vite build tests pass.

🤖 Generated with Claude Code

@socket-security

socket-security Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedartichokie@​0.4.5871008988100

View full report

@NullVoxPopuli NullVoxPopuli left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems good to me

@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent marked this pull request as ready for review September 25, 2026 17:29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if there is a better way to do this, but the purpose of doing this much work to see if a config is serializable is to provide a better error to consumers when part of their config is not serializable

@hyfdev
hyfdev requested a review from sapphi-red September 29, 2026 05:57
@sapphi-red

Copy link
Copy Markdown
Member

Would you add packages/babel/benchmark that compares parallel: false and parallel: true?

Also would you compare the performance with the following packages?

  • workerpool
  • tinypool
  • artichokie

If the performance is same, I'd prefer the one on the bottom in the list as it has smaller package size.

const preset = presets[i]
// The `rolldown` part of a preset (filters and hooks) is only used in the main thread.
const babelPreset = typeof preset === 'object' && 'rolldown' in preset ? preset.preset : preset
if (!isCloneable(babelPreset)) return `${path}[${i}]`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use structuredClone? It has the overhead of actually cloning the vaules, but I think that can be solved by running the check after sending the message was errored.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d7fb6dc. isCloneable is now a structuredClone call in a try block.

The check runs only after a worker message fails with a DataCloneError. The error then names the option, for example "overrides[0].presets[0]" cannot be sent to a worker thread.

(AI reply: Claude Code for @NullVoxPopuli)

NullVoxPopuli and others added 4 commits October 10, 2026 10:50
Port of the `parallel` option from @rollup/plugin-babel (rollup/plugins#1956).

Files go to a `workerpool` thread pool. Default worker count is the CPU
count, with a maximum of 4. The pool starts on the first transform and
stops in `closeBundle` (not in watch mode) or `closeWatcher`.

Babel options must be structured-cloneable, so the plugin rejects
function or object plugins and presets at startup. Rolldown preset
filters and hooks stay in the main thread, so they can be functions.

The other upstream performance change (rollup/plugins#1954, hook
filters) already exists in this plugin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Clone` for the option check

Review follow-up.

`workerpool`, `tinypool`, and `artichokie` give the same build time
in the benchmark, so the plugin uses `artichokie`, the smallest package.

The check for options that cannot go to a worker now uses `structuredClone`.
It runs only after a worker message fails with a `DataCloneError`,
so builds with valid options do not pay for the clone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compares `parallel: false` and `parallel: true` on 100 generated modules
with decorators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Author

Both are done.

packages/babel/benchmark (3eff276) compares parallel: false and parallel: true on 100 generated modules with decorators. Result on 8 cores, 10 samples: 3.55 s against 1.64 s.

Pool comparison, 4 workers, mean build time of 12 rounds with a rotated order:

pool 30 modules 300 modules
none (parallel: false) 1.29 s 9.81 s
workerpool 0.89 s 3.61 s
tinypool 0.90 s 3.57 s
artichokie 0.90 s 3.65 s

The pools are equal inside the noise (standard deviation 0.01 to 0.08 s), so the plugin now uses artichokie (d7fb6dc).

Two notes on artichokie:

  • stop() calls unref() on the workers and does not terminate them. A process that runs many builds keeps the idle workers until it exits.
  • vitest bench shows the result table only with --reporter=verbose. The other benchmarks in this repo have the same behavior.

(AI reply: Claude Code for @NullVoxPopuli)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants