I'm working on a side project that is written in Rust on the backend and the frontend. The frontend component is in Leptos. Our app is about 20kLOC in total, so it takes a little time.
benwis has already written about this
Please see his posts at:
- https://benw.is/posts/how-i-improved-my-rust-compile-times-by-seventy-five-percent
- and: https://benw.is/posts/how-i-improved-my-rust-compile-times-part2
How'd you test this?
Edit a string in a component within our Leptos app back and forth, re-run cargo leptos serve
and monitor the times.
What is cargo leptos serve doing?
This confused me initially but it seems like there's basically three steps to the build:
- rustc wasm build
Front
(you see this in the logs), which is the wasm toolchain stuff. In release mode this'll includewasm-opt
.- rustc backend build
One thing I noticed is it seems to be doing this sequentially, I do wonder if splitting target directories and running the frontend and backend pipelines concurrently might save time.
Starting point
First, for local dev I'm using cargo leptos serve
and cargo leptos watch
with the default dev/debug build. I don't need an optimized build for local dev unless I'm benchmarking or profiling something and even then the dev build has often been representative, if slower.
The two machines I was testing on were:
- Linux box with an Intel 12900K, 32 GiB of RAM, NVMe SSD
- M3 Pro MacBook Pro, 18 GiB of RAM
The initial times to rebuild were:
- Linux: ~16 seconds
- Mac: 7 seconds
The first thing I did was start using mold on Linux, that dropped Linux down to ~7 seconds, putting it on par with macOS. macOS was faster because it uses the fancy new linker Apple introduced out of the box on macOS Sequoia.
With mold on Linux:
- Linux: 7 seconds
- Mac: 7 seconds
Ok, you're using mold. What next?
The next thing I did that helped was enabling opt-level = 3
in the dev profile for all dependencies and build-overrides, but not the project itself. Here's a snippet from our workspace Cargo.toml
:
[profile.dev]
opt-level = 0
debug = 1
incremental = true
[profile.dev.package."*"]
opt-level = 3
debug = 2
# For build scripts and proc-macros.
[profile.dev.build-override]
opt-level = 3
debug = 1
for our workspace made it a bit faster than debug = 2
. I set the dependencies to debug = 2
because they build once. Our workspace is still opt-level = 0
because that's the fastest option. This applies to both the wasm and the backend build.
This brought the times for both down:
- Linux: 5 seconds
- Mac: 5 seconds
What didn't help?
- I didn't need to do anything about incremental builds, so asking for it explicitly didn't do anything.
- Parallel rustc frontend made it slower. This was a bit of a disappointment as the leptos app is a single crate right now. There are other crates in the workspace (e.g. database models) but the app itself is unitary.
- Cranelift didn't help the backend build, it's almost entirely link time at the moment. I think I'd need to squish down link time before compile-time mattered again. I wasn't able to use Cranelift for the wasm build, I don't know if that's supported/stable yet.
- The support in Leptos/cargo-leptos for hot-reloading is pretty cool but didn't make a perceptible impact on responsiveness. I think you'd need an MRE-sized app with very quick recompilation/linking times before that made a difference.
What might help?
Crate-splitting likely wouldn't help with the build times on the backend part. I profiled rustc for the frontend and backend builds and the backend build is almost 95% link time.
I'm a lot less clear on how the rustc wasm build works but linking didn't explicitly show up there, it looked like it was actually re-doing the same compilation work on each rebuild and wasn't very incremental. According to benwis who was very helpful on the Leptos Discord (Linked on their main site), the bundle size is the main thing that impacts the wasm build. The way to ameliorate this for local dev probably looks like crate splitting and separating components of the website with wasm-split
. Probably not worth it for me yet as we don't yet have a neat line of division that would slice off a significant chunk of the wasm payload from the app. There's really just a couple larger main areas of the app and I don't think I can split it horizontally like presentation vs. logic without bridging via JS. I haven't done a lot of frontend in like a decade so there are almost certainly gaps in my understanding here.
-
cargo-add-dynamic
might help the backend project but I haven't tried yet. I'd need to figure out what dependencies are creating the most work for the linker. -
Crate-splitting could help the backend app IFF I combined it with
cargo-add-dynamic
. The backend app is about as divided up as it can be for now. It's already an 8 crate workspace.