fix(editor): load the email code block renderer lazily

Merged
VC
2d ago
+149-33

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.).
  • CodeBlock imported eagerly, and CodeBlock imports prism.ts with 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: renderToReactEmail renders that module through React.lazy. render() already wraps the tree in <Suspense> and awaits allReady (browser and node builds), so the composed HTML is unchanged: the existing Dracula snapshot passes as is. tsdown emits code-block-email-*.mjs/.cjs as 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/editor patch.

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 JSlazy JS (gzip)
before611 KB37181 KB
after201 KB12 (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:unit in packages/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 (composeReactEmail in 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.lazy loads 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.