Rust’s compiler, rustc, is famously thorough, which makes for safe, performant code, but it can also make builds feel like they’re happening in slow motion. The good news is that you can often shave significant time off your compilation process with a few targeted cargo flags and some clever profiling.

The Magic of --release

The most obvious speed-up comes from enabling optimizations. When you build without --release, cargo defaults to a debug build. This means minimal optimizations, more verbose debug symbols, and generally faster compilation times because the compiler is doing less work. However, it produces code that’s much slower to run.

When you do use --release, cargo tells rustc to crank up the optimization levels (typically to -O3 and -Oz for size, depending on the profile). This makes the compiled binary run much faster, but it dramatically increases compilation time because the compiler is doing a huge amount of work to transform the code.

If you’re just testing out a change locally and don’t need optimized performance, skip --release. If you do need release performance, you’re going to have to accept longer compile times. The trick is to use --release only when you truly need it.

Incremental Compilation: The Default Hero

Rust’s incremental compilation is a game-changer, and it’s enabled by default in recent cargo versions. It works by saving intermediate compilation artifacts. When you make a change, cargo only recompiles the parts of your code that were affected by that change, rather than the entire project from scratch.

To ensure it’s on, you don’t need to do anything special; it’s the default behavior for both debug and release builds. You can, however, explicitly disable it if you suspect it’s causing issues (though this is rare for performance) by adding this to your Cargo.toml:

[profile.dev]
incremental = false

[profile.release]
incremental = false

Disabling incremental compilation forces a full rebuild every time, which is almost always slower for iterative development.

Parallel Compilation: More Cores, Less Waiting

cargo can compile multiple crates in parallel. This is controlled by the CARGO_BUILD_THREADS environment variable or by passing the -j flag. By default, cargo tries to be smart and uses a number of threads related to your CPU core count.

To explicitly set the number of parallel jobs, you can use:

CARGO_BUILD_THREADS=8 cargo build --release

Or, more commonly, just make style:

cargo build -j 8 --release

Setting -j to a value slightly higher than your CPU core count can sometimes yield better results, as some threads might be waiting on I/O. Experiment with values like num_cores + 1 or num_cores * 1.5. Too many threads can lead to excessive memory usage and thrashing, slowing things down.

Dependency Management: The Silent Killer

The number and complexity of your dependencies have a massive impact on build times. Every crate you add, and especially every crate they depend on, needs to be compiled.

1. Use cargo tree to analyze your dependencies:

cargo tree

This command shows you a tree-like structure of your project’s dependencies. Look for:

  • Deep dependency chains: Where one crate depends on another, which depends on another, etc. This can lead to long compilation paths.
  • Duplicate dependencies: Sometimes, different versions of the same crate are pulled in, leading to redundant compilation. Cargo usually handles this, but it’s good to be aware.

2. Be judicious with new dependencies: Before adding a new crate, ask yourself if you truly need it or if the functionality can be achieved with existing dependencies or a smaller, more focused crate.

3. Opt for smaller, more focused crates: If you need a JSON parser, for example, serde_json is excellent, but if you only need to parse JSON and not serialize, you might find a more specialized crate that has fewer dependencies itself.

4. Feature flags: Many larger crates offer optional "feature flags" that enable or disable certain functionalities. You can disable features you don’t need in your Cargo.toml to reduce the compilation surface area. For example, for serde:

[dependencies]
serde = { version = "1.0", features = ["derive"] } # Only enable derive if you need it

By default, cargo enables all features. You can explicitly disable them or select only the ones you need.

LTO: Optimization vs. Compile Time

Link-Time Optimization (LTO) is a powerful optimization technique that allows the compiler to optimize across crate boundaries. This can result in significantly faster runtime performance, but it comes at a steep cost to compile time, especially for release builds.

You can control LTO in your Cargo.toml:

[profile.release]
lto = "fat" # or "thin", or "off"
  • lto = "off": No LTO. Fastest compile times, potentially slower runtime.
  • lto = "thin": Thin LTO. A good balance, faster than "fat" LTO, still offers significant cross-crate optimization.
  • lto = "fat": Full LTO. Potentially the best runtime performance, but the slowest compile times.

For most projects, lto = "thin" offers a good compromise. If your build times are becoming unbearable, consider setting lto = "off".

Profile-Guided Optimization (PGO)

This is a more advanced technique that can yield substantial runtime performance improvements, but it also increases build complexity and compile time. PGO involves compiling your code once with special instrumentation, running it with representative workloads to generate profile data, and then recompiling with that data to guide optimizations.

To enable PGO, you typically need to set environment variables before running cargo build:

# For Linux/macOS
export RUSTFLAGS="-Cprofile-use=cargo:input-dir=/path/to/your/project"
cargo build --release

# For Windows (cmd.exe)
set RUSTFLAGS=-Cprofile-use=cargo:input-dir=C:\path\to\your\project
cargo build --release

# For Windows (PowerShell)
$env:RUSTFLAGS="-Cprofile-use=cargo:input-dir=C:\path\to\your\project"
cargo build --release

After running your application with the instrumented build and generating profile data (e.g., .profdata files), you would then re-run cargo build --release without the RUSTFLAGS to produce the final optimized binary.

Profiling Your Builds

When all else fails, or you want to identify the specific bottlenecks, you can profile your build process.

1. cargo build --timings:

cargo build --release --timings

This flag will print a breakdown of how long each crate took to compile, including how much time was spent in the compiler itself versus waiting for other tasks. This is invaluable for spotting which specific dependencies are the slowest.

2. cargo clean && cargo build --release: A simple baseline. If your incremental builds are suddenly very slow, a cargo clean and then a full rebuild can sometimes reveal if incremental state is corrupted or if a change has had a disproportionately large impact.

3. Using perf (Linux) or Instruments (macOS): For a deeper dive, you can profile the cargo process itself. This is more involved and requires understanding system-level profiling tools. You’d typically run cargo build --release under perf record and then analyze the resulting perf.data file to see where CPU time is being spent within rustc.

The next step after optimizing your build times is often improving your application’s runtime performance, which involves different profiling techniques and compiler flags.

Want structured learning?

Take the full Cargo course →