Speeding Up C++ Build Times | Figma Blog (opens in new tab)
Figma cut C++ build times roughly in half by addressing unnecessary header inclusion rather than relying solely on faster hardware or caching. The team found that compiled bytes were growing much faster than the codebase itself, making transitive header dependencies the main culprit. They combined automated include analysis with CI-based measurement to prevent both unused includes and costly dependency regressions.
Why Build Times Were Getting Worse
- In 2023, Figma’s codebase grew by about 10%, but build times increased by 50%.
- C++ builds were a major productivity problem and a top concern in internal developer surveys.
- Faster M1 Max machines, Ccache, and remote caching provided only temporary or insufficient improvements.
- The team observed that build times were closely related to the amount of code passed to the compiler after preprocessing.
How C++ Header Inclusion Affects Builds
- The preprocessor expands every
#includeinto a single large file before compilation. - Transitive dependencies are included as well:
- If file C includes B, and B includes A, C receives the contents of both A and B.
- As a result, a small source change can cause the compiler to process a very large amount of unrelated code.
Removing Unnecessary Includes
- Figma suspected that many files included headers they did not use directly or relied on headers only for transitive dependencies.
- Removing unnecessary includes from the largest files produced:
- A 31% reduction in compiled bytes.
- A 25% reduction in cold build time.
- These results confirmed that compiled byte volume was strongly correlated with build performance.
DIWYDU: Automating Include Cleanup
- Google’s Include What You Use (IWYU) tool was considered but proved difficult to apply retroactively to Figma’s large codebase.
- Figma created a less strict alternative called Don’t Include What You Don’t Use (DIWYDU).
- DIWYDU:
- Uses Python bindings for
libclang. - Parses source and header files into Clang Abstract Syntax Trees.
- Identifies types, functions, and variables directly used by each file.
- Flags headers that are included but provide no directly used symbols.
- Uses Python bindings for
- The tool runs on feature branches to prevent unnecessary includes from accumulating.
DIWYDU’s Limitations
- It analyzes Figma-owned files but excludes Standard Template Library headers.
- STL headers may define symbols through private internal includes, making direct dependency analysis difficult.
- Python’s
libclangbindings expose less of Clang’s AST than the compiler’s native C++ APIs, sometimes producingUNEXPOSED_EXPRnodes. - A future C++ implementation could provide more accurate AST access.
- DIWYDU cannot detect cases where an included header is genuinely required but excessively large.
- Such regressions may need forward declarations or header decomposition instead.
Measuring Dependency Growth with includes.py
- Figma built
includes.pyto measure the transitive bytes associated with each source file. - The tool is written entirely in Python and typically runs in a few seconds without invoking Clang.
- It:
- Crawls first-party source, header, and generated files.
- Counts file sizes.
- Builds a dependency graph.
- Estimates the total bytes passed to the compiler for each source file.
- Standard library includes are treated as zero bytes because Figma mainly accesses them through internal wrapper directories.
- CI uses the measurements to compare pull requests and warn authors when changes significantly increase compiled bytes.
Figma’s approach demonstrates that controlling header dependencies can deliver larger and more durable gains than simply adding hardware or cache capacity. Teams working on large C++ codebases should automate unused-include checks, measure transitive dependency size in CI, and use forward declarations or smaller headers when necessary.