Tutorial2 days ago

2. What Zensical Does at 203 Pages

We built 220,400 words with it. Going from 3 pages to 203 made the wall clock go down, and the search index it ships to your readers is 1.4 MB.

The WJS Desk

Sep 22, 2026 · updated 2 days ago · 7 min read

Photo by William Warby on Pexels

A three page demo site tells you nothing. Every static site generator is fast on three pages. The question is what happens at the size of a real documentation set, and what that costs the people loading it.

So we generated 200 pages of markdown, 220,400 words in total, added them to the project from part 1, and measured.

How we built the test corpus

Deterministic, so it can be reproduced:

import os, random
random.seed(11)
words = "the quick brown fox jumps over a lazy dog while the "\
        "system builds pages and measures throughput".split()
os.makedirs('docs/gen', exist_ok=True)
for i in range(200):
    body = []
    for s in range(6):
        body.append(f"## Section {s+1}\n")
        for p in range(3):
            body.append(' '.join(random.choice(words)
                        for _ in range(60)) + "\n")
    open(f'docs/gen/page-{i:03d}.md', 'w').write(
        f"# Generated page {i}\n\n" + "\n".join(body))

That produces 200 pages, each with six sections and eighteen paragraphs. Plus the three scaffolded pages, the site is 203 pages and 220,400 words. For scale, that is a substantial documentation set: larger than most project docs, smaller than something like the Python standard library reference.

The numbers

BuildReportedWall clock
3 pages, cold0.29 s1,711 ms
203 pages, cold1.37 s1,573 ms
203 pages, one file changed0.61 s766 ms
3 pages, nothing changedn/a329 ms

Read the first two rows again, because the result is counterintuitive. Going from 3 pages to 203 pages made the wall clock time go down.

That is not a measurement error, and it is not zensical getting faster with more work. It is variance in a fixed overhead that dwarfs the actual build. Python startup, loading the extension module and process teardown cost roughly 1.2 to 1.4 seconds regardless of what you are building. The Rust core went from 290 ms to 1,370 ms, which is a real and roughly linear increase, but it is happening underneath a constant that is larger than it.

What this means in practice. At this scale you are not waiting for zensical, you are waiting for Python to start. That has a pleasant consequence: the tool will feel identically fast on a 50 page site and a 500 page site, because the part that scales is not the part you notice. It also means the reported build time and the time you experience will keep diverging as sites get smaller, and the smaller your site the more misleading that printed number is.

On throughput: 203 pages of rendered markdown in 1.37 seconds of actual work is about 148 pages per second, or roughly 160,000 words per second. That is genuinely quick, and it is what the Rust core buys.

Incremental builds

The number that matters day to day is not the cold build, it is what happens when you change one file and the dev server rebuilds.

We appended a blank line to one of the 200 generated pages and rebuilt: 0.61 s reported, 766 ms wall, against a cold build of 1.37 s reported. So it is doing real incremental work rather than rebuilding everything, but it is not doing single-page work either. Roughly 45% of a cold build for a one line change.

For comparison, a no-change rebuild on the small site was 329 ms wall, which is essentially the fixed overhead with nothing to do.

Our read: incremental is implemented and helps, but it is not the near-instant single-file rebuild some generators manage. At this size you will not care. At ten times this size it might be the thing you notice.

The one number to take away

If you read nothing else in this part, read this comparison, because it reframes what "fast static site generator" is actually buying you at documentation scale:

3 pages203 pagesChange
Rust core does the work in290 ms1,370 ms4.7x slower
You wait1,711 ms1,573 msSlightly faster
Pages per second of real work1014814.8x better

The middle row is what a user experiences and it did not change. The top row is what the engineering actually did and it scaled almost linearly. The bottom row shows the fixed overhead being amortised: at three pages you are paying 1.4 seconds of startup to do 290 ms of work, which is a terrible ratio, and at 203 pages the same startup buys you 68 times more output.

This is why "how fast is it" is the wrong question and "how fast is it at my size" is the right one. On a small site zensical will feel exactly as fast as anything else, because you are timing Python. On a large one the difference is real.

What it outputs, and what that costs your readers

This is the part nobody benchmarks and everybody ships.

ArtifactSize
Whole site, 203 pages6.6 MB
search.json1,383 KB
JavaScript bundleone minified file
Stylesheetstwo themes, classic and modern

The search index is 1.4 MB for 203 pages, and that is the number to think hard about. Zensical ships client side search, which means the index is downloaded by the browser. It is fetched lazily by a web worker rather than blocking the page, so it is not as bad as 1.4 MB of render-blocking payload, but it is still 1.4 MB crossing the network for anyone who uses the search box.

Scaled linearly, a 1,000 page documentation site would ship roughly a 7 MB search index. Whether that is acceptable depends entirely on your audience, and it is a question worth answering before you commit rather than after someone on a phone complains.

The output also includes sitemap.xml and objects.inv, the second being the Sphinx style inventory file that lets other projects cross-reference yours. Getting that for free is a genuinely thoughtful touch for documentation.

The two CSS bundles

Worth noticing in the file list: it emits stylesheets/classic/ and stylesheets/modern/, two complete theme variants. The config has a commented variant = "classic" line controlling which one you get.

Both ship regardless. That is a few tens of kilobytes of CSS your readers download and never use, which is minor next to the search index but is the kind of thing that tends to get fixed as a project matures, and has not been yet.

How to measure this yourself

Do not trust our numbers on your hardware or your content. The generator script above is fifteen lines, and timing it properly takes one more:

start=$(python3 -c "import time;print(time.time())")
zensical build
end=$(python3 -c "import time;print(time.time())")
python3 -c "print(f'wall: {($end-$start)*1000:.0f} ms')"

Time the wall clock, not the number the tool prints. That gap was the most useful thing we learned in this part, and it applies to every build tool with an interpreted wrapper around a compiled core.

What we did not test, and would want to before committing

Being explicit about the edges of this benchmark, because a number without its conditions is not much of a number.

One machine. Everything here is a single Apple silicon Mac. The fixed Python startup cost in particular will look different on other hardware, and on a slow CI runner it could easily dominate more than it does here.

Synthetic content. Our 200 pages are generated prose with headings. Real documentation has code blocks, tables, admonitions, images and cross references, all of which cost more to render than plain paragraphs. Treat 148 pages per second as an optimistic ceiling rather than what you will see.

No images. We shipped no image assets at all, so the 6.6 MB output is text, CSS and JavaScript only. A real documentation site with screenshots will be dominated by those instead, and none of that passes through the markdown renderer.

Cold caches, mostly. We deleted the output directory before the cold builds but did not control for filesystem caching between runs.

The comparison we can and cannot make

The obvious question is how this compares to MkDocs with Material, which is what most of the audience is already running. We are not going to give you that number from this test, for a reason worth stating: our 200 generated pages are not a fair MkDocs corpus either, and running two tools on synthetic content and declaring a winner would be the kind of benchmark we complain about in other people's articles.

What we can say from this data is narrower and still useful. The Rust core does 203 pages of markdown in 1.37 seconds and the whole command takes 1.57 seconds, on a project where the entire dependency footprint is one 13.7 MB wheel. If your current build is slow enough that you have noticed it, those are numbers worth testing against your own content.

The honest way to find out takes ten minutes: copy your real docs/ directory into a zensical project, build, and time it. Part 3 covers whether that copy will even work, and it mostly will.

What we would watch

  • The search index, above everything. It is the only output that scales into genuinely problematic territory, and it is the one your readers pay for rather than you.
  • Incremental at much larger sizes. 45% of a cold build for a one line change is fine at 203 pages and would be irritating at 2,000.
  • Fixed startup cost. Harmless interactively, less harmless if you call build in a tight loop from a script.

Next

Part 3 is the question most people reading this actually have: it is from the Material for MkDocs team, so can it read your existing MkDocs project? We tried, and the answer is yes with one silent and significant exception.

Share

We built 203 pages with Zensical. Going from 3 pages to 203 made wall clock time go DOWN, because Python startup dwarfs the Rust core. #StaticSite #Performance #Rust

Never miss a ship

The best stuff that shipped this week, delivered every Thursday. Free, no spam. We read all the boring stuff so you get the fun parts.

Keep reading