Summary
For packages with non-trivial APIs, great-docs build spends nearly all of its time in the
Quarto render phase, and that time scales roughly quadratically with the number of API pages.
The cause is the generated _quarto.yml sidebar: one entry per documented object, as a bare
file path. Quarto re-resolves the full sidebar for every page it renders (apparently reading
each target file's metadata to get its title), so an API with N objects pays ~N² metadata reads.
On a real package (86 API objects, 90 pages total, zero executable code cells), the
measured difference is 19m 29s as shipped vs 30.8s with the sidebar removed — everything else
identical.
This is distinct from #172, which covered generation-phase costs (introspection, source-link
generation). Here generation is fast; it's the quarto render step that blows up.
Measurements
All runs on the same 90-page site (macOS, Quarto 1.9.38, great-docs 0.14.0). The site's API
pages contain no executable cells — pure signature/docstring pages.
| Configuration |
quarto render time |
| As generated by great-docs |
19m 29s |
As generated, minus the jupyter: python3 line |
18m 41s |
Same 90 pages, bare project: type: website |
14.6s |
| + the great-docs SCSS theme (9,167 lines) |
15.1s |
| + the three Lua filters (output-title, details, gd-lightbox) |
15.6s |
| Full generated config, render list cut to 10 pages |
4.3s (~0.4s/page) |
| Full generated config, sidebar block deleted |
30.8s |
Full generated config, sidebar entries rewritten to explicit text:/href: |
6m 07s |
Two observations from the table:
- The theme, filters, and engine binding are all essentially free. The sidebar accounts for
~97% of the render time.
- Per-page cost is not constant: ~0.4s/page when 10 pages render, ~12.4s/page when 90 render.
Cost grows with (pages rendered × sidebar entries), i.e. quadratically in API size, so this
gets dramatically worse for larger packages.
Suggested fixes
- Emit sidebar entries as explicit
text: + href: pairs instead of bare paths.
great-docs already knows every page's title at generation time. Measured effect: 3.1×
(19.5m → 6m). Low-risk, no UX change.
- Offer a compact-sidebar option (sections link to per-section index pages instead of
listing every method). Measured upper bound: ~36× (19.5m → ~31s). Changes navigation UX,
so presumably opt-in.
Even with explicit titles Quarto still pays substantial per-page × per-entry cost (6m is much
better than 19.5m but far from 31s), so the residual may be worth raising with quarto-cli —
happy to file there too if useful.
Side note
jupyter: python3 is written into _quarto.yml unconditionally, binding every page to the
Jupyter engine even when no page in the site has executable cells. At project scale it's minor
(48s here), but it adds ~20s of kernel startup to every single-page render, which hurts
iterating on one page. Only writing the key when executable content exists (or letting users
set jupyter: false) would avoid it.
Environment
- great-docs 0.14.0
- Quarto 1.9.38
- macOS (Darwin 25.5), Python 3.13
- Package: internal library, 86 documented objects / 90 site pages, no executable code cells
Summary
For packages with non-trivial APIs,
great-docs buildspends nearly all of its time in theQuarto render phase, and that time scales roughly quadratically with the number of API pages.
The cause is the generated
_quarto.ymlsidebar: one entry per documented object, as a barefile path. Quarto re-resolves the full sidebar for every page it renders (apparently reading
each target file's metadata to get its title), so an API with N objects pays ~N² metadata reads.
On a real package (86 API objects, 90 pages total, zero executable code cells), the
measured difference is 19m 29s as shipped vs 30.8s with the sidebar removed — everything else
identical.
This is distinct from #172, which covered generation-phase costs (introspection, source-link
generation). Here generation is fast; it's the
quarto renderstep that blows up.Measurements
All runs on the same 90-page site (macOS, Quarto 1.9.38, great-docs 0.14.0). The site's API
pages contain no executable cells — pure signature/docstring pages.
quarto rendertimejupyter: python3lineproject: type: websitetext:/href:Two observations from the table:
~97% of the render time.
Cost grows with (pages rendered × sidebar entries), i.e. quadratically in API size, so this
gets dramatically worse for larger packages.
Suggested fixes
text:+href:pairs instead of bare paths.great-docs already knows every page's title at generation time. Measured effect: 3.1×
(19.5m → 6m). Low-risk, no UX change.
listing every method). Measured upper bound: ~36× (19.5m → ~31s). Changes navigation UX,
so presumably opt-in.
Even with explicit titles Quarto still pays substantial per-page × per-entry cost (6m is much
better than 19.5m but far from 31s), so the residual may be worth raising with quarto-cli —
happy to file there too if useful.
Side note
jupyter: python3is written into_quarto.ymlunconditionally, binding every page to theJupyter engine even when no page in the site has executable cells. At project scale it's minor
(48s here), but it adds ~20s of kernel startup to every single-page render, which hurts
iterating on one page. Only writing the key when executable content exists (or letting users
set
jupyter: false) would avoid it.Environment