Rust Team Member's Four Merged PRs Cut Rustdoc Build Times 33%, Landing in Rust 1.99
A Rustdoc team member traced a Crater-flagged regression to an expensive impl-inlining pass and shipped four PRs averaging a 33% speedup.
Overview
Rustdoc, the tool behind cargo doc that generates the Rust standard library docs and the pages hosted on docs.rs, is roughly a third faster after a series of four pull requests merged into rust-lang/rust in July. Rustdoc team member Noah Lev Bartell-Mangel wrote in a blog post that the changes produced “an average wall-time reduction of 25% (which is a 33% speedup), with up to 40% on some real-world crates like hyper and bitmaps, and up to 60% on microbenchmarks like helloworld.” The improvements are already available on Rust’s nightly channel and, according to the post, “will land on stable in Rust 1.99.”
What We Know
The investigation began with a regression report. According to the blog post, “Last month, Rust release team member @theemathas posted on the Rustdoc Zulip about a strange regression in our latest beta.” The release team’s Crater tool, which according to the post “tests the new version across the public Rust ecosystem” before each stable release, “found Rustdoc to be newly erroring on a crate called indented-blocks,” a minimal crate containing only #![recursion_limit = "8"]. On that crate, Rustdoc “failed with a ‘reached the configured maximum number of stack frames’ error while analyzing an internal-facing trait in core::fmt,” while, in the author’s words, “Rustc successfully finished compilation.”
That divergence between the compiler and the documentation tool drove the author to look deeper. Tracing the failure to collect_trait_impls.rs, the file responsible for inlining trait implementations from external crates, the author found a pass called build_extern_trait_impls that, per the blog post, iterates over every trait impl in a crate’s entire dependency graph and builds a full internal representation of each one before later discarding most of them as irrelevant to the docs being generated. The post describes the resulting cost: “the algorithm’s complexity is linear in the number of trait impls throughout your entire dependency graph, with a large constant factor since build_impl is a rather involved function.”
The first of the four merged pull requests, #159623, addressed that cost directly. Its description states: “Building inlined impls is expensive, and most of them end up being unneeded and stripped later in this function. So we should filter them ahead of time.” Restructuring the pass to filter before building representations, rather than after, produced what the blog post reports as “an average wall-time improvement of 20%,” along with a 12% reduction in maximum resident-set size, a measure of peak memory use. The PR was merged into rust-lang/rust on July 22, 2026.
Two more pull requests followed within days, each targeting a narrower inefficiency in the same code path. PR #159721, merged July 23, stopped Rustdoc from inlining implementations for Rust’s built-in primitive types — such as usize and str — in every crate regardless of whether that crate declared any primitives itself. The PR notes: “Since most crates do not define #[doc(primitive)]s, this is a bunch of wasted work that is done in every crate.” According to the blog post, restricting the pass to local primitives improved wall time by 12% and max RSS by 6% on average. PR #159779, merged July 24, applied a similar filter to “synthetic impls” — the auto-trait and blanket-impl representations Rustdoc constructs on the fly for traits like Send and Sync. The PR’s description says the prior code “would skip synthesizing impls if the trait was not documented, but it would still synthesize even if the type was not documented. This is wasted work.” The blog post reports this change reduced wall time by 6% on average and “up to 13% on some real-world crates like hyper.”
The fourth and final pull request, #159854, merged July 29, came after the author examined flamegraph profiles generated following the first three changes and found that a single Rustc query, param_env, was “accounting for a remarkable 50% of the time taken by the build_extern_trait_impls pass,” according to the blog post. The PR’s description explains the fix: “We only care about whether the self type is a generic or an item (inlined) in the current crate, so we don’t actually need to compute the param_env, which is expensive when done to every external impl.” By deferring that computation and examining only the “head” of a type’s self type first, the change yielded what the blog post describes as “an average 18% wall-time speedup across the benchmark suite, including real-world crates like clap_derive.”
All four pull requests were authored by the same GitHub account, camelid, whose public profile describes the account holder as a “Rust Compiler and Rustdoc team member,” consistent with the blog post’s identification of its author as a Rustdoc team member.
What We Don’t Know
The blog post does not give an exact date for when Rust 1.99 itself is expected to ship, only that the Rustdoc improvements are present on nightly now and are slated for that stable release. The post also does not quantify how the fixes affect documentation-build times for the standard library itself, focusing its reported figures on the benchmark suite and specific crates named — hyper, bitmaps, helloworld, and clap_derive.
Analysis
The fix illustrates a debugging pattern common in large compiler codebases: a tooling regression that looked cosmetically minor — one crate failing to build docs under an unusually low recursion_limit — surfaced a structural inefficiency affecting every crate’s documentation build. The author’s blog post frames the underlying lesson as much about process as code, writing, “Just because a piece of code… has been a certain way for a long time doesn’t mean that it’s optimal or even correct.” Along the way, deleting what turned out to be dead filtering logic also surfaced a separate, unrelated bug: the impl-inlining code, per the post, “never considered notable-trait status when making its decisions,” a gap that had been masked by a GUI test passing for the wrong reason involving Vec<u8>’s Deref chain to Write.