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.

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

👉 Download the Modern CMake Targets Guide (Free PDF)