One year of Dioxus fullstack in production: the good, the painful
Hauke Jung··7 min readOn this page (5 sections)
After a year of Dioxus fullstack in production, the Rust code is the most boring part of my business. CI is the most exciting part, and not in a good way: a release build takes 90 minutes on a GitHub-hosted runner.
That year started at my previous job and continued on my own products. Back in April I wrote about why I picked Rust for a production micro SaaS. The short version: slower at the start, cheaper to live with later. This post is the bill.
What I’m running
SeggWat is an in-app feedback tool for SaaS products. Its dashboard is a Dioxus 0.7 fullstack app inside a Cargo workspace: a backend crate with server functions, an auth crate, and about ten UI crates covering insights, feedback, ideas, boards, widgets and settings. Twelve crates depend on dioxus. Everything sits on PostgreSQL.
infra.page is built on the same stack.
“Fullstack” means one Rust codebase doing three jobs. It renders on the server, runs server functions as the backend, and compiles the client to WebAssembly so the page becomes interactive.
The good
One language, all the way down
Database query, business rule, server function, button: all Rust. One toolchain, one formatter, one set of habits, one place to look when something breaks.
That sounds minor until you’re a solo founder juggling several products. I don’t have a “frontend brain” and a “backend brain” that each need warming up. I open the workspace and keep going.
The same types on both sides
This is the one I’d miss most.
A struct gets defined once. The database layer returns it, the server function sends it, and the component renders it. When I rename a field, cargo check gives me a list of every place that broke, including the UI. There’s no OpenAPI spec to regenerate and no TypeScript interface drifting out of sync with the backend.
The win isn’t writing code faster. It’s refactoring without fear, late at night, with nobody to review it.
The door to other platforms stays open
Dioxus can target desktop and mobile, including iOS, from the same component model. None of my products needs a native app today. But knowing I could ship one without a rewrite mattered when I decided where to put a year of work.
Production is boring
That’s the whole point. Once it compiles, it mostly just runs. A whole class of “undefined is not a function” bugs never reaches users, so the bugs that do get through are logic bugs. That’s where I’d rather spend my attention anyway.
The “boring in production” argument from April survived contact with real users.
The painful
Compile times
This is the part Rust evangelists tend to skip.
The dashboard compiles twice: once for the server and once to wasm32 for the browser. A dozen Dioxus crates plus a macro-heavy UI add up. Locally it’s tolerable on a decent machine. CI is a different story, and the comments in my workflow files read like a diary of it:
- The WASM build alone takes about 22 minutes on 6 cores. That’s a single leaf crate: the dashboard compiled for the browser.
- A release image takes about 90 minutes on a GitHub-hosted 2-core runner. On my own VPS runner it’s around 40, and that’s the good version.
- Clippy got OOM-killed.
cargo clippy --workspace --all-targetson the 7 GB hosted runner died with exit code 143 and no diagnostics at all. The fix was a 10 GB swapfile plusCARGO_BUILD_JOBS=1to cut peak memory. My linter needs swap.
At some point I stopped building images on every push to main. They now build on release tags or when I trigger them by hand, because 90 billed minutes per push is a silly way to spend money.
What I tried
- Caching with
Swatinem/rust-cache. It helps, but there’s a trap. A workflow run can only restore caches from its own ref or the default branch, and GitHub evicts caches unused for 7 days. Once I stopped building on main, release builds kept starting cold. My workaround is almost funny: after a dependency bump, I manually trigger a build on main just to keep the cache warm. - Building and testing in one go, so the tests reuse compiled artifacts instead of compiling the world a second time.
- The
moldlinker. Nice, but linking was never the bottleneck. Compiling was. - A self-hosted runner for the heavy builds. This was by far the biggest win, 90 minutes down to 40. It also means I now maintain a build server.
Every one of these helped. None of them changed the underlying math: two compile targets, a big dependency tree and lots of macros means a lot of compiling.
“All Rust” comes with an asterisk
The business logic is all Rust. The edges aren’t.
SeggWat’s insights page shows charts, and I couldn’t find a Rust charting library I’d trust in production. So the charts are Chart.js, loaded from a CDN and wrapped in about 340 lines of JavaScript that Dioxus calls through dioxus-use-js. Styling is Tailwind, so CI also installs Bun and runs the Tailwind CLI before Rust even starts.
I’m fine with that. It’s pragmatic. But if you’re choosing Dioxus specifically to escape JavaScript, know what you’re signing up for: Rust, plus some JS glue, plus a JS toolchain in your pipeline.
Would I do it again?
People ask, “What helps me ship fastest?” The better question is, “What helps me keep shipping without hating my life?”
For the backend, the answer is still Rust, and it isn’t close.
For the frontend, I’ve changed my mind a bit. If I were building something truly app-like, with heavy client state, offline support or a real mobile app, I’d pick Dioxus again. But most of what I build is dashboards: forms, tables, a few charts and some toggles. For that, I’m paying a 22-minute WASM tax on every release for interactivity I barely use.
What’s next: Topcoat
So my next project is going to use Topcoat, a new fullstack framework from the tokio-rs org, announced in July.
It makes the opposite bet from Dioxus. Everything renders on the server, and components are async functions that can query the database directly. For interactivity, you write $(...) expressions in ordinary, type-checked Rust. Topcoat evaluates them on the server for the first render and also translates them to JavaScript, so they re-run instantly in the browser. There’s no WASM bundle and no second compile target.
async
Example from the Topcoat README.
On paper, Topcoat keeps what I love about Dioxus (one language, shared types, no API layer) and removes the part that hurts most, the 22-minute WASM build.
To be clear, this is a bet, not a result. Topcoat’s README calls it early-stage and experimental, with breaking changes expected. I’m giving up the multi-platform option. And I haven’t measured a single build yet.
I’ll write the follow-up with real numbers: compile times, CI minutes, and whatever I end up missing from Dioxus. If my release build drops from 40 minutes to 10, you’ll hear about it. If it doesn’t, you’ll hear about that too.
I’m Hauke. I build SeggWat, Stepshots and infra.page in Rust and write about it on OXIDT.
