For all the progress we’ve made in web development, designing emails still feels like coding for Internet Explorer 6. Tables everywhere. Inline styles. No flexbox, no grid, and every email client with its own quirks. Without knowing it, you step into a time machine, only you’re not sure if you’ll ever make it back (and if you do, Outlook will be waiting).
That’s why frameworks like MJML exist. They let people write clean, modern markup and compile it down to the gnarly HTML that actually works in Outlook and Gmail. But the thing is, MJML’s official implementation is Node.js-based. If you’re building backend services in Go, that means shelling out to Node, running a separate process, or calling a web API. It works, but it’s clunky. It’s not Go. Plus, it’s darn slow. Try running the MJML compiler at scale, and you’ll quickly explode your server costs.

MJML - The Responsive Email Framework
The only framework that makes responsive email easy. MJML is a markup language designed to reduce the pain of coding a responsive email.
For what it’s worth, my team and I use MJML for pretty much all of our projects, and we genuinely love it. But frankly, it’s always a bit of a pain having to manually convert MJML into HTML, keep both versions in your repository, and deal with the inevitable “did I forget to recompile this one?” moments.
Smoothing that friction out is exactly why I set out to build gomjml.
preslavrachev/gomjml
Repository information unavailable
gomjml
is a native Go implementation of the MJML framework. It lets you take MJML markup and compile it directly to responsive HTML, all within your Go application. No Node, no external dependencies, just Go.
Why bother? Because I wanted something fast, embeddable, and easy to deploy. I wanted to remove the friction of email generation from my Go projects. And honestly, I was tired of the “just use Node” workaround. Sometimes, you want your tools to speak the same language.
And the numbers back this up:
Tool | 50x Total (ms) | Avg (ms) | Max RAM (MB) | Avg CPU (%) |
---|---|---|---|---|
gomjml | 162 | 3 | 2 | 0 |
mrml | 96 | 1 | 1 | 0 |
mjml (JS) | 13530 | 270 | 90 | 20.9 |
In our benchmarks, gomjml
compiles a real-world MJML template in about 3ms, using just 2 MB of RAM. The official JavaScript MJML compiler? Try 270ms and 90 MB of RAM, plus a hefty chunk of CPU. If you’re generating lots of emails, those differences add up—fast. And thinking long-term, this isn’t saving money is only one side of the coin—using less compute and memory also means a smaller carbon footprint for our software. That’s a win for everyone.
gomjml
is still new and in active development, but it already covers all the core MJML components and is fast enough for real-world use. If you’re curious about the technical details or want to try it out, check out the README and the repo.
One important note: while MJML is the original framework, my reference for gomjml has actually been MRML—the excellent Rust implementation of MJML. MRML is fast, native, and closely tracks the MJML spec, making it a much better fit for building a Go-native compiler than the official Node.js version. So if you notice gomjml behaving a bit more like MRML than MJML, that’s by design.
Email is still a mess. But at least, with gomjml, it’s a mess I can more easily handle with Go code.
Thank you for reaching this point! If you enjoyed this post or found it helpful, consider supporting my creative journey! Every coffee helps me keep writing and sharing new ideas.
Have something to say? Send me an email or ping me on social media 👇
Want to explore instead? Fly with the time capsule 🛸
You may also find these interesting
Display your Claude Code Token Usage on Your Mac's Toolbar
A simple Python script and xbar setup to monitor Claude Code token usage directly in your macOS toolbar.
Hitting the Brakes on Claude Code
Prevent Claude Code from burning tokens aimlessly. Slow things down with a simple shell trick.
Why I Made Peace With Go’s Date Formatting
If we’re all going to google it anyway, we might as well google something that makes sense.
The Two Reasons I Prefer Passing Struct Pointers Around
Choosing consistency over performance.