Skip to content

feat(build): parallel build across worker threads - #1741

Merged
cossssmin merged 5 commits into
masterfrom
feat-parallel-build
Jun 6, 2026
Merged

feat(build): parallel build across worker threads#1741
cossssmin merged 5 commits into
masterfrom
feat-parallel-build

Conversation

@cossssmin

@cossssmin cossssmin commented Jun 5, 2026

Copy link
Copy Markdown
Member

Parallel build across worker threads

Large projects (hundreds–thousands of templates) build slowly because templates render sequentially through a single Vite SSR renderer. This adds an opt-in/auto parallel build that shards templates across worker threads.

Config

New top-level parallel key:

parallel?: boolean | { workers?: number; threshold?: number }
  • omitted (default) - auto: parallel when there are more than 50 templates, sequential below
  • true - always parallel, min(CPU − 1, 8) workers
  • false - always sequential
  • { workers, threshold } - workers thread count (default min(CPU − 1, 8)), threshold template count to trigger parallel (default 50, 0 = always)

How it works

  • Main thread runs beforeCreate once, shards the template list, and runs afterBuild once with the full file list.
  • Each worker reloads the config file (to recover function hooks - functions can't cross the thread boundary), then renders + transforms + writes its batch via the same buildTemplate() the sequential path uses, firing per-template events (beforeRender/afterRender/afterTransform) in-thread.
  • No event handler closure ever crosses a thread boundary, so output is identical to a sequential build.
  • Uses tinypool; the worker entry is a tiny jiti shim that loads the compiled .js in dist and the .ts source in dev/tests.

Benchmarks

Setup: 24-core machine, default two-factor.vue Maizzle 6 template.

The worker count matters more than the threshold, over-provisioning hurts:

N=256 4w 8w 12w 16w 23w
time 10.2s 9.3s 9.2s 10.7s 13.5s

So the default caps at 8 workers. With that cap the crossover is ~25 templates:

templates sequential parallel (8w) speedup
64 6.4s 4.2s 1.5×
128 12.3s 6.4s 1.9×
1024 93s 25s 3.7×

Behavior & limits

  • Only applies to file-based configs (CLI / config path / default cwd config) — workers reload the config file; a programmatic inline config object can't provide that, so it builds sequentially.
  • SFC-registered afterBuild handlers can't run in a worker (they'd need to fire once on the main thread with the aggregate file list) - they're counted and a warning is logged; use the config afterBuild hook instead.
  • Small builds stay sequential (no worker startup overhead).

Tests

resolveParallel gating (threshold, worker cap, explicit override, false, inline-object fallback) + two end-to-end parallel builds
verifying config events fire in workers and output matches sequential.

Summary by CodeRabbit

  • New Features

    • Parallel template building via a new parallel option with worker/threshold controls.
    • Builds now preserve template subdirectory structure under the output directory.
    • Per-template rendering supports source overrides and isolated per-template config handling.
  • Tests

    • Expanded test coverage for parallel builds, per-template hooks, and related behaviors.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c3d0e2c-8c2f-4f44-863a-b867a70631e0

📥 Commits

Reviewing files that changed from the base of the PR and between 69f03ca and d353b10.

📒 Files selected for processing (3)
  • src/render/buildTemplate.ts
  • src/render/parallel/buildWorker.ts
  • src/tests/build.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/render/parallel/buildWorker.ts
  • src/render/buildTemplate.ts
  • src/tests/build.test.ts

📝 Walkthrough

Walkthrough

Adds optional CPU-aware parallel template building via tinypool, extracts per-template rendering into buildTemplate, adds renderer source override support for beforeRender hooks, implements worker orchestration and config, and expands tests and packaging to support the new flow.

Changes

Parallel Template Build Feature

Layer / File(s) Summary
Configuration and Dependencies
src/types/config.ts, package.json
Add parallel config option to MaizzleConfig supporting boolean and object forms with workers and threshold. Add tinypool v^2.1.0 dependency.
Renderer Source Override Support
src/render/createRenderer.ts
Extend Renderer.render signature with optional opts.source. Add per-render sourceOverrides map, Vite plugin loader for override injection, and cleanup/invalidations to avoid cross-render leaks.
Template Rendering Pipeline Extraction
src/render/buildTemplate.ts, src/tests/build.test.ts
Extract single-template rendering into buildTemplate with BuildTemplateContext/BuildTemplateResult. Implement event firing, transformer/plaintext handling, computeContentBase, and resolveOutputPath. Add tests for beforeRender source override and per-template config cloning.
Build Function Refactoring and Parallel Decision Logic
src/build.ts, src/tests/build.test.ts
Replace sequential template loop with branching: parallel path uses runParallelBuild() with tinypool; sequential path creates single renderer and calls buildTemplate() per template. Implement exported resolveParallel() evaluating config, CPU count, template thresholds, and file-based config. Add tests covering resolveParallel behavior and parallel integration.
Worker-Side Build Logic and Entry Point
src/render/parallel/buildWorker.ts, src/render/parallel/worker.mjs, tsdown.config.ts
Define BuildWorkerData/BuildWorkerResult. Implement worker run() that reloads and merges config (arrays replaced), creates a renderer, builds assigned templates with buildTemplate, aggregates files and SFC afterBuild counts, and ensures cleanup. Provide worker.mjs entry that dynamically loads compiled or TS implementation. Update packaging to copy worker.mjs.
Tests and Test Imports
src/tests/build.test.ts
Add availableParallelism import and multiple tests for beforeRender behavior, per-template config isolation, parallel build scenarios, and resolveParallel unit tests.

Sequence Diagrams

sequenceDiagram
  participant build
  participant resolveParallel
  participant runParallelBuild
  participant tinypool
  participant worker_mjs
  participant buildWorker
  participant buildTemplate
  participant FileSystem

  build->>resolveParallel: evaluate config, CPU, template count
  resolveParallel-->>build: { enabled, workers }

  alt parallel enabled
    build->>runParallelBuild: submit sharded template batches
    runParallelBuild->>tinypool: create pool with worker.mjs
    runParallelBuild->>tinypool: run each shard
    tinypool->>worker_mjs: invoke with shard data
    worker_mjs->>buildWorker: load impl and call run(data)
    buildWorker->>buildTemplate: build each template in shard
    buildTemplate->>FileSystem: write HTML + plaintext outputs
    buildTemplate-->>buildWorker: return files[], sfcAfterBuildCount
    buildWorker-->>runParallelBuild: aggregated results
    runParallelBuild-->>build: aggregated files + total sfcAfterBuildCount
  else sequential
    build->>buildTemplate: create renderer, build per-template
    buildTemplate->>FileSystem: write outputs
    buildTemplate-->>build: files[], sfcCount
  end

  build->>FileSystem: copy static files
  build->>build: fire afterBuild event
Loading

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(build): parallel build across worker threads' directly and clearly summarizes the main change: adding parallel template building using worker threads, which is the primary focus of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-parallel-build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/build.ts`:
- Around line 197-210: Detect and prevent duplicate output targets before
spawning parallel workers: before calling Promise.all([...pool.run(...)])
flatten the batches and, using the same output-resolution logic used by
buildTemplate (extract that logic into or call a new helper like
computeOutputTarget(templatePath, configPath, configData, outputPath,
outputExtension) if necessary), compute each template's final output file path
deterministically, check for duplicates and fail-fast with a clear error if any
collisions exist; alternatively you can change pool.run workers to write to
per-worker temp files and then, on the main thread after results return,
atomically move/merge those temp files into the real output directory in a
deterministic order while checking for duplicates — reference batches, pool.run,
buildTemplate (or the new computeOutputTarget helper), results and files when
implementing the change.
- Around line 164-165: The current return forces sequential runs for
single-template builds because enabled is computed as workers >= 2 && count >=
2; update the condition to respect explicit parallel/threshold settings: keep
const workers = Math.min(maxWorkers, count) but return { enabled: workers >= 2
&& (count >= 2 || parallel === true || threshold === 0), workers } (or use the
actual option names in scope if different) so that explicit parallel:true or
threshold:0 will enable parallel even when count === 1; adjust the function
signature or captured variables to reference the surrounding parallel and
threshold config values used elsewhere in this module.

In `@src/render/buildTemplate.ts`:
- Around line 122-124: The code uses basename(templatePath) when setting
ptOutputPath in the sfcPlaintext branch, causing different templates with the
same filename (e.g., emails/a/welcome.vue and emails/b/welcome.vue) to collide;
change it to preserve the template's relative directory structure by computing
the path relative to your templates root (or a common templates base) and
include that directory when joining the destination: compute relDir =
relative(templatesRoot, dirname(templatePath)) and set ptOutputPath =
join(resolve(sfcPlaintext.destination), relDir, `${name}.${ptExtension}`)
(ensure destination dirs are created); apply the same relative-path-preserving
change to the global-destination branch that sets ptOutputPath in the other
block.
- Around line 153-169: The code currently picks the first non-negated glob in
computeContentBase which breaks multi-root content setups; change
computeContentBase to derive a common root from all positive patterns by
extracting each pattern's staticPart (the segment before the first glob char),
normalizing/dirname'ing them, and computing their longest common path (common
ancestor) instead of returning the first match; keep resolveOutputPath as-is (it
can continue to use the computed contentBase) so relative(...) no longer
produces leading ".." for templates in other configured roots.
- Around line 82-91: The post-render hooks are being called with the original
renderConfig instead of the merged effective template config
(rendered.templateConfig), causing hooks to see stale settings; update both
calls to events.fireAfterRender and events.fireAfterTransform to pass
templateConfig (the merged rendered.templateConfig) as the config argument
(i.e., replace renderConfig with templateConfig) so hooks receive the effective
config used by subsequent logic (ensure you reference rendered.templateConfig
when building the templateConfig variable used in those calls).

In `@src/render/parallel/buildWorker.ts`:
- Line 40: The current call const config = defu(configData, reloaded) in
buildWorker.ts uses defu's default array-concatenation semantics and can
duplicate array entries; change the merge to one where configData overrides
reloaded and arrays are replaced (not concatenated). Replace the defu call with
a merge utility/configured deep-merge that treats arrays as replace (e.g.,
deepmerge or a custom merge function) and pass arguments in the correct
precedence so configData wins; ensure the symbol names remain config, configData
and reloaded and that the resulting type is still castable to MaizzleConfig.
Validate behavior for plugin/source lists to confirm arrays from configData
fully replace those from reloaded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f484aa9-f23a-4f2f-8be6-e5bc966f69fa

📥 Commits

Reviewing files that changed from the base of the PR and between ead901e and 69f03ca.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • package.json
  • src/build.ts
  • src/render/buildTemplate.ts
  • src/render/createRenderer.ts
  • src/render/parallel/buildWorker.ts
  • src/render/parallel/worker.mjs
  • src/tests/build.test.ts
  • src/types/config.ts
  • tsdown.config.ts

Comment thread src/build.ts
Comment on lines +164 to +165
const workers = Math.min(maxWorkers, count)
return { enabled: workers >= 2 && count >= 2, workers }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

parallel: true is still forced to sequential for a single template.

Line 165 requires workers >= 2 && count >= 2, so parallel: true and threshold: 0 do not actually force parallel for single-template builds, which conflicts with the documented behavior in this file.

Suggested fix
-  return { enabled: workers >= 2 && count >= 2, workers }
+  return { enabled: workers >= 1 && count >= 1, workers }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const workers = Math.min(maxWorkers, count)
return { enabled: workers >= 2 && count >= 2, workers }
const workers = Math.min(maxWorkers, count)
return { enabled: workers >= 1 && count >= 1, workers }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/build.ts` around lines 164 - 165, The current return forces sequential
runs for single-template builds because enabled is computed as workers >= 2 &&
count >= 2; update the condition to respect explicit parallel/threshold
settings: keep const workers = Math.min(maxWorkers, count) but return { enabled:
workers >= 2 && (count >= 2 || parallel === true || threshold === 0), workers }
(or use the actual option names in scope if different) so that explicit
parallel:true or threshold:0 will enable parallel even when count === 1; adjust
the function signature or captured variables to reference the surrounding
parallel and threshold config values used elsewhere in this module.

Comment thread src/build.ts
Comment on lines +197 to +210
const results = await Promise.all(
batches.map(templatePaths => pool.run({
templatePaths,
configPath,
configData,
outputPath,
outputExtension,
contentBase,
})),
)

return {
files: results.flatMap(r => r.files),
sfcAfterBuildCount: results.reduce((n, r) => n + r.sfcAfterBuildCount, 0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Parallel workers can nondeterministically overwrite the same output file.

At Line 197, batches are executed concurrently and each worker writes to disk inside buildTemplate(). If two templates resolve to the same output path (for example via custom output mappings), the final file becomes timing-dependent in parallel mode, while sequential mode is deterministic.

Please add collision protection (fail-fast on duplicate output targets, or write to per-worker temp locations and merge deterministically on the main thread).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/build.ts` around lines 197 - 210, Detect and prevent duplicate output
targets before spawning parallel workers: before calling
Promise.all([...pool.run(...)]) flatten the batches and, using the same
output-resolution logic used by buildTemplate (extract that logic into or call a
new helper like computeOutputTarget(templatePath, configPath, configData,
outputPath, outputExtension) if necessary), compute each template's final output
file path deterministically, check for duplicates and fail-fast with a clear
error if any collisions exist; alternatively you can change pool.run workers to
write to per-worker temp files and then, on the main thread after results
return, atomically move/merge those temp files into the real output directory in
a deterministic order while checking for duplicates — reference batches,
pool.run, buildTemplate (or the new computeOutputTarget helper), results and
files when implementing the change.

Comment thread src/render/buildTemplate.ts Outdated
Comment thread src/render/buildTemplate.ts Outdated
Comment thread src/render/buildTemplate.ts
Comment thread src/render/parallel/buildWorker.ts Outdated
@cossssmin
cossssmin merged commit 2471ed9 into master Jun 6, 2026
6 checks passed
@cossssmin
cossssmin deleted the feat-parallel-build branch June 10, 2026 13:01
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.

1 participant