Guide

Incremental builds

ssg --incremental rebuilds only the pages a change can reach. It is on by default in --watch, where the alternative is a full build on every save.

Everything here rests on one rule: an uncertain dependency means a full build. A graph that misses an edge produces a stale page and a green build, which is a worse failure than a slow one. Every case below that ends in "the whole site" is that rule being applied, not a limitation waiting to be fixed.

Turning it on

1ssg --watch                  # incremental, because a person is waiting
2ssg --incremental            # a one-shot build that reuses the last one
3ssg                          # a full build

A one-shot build is full unless asked, because a build nobody is waiting on should be the simple one.

What it actually saves, and what it does not

On a 5 000-post corpus, editing one post, --incremental renders 268 pages instead of 5 251. The wall clock moves less than that ratio suggests, and the reason is worth knowing before you turn it on expecting more:

Phase of an incremental build after one edit Time Share
Loading content 800 ms 59%
Generating site 300 ms 22%
Sitemap and robots 240 ms 17%
Everything else ~30 ms 2%

Rendering pages is the only phase the graph narrows, and it is around a fifth of the build. Loading content reads and parses every page whether or not that page will be written, because an archive or a listing may show any page's body, and no dependency graph can remove that.

So the honest summary at 5 000 posts: --incremental saves most of the render phase and about a tenth overall. It is worth having in a watch loop, where the alternative is that tenth on every save, and on sites large enough for the render phase to grow past the reading.

Whether the load phase could be remembered instead of redone was measured under #270. Most of it turned out not to be conversion at all, and the answer was to stop doing unnecessary work rather than to cache it — see markdown_cache in CONFIGURATION, which is off by default and says why.

Measure your own site rather than trusting a number from another machine:

1BENCH_INCREMENTAL=1 scripts/bench-build.sh 5000
2ssg --incremental --profile=text        # where your build actually spends time

What a change rebuilds

ssg graph answers that from the last build, without running another one.

1ssg graph                                   # what the last build recorded
2ssg graph content/site/posts/hello.md       # what changing that file rebuilds
3ssg graph content/site/posts/hello.md --json
4ssg graph --dot | dot -Tsvg > graph.svg

With no argument it prints the size of the graph, or the reasons this site's builds cannot be narrowed. With a file it prints the outputs that file reaches, or the reason a change to it is a full build.

The graph holds pages, not aggregates. Listings, feeds and the sitemap are computed from the whole site on every build and are never skipped, so they do not appear in the count.

When a build is full anyway

Situation Why
No previous build, or a graph from an older ssg Nothing to compare against
A template or a partial changed The graph does not model which pages a partial reaches, so it will not guess
The configuration file changed A setting can change anything
A file appeared that the last build never saw A new page reaches archives, feeds, tag listings and the sitemap, and none of those edges exist yet
--clean The output directory is emptied, so there is nothing left to keep
Content comes from MDDB The graph cannot hash a remote collection
External sources are enabled They can change without any file changing
A CMS import contributes pages Those pages have no source file

The last three are properties of the site, not of one build: ssg graph prints them under "This site's builds cannot be narrowed", and --incremental saves nothing until they change.

What it is not

It is not a partial site. Every build loads all the content and computes every aggregate — archives, taxonomies, the sitemap, the site graph — from the full set. Incremental only skips writing pages whose bytes cannot have changed. That is why an incremental build and a full one produce the same tree, and why a test asserts exactly that over a random sequence of edits.

Assets beside a page count as that page's inputs, so replacing an image in place rebuilds the page and refreshes the copy. A stale picture under a green build is the same failure as a stale page.

It is not a replacement for the watch loop's hash check. The watcher still skips a rebuild entirely when nothing changed by a byte. Incremental is what happens after that check says something did.

Where the graph is kept

.ssg-cache/graph/graph.json, beside the other build caches. It is a cache: delete it and the next build is full. ssg cache --namespace=graph --dry lists it; .gitignore already excludes .ssg-cache/.

The file records a schema version. A graph written by a different version of ssg is discarded rather than read, which makes an upgrade cost one full build.

Further reading

What breaks if I change this covers the same feature from a maintainer's side: why the graph says "everything" so often, the replaced-image bug it nearly shipped with, and how the two builds are proven to agree.