Precompiled headers in CMake? Easy peasy, lemon squeezy

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 via Bar.h
  • main.cpp – also transitively via Bar.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:

  1. Compiles an empty source file
  2. Lists all headers to precompile
  3. Holds all required preprocessor definitions
  4. 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.

Don’t guess which headers are expensive. Measure first. I covered how to find the headers and translation units that actually dominate your build time in How to Find and Fix C++ Compile-Time Bottlenecks.

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.

PCH is most useful when a heavy header genuinely needs to be included across many translation units.

But sometimes the better optimization is to avoid including that header in the first place. Forward declarations, moving implementation details into .cpp files, and PIMPL can significantly reduce transitive dependencies. I cover those techniques in How Header Dependencies Kill C++ Build Times.

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 to Understand Modern CMake Beyond PCH?

Precompiled headers are just one example of why understanding targets and their dependencies matters in modern CMake.

If PUBLIC, PRIVATE, INTERFACE, usage requirements, and target-based configuration still feel a bit fuzzy, I put together a free practical guide that explains the underlying model:

👉 Modern CMake Becomes Simple Once You Understand How Targets Work

It shows how to structure clean, isolated targets, manage dependencies and include paths, and avoid the global configuration that makes CMake projects difficult to maintain.