
Compile times grow gradually until one day they quietly become a problem.
And when they do, you quickly realize that improving them pays off in two ways:
less CPU time wasted and faster developer iteration.
One of the simplest and most effective techniques for reducing compilation time is precompiled headers (PCH).
Let’s walk through how they work and how to use them properly with CMake.
Why Precompiled Headers Help
The C++ preprocessor processes each translation unit independently.
If a header is included in many .cpp files, it will be parsed again and again, every time.
For example, if Foo.h is included by:
Foo.cpp– directly,Bar.cpp– transitively viaBar.hmain.cpp– also transitively viaBar.h
…it will be reprocessed three times.

Precompiled headers solve this by parsing the header once, storing an intermediate representation, and reusing it across translation units.
The more widely-used a header is, the bigger the gain.
Precompiled Headers in CMake
The good news: modern CMake, GCC, and Clang support PCH out of the box.
The key command is:
target_precompile_headers(<target> <INTERFACE|PUBLIC|PRIVATE> [header1...]
[<INTERFACE|PUBLIC|PRIVATE> [header2...] ...])
This tells the compiler which headers should be precompiled when building a specific target.
Different targets may have different PCH sets — or they can reuse a common one.
CMake even supports sharing precompiled headers between targets:
target_precompile_headers(<target> REUSE_FROM <other_target>)
A Dedicated Target for Precompiled Headers
Although you can define a list of precompiled headers individually for each target, the greatest benefit uWhile you can define PCH per target, the most efficient approach is to create a single dedicated target that:
- Compiles an empty source file
- Lists all headers to precompile
- Holds all required preprocessor definitions
- Is reused by other targets
This avoids duplication and ensures consistency across the project.
Step 1 — Create the PCH target
add_library(MyPrecompiledHeaders STATIC dummy.cpp)
dummy.cpp is an empty file — it simply forces the target to compile so that the PCH can be generated, while headers are precompiled only during the target build.
Step 2 — Define preprocessor directives
Some libraries (e.g., nlohmann_json) require stable preprocessor definitions for PCH reuse:
target_compile_definitions(MyPrecompiledHeaders PUBLIC
JSON_USE_IMPLICIT_CONVERSION=0
JSON_DIAGNOSTICS=0
)
These must be consistent across all translation units that reuse the PCH, thus mark them as PUBLIC.
Step 3 — Add headers to precompile
target_precompile_headers(MyPrecompiledHeaders PUBLIC
<string>
<tuple>
<map>
<nlohmann/json.hpp>
)
You should precompile only widely-used and heavy headers.
Step 4 — Link necessary libraries
The compiler must be able to locate the headers:
target_link_libraries(MyPrecompiledHeaders PRIVATE
nlohmann_json::nlohmann_json
)
Reusing Precompiled Headers
Now other targets can reuse the PCH::
target_link_libraries(foo PRIVATE MyPrecompiledHeaders)
target_precompile_headers(foo REUSE_FROM MyPrecompiledHeaders)
All preprocessor definitions and include paths propagate automatically to the target foo.
One limitation:
You can reuse PCH from only one target:
# ❌ Invalid — cannot reuse from multiple targets
target_precompile_headers(foo REUSE_FROM bar)
target_precompile_headers(foo REUSE_FROM zoo)
A Note About Clang-Tidy
Clang-Tidy does not benefit from precompiled headers. In fact, using PCH may slow it down significantly. Keep this in mind when optimizing CI pipelines.
Real-World Impact
In one of my projects, introducing a shared PCH target reduced overall compilation time by 25% — with no changes to the C++ code itself, only to the build configuration.
If you haven’t tried PCH in CMake yet — do it.
⭐ Want more CMake insights?
If you’re improving your build performance, the next step is understanding how to structure your CMake targets properly.
I’ve put together a free guide that explains:
- how modern CMake’s target model works
- why global configuration slows your builds
- how to isolate targets for faster incremental compilation
- and how PUBLIC / PRIVATE / INTERFACE actually behave