Figma — Speeding Up C++ Build Times¶
Summary¶
Figma's Core team retrospective on cutting C++ cold-build times ~50% after a
year in which the codebase grew 10% but build times grew 50%. Hardware
(M1 Maxs), Ccache, and remote caching were insufficient. The structural
observation: C++ build times are roughly proportional to the number of
bytes the compiler sees after pre-processing, and that number was growing
disproportionately to source code — a signature of include bloat. Figma
built two custom tools on top of libclang + its own transitive-byte
accounting, plus a codebase-wide Fwd.h-per-directory pattern, and wired
them into CI. 31% drop in compiled bytes and 25% drop in cold build time
from an initial one-shot include cleanup of the largest files; full
tooling now prevents 50-100 would-be slowdowns/day.
Key takeaways¶
- Post-pre-processing bytes is the right cost proxy for C++ build time,
not LOC. In C++ compilation,
#includedirectives transitively flatten all referenced header content into a single mega-file that the compiler then parses. If C includes B and B includes A, C's compile unit contains all of A's bytes. Figma's diagnostic signal was the ratio of bytes sent to compiler / lines of code added growing unboundedly. See concepts/c-plus-plus-compilation-model. (Source: article pre-processing explainer.) - Removing unnecessary includes from the largest files alone delivered 31% compiled-byte reduction and 25% cold-build-time reduction. Proof that the bytes-proportional-to-build-time model was correct and that unnecessary includes were a dominant contributor. (Source: article "initial cleanup" results.)
- DIWYDU (Don't Include What You Don't Use) — a Figma
tool, deliberately looser than Google's
IWYU. IWYU demands the exact minimal
include set (provably correct but hard to retrofit); DIWYDU demands only
that every included header be directly used. Built on
libclang's Python bindings;
walks the AST of each source + header file, records which symbols come
from which includes, flags includes whose symbols are never directly
referenced. Runs on feature branches. Explicit scope: skips STL headers
(private includes defeat both DIWYDU and IWYU) and sometimes can't
resolve
UNEXPOSED_EXPRAST nodes because libclang's Python bindings ride the C binding, not the full C++ Clang API — a known precision limitation. (Source: article DIWYDU section.) - Include bloat has a second failure mode DIWYDU doesn't catch: a
used header that pulls in a huge transitive tree. The include is
genuinely used, so DIWYDU can't flag it; but adding it may send
megabytes of extra bytes to the compiler. Fix = concepts/forward-declaration
or splitting the header. Detection = measure the delta in transitive
bytes. (Source: article motivation for
includes.py.) includes.py— a pure-Python (no Clang) static byte counter over the include graph, fast enough (seconds) to run in CI on every PR. Crawls all first-party header/source files, treats standard-library includes as 0 bytes (safe for Figma: stdlib usage is gated to one wrapper directory), builds the include DAG, sums per-file + transitive bytes. The tool feeds CI policy: warn on PRs whose byte-delta per source file is significant, block merge until addressed. Canonical instance of a patterns/ci-regression-budget-gate — cost is measured, not assumed, and the gate stops regressions at authoring time. (Source: articleincludes.pysection.)Fwd.hper directory — patterns/centralized-forward-declarations. When the fix to anincludes.pyregression is to forward-declare a symbol instead of including its header, the naive approach (copy thestruct Foo;declaration into every file that needs it) hurts searchability and readability. Figma's directory-granular solution: oneAnimalFwd.hper directory listing every forward declaration that other files in the directory need, included from every header in the directory. Critically: source files never includeFwd.h(forward declarations only help in headers where they prevent transitive includes). Result: individual engineers don't have to think about forward-declaration discipline — it's a directory-level default. (Source: article "Fwd.h" section +AnimalFwd.hcode sample.)- Bazel remote caching trimmed ~2+ min off local builds when the cache hits. Adopted with "make-sense" gating — only use remote cache for local builds when likely to hit. This is a classic concepts/content-addressed-caching add-on, complementary to but not a replacement for the bytes-reduction work. The order matters: reduce first, then cache. (Source: article "other improvements" section.)
- Reported end-to-end outcome. Build times cut ~50%. CI rejects / warns on 50-100 regressions per day that would otherwise have silently slowed builds. (Source: article conclusion.)
Numbers¶
- Codebase growth vs build-time growth (2023): 10% code / 50% build time.
- Initial one-shot cleanup (largest files, unnecessary includes): −31% compiled bytes, −25% cold build time.
- End state after full tooling rollout: −50% build time.
- CI gate throughput: 50-100 would-be regressions prevented / day.
- Bazel remote cache: >2 min off local builds when cache hits.
includes.pyruntime: "usually in just a couple of seconds" — fast enough to run in CI on every PR.
Architectural ideas¶
- Measure the right thing: bytes, not LOC. The article is an object lesson in identifying the actual cost driver. LOC is cheap to measure and looks roughly correlated; post-pre-processing bytes is the cost. The bytes/LOC ratio growing unboundedly was the tell.
- Two-tool split by detection class. DIWYDU catches the "included but
not directly used" class (solvable by removing the include).
includes.pycatches the "used but enormous" class (solvable by forward-declaration or header splitting). Trying to make one tool do both would merge two different policies — DIWYDU would stay a static-analysis problem andincludes.pywould stay a budget-measurement problem. Clean separation. - Laxer-than-IWYU was a feature, not a shortcut. Figma called IWYU's
precision-mode failure "challenging to apply retroactively to a
substantial codebase" — the strictness forced engineers to debate every
single
#includeline, blocking rollout. DIWYDU's "directly-used is sufficient" rule is migratable incrementally. - CI gate + measurement is the leverage point. DIWYDU +
includes.pyaren't invoked manually by engineers — they're in the CI signal loop. "50-100 regressions/day prevented" is only possible because the cost regression is surfaced before merge, not during a slow-build retrospective. Fwd.hpushes policy to the directory, not the file. Individual authors neither read nor write forward declarations by hand. Centralization per directory is the discipline mechanism — the same shape as DIWYDU's "the tool enforces it, not the engineer."
Caveats / self-reported limits¶
- DIWYDU doesn't analyze STL. Standard-library private includes defeat AST-based header-to-symbol mapping. IWYU has the same problem. For Figma, mitigated because stdlib usage is confined to one wrapper directory.
- libclang Python bindings ride C bindings.
UNEXPOSED_EXPRAST nodes lose type information; DIWYDU falls back to "less elegant solutions." Article flags migrating DIWYDU to C++ (full Clang C++ API) as a possible next step. includes.pyover-counts standard library as 0 bytes. Safe only because Figma wraps stdlib in one directory; would be unsound for projects that use STL directly everywhere. Known assumption.- Forward declaration has readability cost. Too many
Fwd.hentries hurts codebase searchability (a symbol appears in declaration-only form in many places). The directory-localFwd.hbounds this to one place per directory.
Why ingest (Tier-3 gate)¶
Figma is not in AGENTS.md's formal tier lists — treating as Tier-3-equivalent. The post passes the Tier-3 gate on two dimensions:
- Distributed-systems / infrastructure-architecture substance: custom
AST-based static-analysis tool (DIWYDU), custom CI-gated transitive-byte
cost model (
includes.py), codebase-wide convention (Fwd.hper directory). All three are portable design ideas, not feature announcements. - Production outcomes with numbers: 50% build-time cut, 31%/25% one-shot wins, 50-100 regressions/day caught. Real metrics.
Adjacent to the existing build-systems concepts (concepts/build-graph, concepts/content-addressed-caching, concepts/hermetic-build, concepts/remote-build-execution — all from the Canva CI ingest) but operates one layer below: not "how do we distribute the build and cache it" but "how do we shrink what the compiler has to do at all." These compose.
Cross-references¶
- companies/figma — company landing page.
- systems/diwydu, systems/includes-py, systems/include-what-you-use.
- concepts/c-plus-plus-compilation-model, concepts/forward-declaration.
- patterns/centralized-forward-declarations, patterns/ci-regression-budget-gate.
- concepts/content-addressed-caching — Bazel remote cache as the complementary-but-insufficient-alone lever.
Raw¶
- Raw file:
raw/figma/2024-04-27-speeding-up-c-build-times-d4bc0f56.md - Original URL: https://www.figma.com/blog/speeding-up-build-times/
- HN: https://news.ycombinator.com/item?id=40178634 (165 points)
- Tier: 3 (Figma not in AGENTS.md tier list; treated as Tier-3-equivalent)