一年 Dioxus 全栈生产实践:优点与痛点
One year of Dioxus fullstack in production: the good, the painful

原始链接: https://oxidt.com/blog/one-year-of-dioxus-fullstack-in-production-the-good-the-painful

在生产环境中运行 Dioxus 全栈 Rust 应用一年后,作者称赞了单语言开发、共享类型、安全重构和稳定的生产表现。主要痛点是编译时间:仪表盘需要构建两次(服务端和 WASM),发布构建在 GitHub runner 上约需 90 分钟,在自托管 runner 上约需 40 分钟,且 Clippy 需要交换内存。缓存和自托管 runner 有帮助,但未解决底层的双目标构建成本。某些边缘功能仍需 JavaScript,包括 Chart.js 和 Tailwind。作者仍会选择 Dioxus 用于应用类或原生目标,但对于仪表盘,WASM 构建感觉是不必要的开销。下一个项目将尝试 Topcoat,一个没有 WASM 包的实验性服务端渲染全栈 Rust 框架,希望保留 Rust 的类型共享优势,同时大幅缩短 CI 构建时间。

一个 Hacker News 帖子链接到一篇关于在生产环境中使用 Dioxus fullstack 一年的文章,涵盖其优点和痛点。评论讨论了 CI/构建优化:rust-cache 在 ref 访问和 7 天缓存淘汰方面存在限制;自托管 runner 将构建时间从 90 分钟缩短到 40 分钟;建议使用 WarpBuild 进行基于 S3 的缓存和托管 spot 基础设施,并安排午夜定时重建以保持缓存温暖。一些评论者猜测文章或应用可能是 AI 生成的,而其他人对此提出异议。页面还包括标准的 Hacker News 导航和 YC 申请推广。
相关文章

原文

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-targets on the 7 GB hosted runner died with exit code 143 and no diagnostics at all. The fix was a 10 GB swapfile plus CARGO_BUILD_JOBS=1 to 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 mold linker. 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.

#[component]
async fn faq(cx: &Cx) -> Result<impl View> {
    let open = signal(cx, || false);
    Ok(view! {
        <button @click=$(|_e| open.set(!open.get()))>"What is Topcoat?"</button>
        <p :hidden=$(!open.get())>"A full-stack Rust framework."</p>
    })
}

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.

联系我们 contact @ memedata.com