Your email templates are probably slow. Not "slow to load"—slow to render, slow to send, and bloated with unnecessary HTML that makes Gmail's servers work harder than they need to.
Most developers ship React Email templates without thinking about performance. The result? 200KB+ HTML payloads, 3-second render times in development, and ESPs throttling your send rate because your templates are too heavy.
This guide covers five performance optimization strategies that reduce HTML size by 40-60%, cut render time in half, and make your email sending infrastructure faster and more reliable.
1. HTML Size Optimization: Cut the Fat
The average React Email template renders to 80-150KB of HTML. Most of that is unnecessary: redundant inline styles, copy-pasted CSS, and verbose table structures.
Deduplicate Inline Styles
React Email inlines styles on every element. If you're repeating the same button style 10 times, you're shipping the same CSS 10 times.
// ❌ Bad: Inline styles on every button
<a href={url} style={{
backgroundColor: "#007bff",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
textDecoration: "none",
display: "inline-block"
}}>
Click here
</a>
// ✅ Good: Shared style object
const buttonStyles = {
backgroundColor: "#007bff",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
textDecoration: "none",
display: "inline-block",
};
export function Button({ href, children }: ButtonProps) {
return <a href={href} style={buttonStyles}>{children}</a>;
}Impact: Reduces HTML size by 15-25% for template-heavy emails (invoices, notifications).
Minimize Table Nesting
Email HTML uses tables for layout. Every nested table adds ~200-400 bytes of boilerplate. Flatten your layout where possible.
// ❌ Bad: 4 levels of table nesting
<table><tr><td>
<table><tr><td>
<table><tr><td>
<table><tr><td>
Content here
</td></tr></table>
</td></tr></table>
</td></tr></table>
</td></tr></table>
// ✅ Good: 2 levels, semantic sections
<table><tr><td>
<Section style={containerStyles}>
<Row>
<Column>Content here</Column>
</Row>
</Section>
</td></tr></table>Impact: Saves 2-5KB per email. Multiply that by 100K sends/month and you're saving 200-500MB of data transfer.
Remove Unused Styles
Copy-pasting components brings unused styles along for the ride. Audit your templates for dead CSS.
# Search for unused style definitions
grep -r "style={{" emails/ | sort | uniq -c | sort -n
# Or use a React Email plugin (if available)
npx react-email optimize ./emails2. Render Performance: Speed Up Development
Slow render times kill your development velocity. If it takes 3 seconds to preview an email change, you're wasting hours per week.
Lazy-Load Preview Data
Don't compute complex preview data on every render. Memoize it or load it lazily.
import { useMemo } from "react";
// ❌ Bad: Recomputes on every render
function InvoiceEmail({ invoiceId }: Props) {
const lineItems = generateMockLineItems(50); // Expensive!
const calculations = calculateTotals(lineItems); // More expensive!
return <InvoiceTemplate items={lineItems} totals={calculations} />;
}
// ✅ Good: Memoized preview data
function InvoiceEmail({ invoiceId }: Props) {
const lineItems = useMemo(() => generateMockLineItems(50), []);
const calculations = useMemo(() => calculateTotals(lineItems), [lineItems]);
return <InvoiceTemplate items={lineItems} totals={calculations} />;
}Optimize Component Re-Renders
React Email templates are just React components. Use React.memo on expensive sections.
import { memo } from "react";
// Expensive component: 50 line items × complex calculations
const InvoiceLineItems = memo(function InvoiceLineItems({ items }: Props) {
return (
<table>
{items.map((item) => (
<tr key={item.id}>
<td>{item.description}</td>
<td>{formatCurrency(item.amount)}</td>
</tr>
))}
</table>
);
});Impact: Cuts preview render time from 3s → 1s for complex templates (invoices, reports).
3. Image Optimization: Shrink Your Assets
Images are the #1 cause of slow email loads on mobile. Optimize aggressively.
Compress and Resize
- Logos: Max 200px wide, save as PNG-8 or SVG (but test SVG support—Outlook doesn't support it).
- Hero images: Max 600px wide (email standard), compress to <100KB using tools like TinyPNG or ImageOptim.
- Icons: Use Unicode symbols (✓ ✗ ★) or embedded data URIs for tiny icons (<2KB).
# Batch optimize images
npx @squoosh/cli --webp auto public/emails/*.{jpg,png}
# Or use a build script
npm install sharp --save-devconst sharp = require("sharp");
const fs = require("fs");
const path = require("path");
const inputDir = "./public/emails";
const outputDir = "./public/emails/optimized";
fs.readdirSync(inputDir).forEach(async (file) => {
if (file.match(/.(jpg|jpeg|png)$/)) {
await sharp(path.join(inputDir, file))
.resize(600) // Max width for emails
.jpeg({ quality: 80 })
.toFile(path.join(outputDir, file));
}
});Use a CDN with Caching
Host images on a CDN (Cloudflare, AWS CloudFront) with aggressive caching headers. Gmail and Outlook cache images—make sure your CDN does too.
Cache-Control: public, max-age=31536000, immutable
Content-Type: image/jpeg
Vary: Accept-EncodingImpact: Reduces load time by 50-70% for image-heavy emails (newsletters, product updates).
4. CSS Inlining Strategies: Balance Size vs. Compatibility
React Email inlines CSS by default (required for Gmail). But you can optimize how much CSS gets inlined.
Inline Critical CSS Only
Not all styles need to be inline. Use <style> tags for repeated utility styles (supported by Apple Mail, Outlook 365).
// ✅ Good: Inline critical styles, <style> tag for utilities
<Html>
<Head>
<style>{`
.text-center { text-align: center; }
.text-muted { color: #6c757d; }
.mb-4 { margin-bottom: 16px; }
`}</style>
</Head>
<Body>
<Section className="text-center mb-4">
<Heading>Welcome</Heading>
</Section>
</Body>
</Html><style> tags. Only use this for non-critical styles that can gracefully degrade.Use Tailwind with Caution
Tailwind is tempting for emails, but it bloats HTML fast. Every utility class adds ~50-100 bytes when inlined.
// ❌ Tailwind: 300+ bytes inlined
<div className="flex items-center justify-between bg-gray-100 p-4 rounded-lg shadow-sm">
Content
</div>
// ✅ Semantic styles: 150 bytes inlined
<Section style={cardStyles}>
Content
</Section>Rule of thumb: If you're using more than 5 Tailwind classes on a single element, extract it into a semantic component.
5. Send Performance: Faster at Scale
Template size affects send performance. ESPs process emails faster when they're smaller and simpler.
Measure Send Time
Track how long it takes to render and send each template. Slow sends signal optimization opportunities.
import { render } from "@react-email/render";
import { resend } from "@/lib/resend";
export async function sendEmail(template: React.ReactElement, to: string) {
const startRender = Date.now();
const html = render(template);
const renderTime = Date.now() - startRender;
const startSend = Date.now();
await resend.emails.send({
from: "noreply@example.com",
to,
subject: "...",
html,
});
const sendTime = Date.now() - startSend;
console.log(`Render: ${renderTime}ms | Send: ${sendTime}ms | Size: ${html.length} bytes`);
}Cache Rendered Templates
For templates with static content (welcome emails, receipts), render once and cache the HTML. Re-render only when props change.
import { render } from "@react-email/render";
import { LRUCache } from "lru-cache";
const templateCache = new LRUCache<string, string>({
max: 100, // Cache 100 rendered templates
ttl: 1000 * 60 * 60, // 1 hour TTL
});
export function renderWithCache(template: React.ReactElement, cacheKey: string) {
const cached = templateCache.get(cacheKey);
if (cached) return cached;
const html = render(template);
templateCache.set(cacheKey, html);
return html;
}Impact: Reduces send time by 30-50% for high-volume templates (password resets, notifications).
Testing and Monitoring
Optimization is pointless without measurement. Track these metrics to catch performance regressions:
- HTML size: Target <50KB for simple emails, <100KB for complex ones (invoices, reports).
- Render time: <100ms for production sends, <1s for development previews.
- Image size: <100KB per image, <300KB total per email.
- Send time: <500ms end-to-end (render + ESP upload + acceptance).
Automated Performance Tests
Add performance tests to your CI pipeline to catch bloated templates before they ship.
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/emails/welcome";
import { describe, it, expect } from "vitest";
describe("Email performance", () => {
it("welcome email should be under 50KB", () => {
const html = render(<WelcomeEmail name="Test User" />);
const sizeKB = new Blob([html]).size / 1024;
expect(sizeKB).toBeLessThan(50);
});
it("should render in under 100ms", () => {
const start = Date.now();
render(<WelcomeEmail name="Test User" />);
const duration = Date.now() - start;
expect(duration).toBeLessThan(100);
});
});Practical Performance Checklist
Before shipping a new email template, run through this checklist:
- HTML size: Render to HTML and check file size. Target <50KB for simple emails.
- Style deduplication: Extract repeated inline styles into shared components.
- Table nesting: Flatten layout tables to 2-3 levels max.
- Image optimization: Compress images to <100KB, resize to 600px max width.
- Render performance: Measure render time in dev—should be <1s for fast iteration.
- Send performance: Log render + send time for production sends.
- Caching: Consider caching rendered HTML for high-volume static templates.
The Bottom Line
Email template performance isn't glamorous. But for high-volume SaaS products sending 100K+ emails/month, these optimizations save real money (lower ESP costs), improve reliability (faster send queues), and boost deliverability (smaller emails = less spam risk).
Start with the low-hanging fruit: deduplicate inline styles, optimize images, and add performance tests. The rest you can optimize as needed.