Skip to content

Compilation Benchmarks ​

This page compares the compilation performance with bon and alternative builder crates.


For any proc macro, two things are contributing to compile times:

  1. Compiling the proc macro crate into a dynamic library.
  2. Compiling the code generated by the proc macro.

The benchmarks here intentionally include only 2 in measurements.

See the reasoning behind this

The proc macro crate dynamic library is compiled only once and remains in the build cache. For example, once you add bon as a dependency, it'll be compiled only once and never recompiled during incremental rebuilds. The cost of that build is rather small and constant. So this isn't of a big interest to the scope of these benchmarks.

Measurements ​

The benchmarks measure the time it takes to run cargo build against a crate that uses #[derive(Builder)] for the given number of structs with the given number of fields in each of them. Every struct is also built once with all of its setters called, so the measurements include the cost of the builder usage in addition to the cost of the builder definition.

Builder crate10 structs with 50 fields100 structs with 10 fields
bon1.557s1.612s
typed-builder2.059s1.723s
derive_builder0.407s1.055s
no macros0.094s0.114s

Summary ​

Builder macros do add noticeable overhead to the compilation performance due to the number of additional structs and methods they generate.

bon compiles faster than typed-builder in both benchmarks, but slower than derive_builder.

Can bon improve compile times? ​

bon v3.10.2 already improved its compile times by ~30% (#405). Before that release, bon was slower to compile than typed-builder.

Once the associated_type_defaults nightly Rust feature hits stable, it'll be possible to greatly improve compile times. See this comment, which measures the improvement at 16-58%.

Other improvements come at a cost 🪙. bon generates additional traits and structs to expose a nice and stable typestate API. This also reduces the builder's type signature size and the generic type noise in the generated builders' docs. See docs comparison (the difference is drastic). bon prioritizes the convenience of the generated builder API, documentation cleanness, and higher level of compile-time checks by default.

If you'd like to improve your compile times, consider disabling compile-time checks for overwrites of optional members in setters with #[builder(overwritable)]. However, don't rush with that, think carefully about the tradeoff you would make.

Why derive_builder is fastest to compile? ​

The main contributor to compile times of code generated by bon and typed-builder is their usage of generics to represent the typestate. Builders generated by derive_builder don't use any generics.

The main downside of derive_builder's approach is that its finishing build() method always returns a Result, because it needs to validate, that all required fields were set at runtime.

Is this all worth compilation time overhead? ​

It depends on your use case.

If you are considering builders in your crate's public API, then it's definitely worth it. One of the main focus areas of bon is breaking change prevention (i.e. Compatibility) and builder API ergonomics. This way you can provide stable, evolvable and convenient API for your users.

If you are considering builders in your private modules, then the main feature of bon you'd be interested in is optional members and nicer syntax.

The general rule is that builders make more sense for pervasive structs and functions that you have to construct/call frequently. Don't blindly add builders to every other struct or function. If that struct or function is used only in a single place in your code, then using a builder for it may be unnecessary.

Hardware ​

The benchmarks were run on a dedicated root server AX51-NVMe on Hetzner.

  • OS: Ubuntu 26.04.1 (Linux 7.0.0-38-generic)
  • CPU: AMD Ryzen 7 3700X 8-Core Processor (x86_64)
  • RAM: 62.8 GiB
  • Rust: 1.99.1

References ​

The source code of the benchmarks is available here.