fix(editor): load the email code block renderer lazily
Every bundle that mounts the editor ships all 371 Prism grammars and every react-email component, even when no email is ever rendered from it. On resend.com this is a 1.36 MB (427 KB gzip) chunk on the homepage, which only hosts an editor demo.
The cause is the code block's email renderer in extensions/code-block.tsx:
import * as ReactEmailComponents from 'react-email', read by a dynamic key to pick the theme, so bundlers can't tree-shake any react-email export (Tailwind, Markdown, every theme,CodeBlock, etc.).CodeBlockimported eagerly, andCodeBlockimportsprism.tswith every grammar.
Changes
extensions/code-block-email.tsx(new): the email renderer, with an explicit map of react-email's 38 code block themes in place of the namespace lookup. Unknown names, including the'default'theme, still fall back to the gray block.extensions/code-block.tsx:renderToReactEmailrenders that module throughReact.lazy.render()already wraps the tree in<Suspense>and awaitsallReady(browser and node builds), so the composed HTML is unchanged: the existing Dracula snapshot passes as is. tsdown emitscode-block-email-*.mjs/.cjsas a separate chunk.code-block.spec.tsx: a test that every theme react-email exports is in the map, so adding a theme to react-email without adding it here fails CI (checked by removing one).- Changeset:
@react-email/editorpatch.
Impact
Bundling StarterKit from @react-email/editor/extensions for the browser with rolldown, react and react-dom external:
| eager JS (gzip) | Prism grammars in eager JS | lazy JS (gzip) | |
|---|---|---|---|
| before | 611 KB | 371 | 81 KB |
| after | 201 KB | 12 (the in-editor highlighter's) | 211 KB |
In a Next.js 16 (Turbopack) production build of resend.com with the same change applied as a patch, the 427 KB gzip chunk leaves the homepage and the editor pages, and the lazy chunk is referenced by no route. esbuild's splitter still hoists CodeBlock into a shared chunk because the eager graph reaches react-email's index.mjs, so esbuild users see a smaller win.
Testing
pnpm test:unitinpackages/editor: 54 files, 522 tests pass (521 before plus the new one).pnpm typecheck,pnpm lint,pnpm build.- In resend.com, composed emails with code blocks still come out highlighted in the chosen theme (
composeReactEmailin jsdom and in the browser editor).
Not in this PR
prism-plugin.ts still does import(\prismjs/components/prism-${language}`), which makes bundlers emit a chunk for every Prism component; resend.com patches it to a fixed list. A loadLanguageoption onCodeBlockPrism` would let apps choose, but it changes the API, so it's left for a separate discussion.
Summary by cubic
Loads the email code block renderer lazily so bundles that mount the editor no longer ship all 371 Prism grammars and every react-email component. On resend.com this removes a 427 KB gzip chunk from the homepage.
- Moves the renderer into a new module with an explicit map of react-email's code block themes;
React.lazyloads it only when an email with a code block is rendered. - Composed HTML is unchanged because
render()already waits for the lazy chunk inside its Suspense boundary. - Adds a test that fails CI when react-email exports a theme not in the map.