fix: avoid OOM when exporting many email templates#3518
Conversation
`email export` crashed with "JavaScript heap out of memory" once a project had a few hundred templates. Two distinct causes, in two distinct phases: 1. Build phase: `esbuild.build()` was called once with every template as an entry point. With `bundle: true`, esbuild expands each entry's full dep graph in-memory (each template pulls ~1.5MB of react-email + transitive). The Go subprocess held all of that simultaneously. 2. Render phase: every bundled `.cjs` is self-contained with its own copy of react-email, tailwind, css-tree, react. Sequentially requiring them accumulates V8 state that `delete require.cache[...]` cannot release; heap grew ~14MB per template and crashed at ~290. Build phase now runs in batches of 10 entry points with `esbuild.stop()` between batches so the Go subprocess's RSS is released. Render phase runs batches of 25 templates inside `worker_threads.Worker` instances; each worker's V8 isolate is reclaimed on termination, so peak heap stays bounded by one batch's worth. Closes #2887
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 4ca81a9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
gabrielmfern
left a comment
There was a problem hiding this comment.
can you also add a changeset before we merrge?
There was a problem hiding this comment.
1 issue found across 1 file
Confidence score: 3/5
- There is a concrete regression risk in
packages/react-email/src/cli/commands/export.ts: explicitly forwardingprocess.execArgvtonew Worker()can triggerERR_WORKER_INVALID_EXEC_ARGVunder common Node flags. - Given the issue’s relatively high severity/confidence (7/10, 8/10) and direct runtime impact, this is more than a minor cleanup and could affect CLI export behavior for users.
- Pay close attention to
packages/react-email/src/cli/commands/export.ts- remove explicitexecArgvforwarding so worker startup remains compatible with inherited Node arguments.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.
Re-trigger cubic
Passing process.execArgv explicitly causes ERR_WORKER_INVALID_EXEC_ARGV when the parent runs with V8 flags like --max-old-space-size — which is exactly the workaround users reach for when hitting OOM. Workers inherit execArgv by default, and Node filters out flags that aren't worker-safe.
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Requires human review: This change restructures the core export pipeline with batching and worker threads, introducing moderate risk and requiring human review to ensure edge cases around error handling, worker lifecycle, and file cleanup are handled correctly.
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Auto-approved: The change addresses an OOM crash by batching esbuild builds and rendering in worker threads, all within the isolated CLI export command—the blast radius is limited and the risk of breakage is low.
Re-trigger cubic
email exportcrashed with "JavaScript heap out of memory" once a project had a few hundred templates. Two distinct causes, in two distinct phases:Build phase:
esbuild.build()was called once with every template as an entry point. Withbundle: true, esbuild expands each entry's full dep graph in-memory (each template pulls ~1.5MB of react-email + transitive). The Go subprocess held all of that simultaneously.Render phase: every bundled
.cjsis self-contained with its own copy of react-email, tailwind, css-tree, react. Sequentially requiring them accumulates V8 state thatdelete require.cache[...]cannot release; heap grew ~14MB per template and crashed at ~290.Build phase now runs in batches of 10 entry points with
esbuild.stop()between batches so the Go subprocess's RSS is released. Render phase runs batches of 25 templates insideworker_threads.Workerinstances; each worker's V8 isolate is reclaimed on termination, so peak heap stays bounded by one batch's worth.Closes #2887
Summary by cubic
Prevents OOM crashes in
email exportby batchingesbuildbuilds and rendering in short‑livedworker_threads. Also adds a changeset for areact-emailpatch release.esbuildentry points (10 at a time) and callesbuild.stop()between batches.worker_threads; each worker exits to reclaim its V8 heap. Do not forwardexecArgvto avoidERR_WORKER_INVALID_EXEC_ARGVwith flags like--max-old-space-size..cjsbundles after rendering.Written for commit 4ca81a9. Summary will update on new commits. Review in cubic