1. Isn’t make Fast Enough?
After a few years of using make, have you ever noticed this phenomenon:
make -j8
You have 8 parallel jobs running, the CPU is saturated, and yet make still “hangs” for a few seconds at startup before it really starts compiling.
What is make doing during those few seconds?
It is building the dependency graph — parsing the Makefile to work out who depends on whom, and who gets compiled before whom. A Makefile is a text format, so make re-parses it from scratch on every startup. The bigger the project, the longer that parsing takes.
What’s more, make’s reasoning about the dependency graph is rather conservative — even when there is no ordering dependency between A and B, make often runs them one after the other, because it has no global view.
Ninja goes straight at both of these pain points: faster startup, more aggressive parallelism.
2. Ninja’s Design Philosophy
Ninja’s slogan is refreshingly plain: “dedicated to making your compiles faster”.
Its design idea is simple:
- Build files are not meant to be written by people; they are generated by programs. Ninja’s
.ninjafile format is extremely lean, with almost no syntactic sugar - The codebase is tiny, so parsing is fast enough to be essentially negligible. The few seconds make spends “staring into space” become milliseconds in Ninja
- Its parallelism decisions are more precise, making the most of a multi-core CPU
Ninja is not positioned to replace CMake but to replace make. You can think of Ninja as a back-end engine that “starts builds faster and runs them faster”.
CMakeLists.txt → cmake -G Ninja → build.ninja → ninja runs
3. Generating Ninja Build Files with CMake
There is almost nothing extra to learn. You only have to pick a different generator when you run cmake:
mkdir build && cd build
cmake .. -G Ninja
ninja
Exactly the same usage as cmake .. && make. Not a single line of CMakeLists.txt needs changing.
If you haven’t installed Ninja yet:
# Ubuntu / Debian
sudo apt install ninja-build
# macOS
brew install ninja
Once it is running you feel the difference immediately — Ninja barely pauses at all; it starts compiling right away.
4. What build.ninja Looks Like
You normally do not need to write .ninja files directly, but a look at the structure helps explain why it is so fast:
# Variables
cflags = -Wall -g
# rule: defines one way of "how to compile"
rule cc
command = gcc $cflags -c $in -o $out
description = CC $out
# build: describes "which rule, from what, produces what"
build main.o: cc main.c
build utils.o: cc utils.c
# Linking rule
rule link
command = gcc $in -o $out
description = LINK $out
build myapp: link main.o utils.o
# Default target
default myapp
A rule defines the template for a compiler command; a build statement declares a concrete compilation task and its dependencies. The format is extremely lean, with nothing fancy in it — which is exactly why parsing is so fast.
5. Ninja vs Make: A Speed Comparison
Take a medium-sized C project (about 500 source files) and a pure incremental build (changing nothing at all):
# make
time make -j8
# real 0m3.2s
# ninja
time ninja
# real 0m0.3s
Ninja’s incremental build (no-op build) is nearly 10 times faster than make. Accumulated over a day of repeatedly editing, compiling and testing, those 3 seconds save you a lot of time.
For a full build the gap is not that large, but Ninja’s advantage in parallel scheduling still makes it a little faster. With a caching tool such as ccache, Ninja’s no-op advantage becomes even more obvious.
6. Ninja’s Limitations
Fast as it is, Ninja is not a universal solution:
- Not suited to being written by hand:
.ninjafiles are extremely tedious. You will never want to write one yourself - It needs a high-level build system alongside it: Ninja is only an execution engine; you still need CMake (or GN, Meson) to generate the build files
- No conditionals, functions or other high-level features: those are the generator’s (CMake’s) responsibility; Ninja only executes
You can think of it this way: make is both a build description language and a build execution engine. Ninja separates the two roles — you describe the build logic with CMake and Ninja executes it, each minding its own business.
Modern large C++ projects (Chromium, LLVM) almost all use Ninja as their build back end.
7. When to Use Ninja
| Scenario | Recommendation |
|---|---|
| Small project (< 10 source files) | make is enough; no need to bring in Ninja |
| Medium project (10-100 source files) | Ninja’s startup speed is noticeable, but the difference is small |
| Large project (100+ source files) | Ninja is strongly recommended — the time saved is very substantial |
| Continuous integration (CI) | Ninja is a must — a CI environment builds from scratch every time |
| Used together with CMake | Switching costs almost nothing: cmake -G Ninja gets you there in one step |
If you are already using CMake, switching to Ninja costs you almost nothing — change one generator argument and you get faster builds. Why not?
In the next article we will meet a heavyweight build system from Google — Bazel. It is said to handle monorepo projects with tens of thousands of source files; what design philosophy is behind it?
A small step forward every day is a new height!