Benchmarks
TL;DR
60 rows, 33 of them gated: a gated row over its budget fails the build. Today none is. Version 0.1.4 on Bun 1.4.0, taken 2026-09-15, tokens counted with o200k_base.
23 ms
MCP cold start, release binary (source)
510 tokens
to read one page as markdown (source)
0 KB
JavaScript on the page (source)
How to read this page
Every row is a value, a budget and a note saying how the value was taken. A budget is a design statement — the number the product promises, written in snypd.yaml under bench.budgets so a site can tighten it. CI passes a timed row at 80 % of its budget, because a runner is noisy and a promise met on a bad afternoon is the promise; a counted row — tokens, kilobytes, actions — passes at the budget exactly. report rows have no budget: they are published so the next decision has a number, and a budget is set from them once there is one worth setting. The numbers are CI's, not a laptop's; the record this page is generated from is committed on every run that changes it.
Build
A cold build from nothing — no dist/, no index — at three sizes, and the incremental build a single edit costs. parse rows are the share of each build spent turning markdown into the typed tree.
| Metric | Value | Budget |
|---|---|---|
build.cold.100 | 292.5 ms | 2000 ms ✅ |
build.cold.100.parse | 222.3 ms | report-only |
build.cold.100.parse.bun | 2.3 ms | report-only |
build.cold.1000 | 2748 ms | 20000 ms ✅ |
build.cold.1000.parse | 2161.2 ms | report-only |
build.cold.1000.parse.bun | 22.8 ms | report-only |
build.cold.10000 | 27720.5 ms | 200000 ms ✅ |
build.cold.10000.parse | 21751.3 ms | report-only |
build.cold.10000.parse.bun | 232.8 ms | report-only |
build.incremental.100 | 13.1 ms | 300 ms ✅ |
build.noop.100 | 9.3 ms | report-only |
How each was taken:
build.cold.100— 113 routes, no dist, no index · 2.36 ms an item over 124 · config 3 · theme 0 · sync 6 · plan 4 · render 279 = stat 0 + parse 222 + html 30 + write 15 + weigh 9 + index 1 msbuild.cold.100.parse— report-only (F1): micromark + the typed tree, 76 % of the build — with html at 10 %, the CPU a worker pool could split; write 5 %, index 0 % and weigh 3 % it could notbuild.cold.100.parse.bun— report-only (S20):Bun.markdown.htmlover the same 100 sources (0.27 MB) — 98× under the parse row, 119 MB/s. A renderer, not a parser: no tree, no positions, no directives, Bun-only — the ceiling, not a candidate (decision 168)build.cold.1000— 1013 routes, no dist, no index · 2.68 ms an item over 1024 · config 3 · theme 0 · sync 38 · plan 26 · render 2680 = stat 0 + parse 2161 + html 244 + write 184 + weigh 76 + index 9 msbuild.cold.1000.parse— report-only (F1): micromark + the typed tree, 79 % of the build — with html at 9 %, the CPU a worker pool could split; write 7 %, index 0 % and weigh 3 % it could notbuild.cold.1000.parse.bun— report-only (S20):Bun.markdown.htmlover the same 1000 sources (2.73 MB) — 95× under the parse row, 120 MB/s. A renderer, not a parser: no tree, no positions, no directives, Bun-only — the ceiling, not a candidate (decision 168)build.cold.10000— 10013 routes, no dist, no index · 2.77 ms an item over 10024 · config 6 · theme 1 · sync 393 · plan 286 · render 27035 = stat 1 + parse 21751 + html 2674 + write 1793 + weigh 673 + index 92 msbuild.cold.10000.parse— report-only (F1): micromark + the typed tree, 78 % of the build — with html at 10 %, the CPU a worker pool could split; write 6 %, index 0 % and weigh 2 % it could notbuild.cold.10000.parse.bun— report-only (S20):Bun.markdown.htmlover the same 10000 sources (27.26 MB) — 93× under the parse row, 117 MB/s. A renderer, not a parser: no tree, no positions, no directives, Bun-only — the ceiling, not a candidate (decision 168)build.incremental.100— one body edit → 1 rendered, 123 cachedbuild.noop.100— touch only (mtime): stat + one hash, nothing rendered; report-only
Lint
Editorial lint over the same corpora, warm and cold.
| Metric | Value | Budget |
|---|---|---|
lint.100 | 9.7 ms | 100 ms ✅ |
lint.100.cold | 205.2 ms | report-only |
lint.1000 | 67 ms | 1000 ms ✅ |
lint.1000.cold | 2057.1 ms | report-only |
How each was taken:
lint.100— 0 errors · 0 warnings; mdast cache warmlint.100.cold— parse (micromark) + lint from an empty cache; report-onlylint.1000— 0 errors · 0 warnings; mdast cache warmlint.1000.cold— parse (micromark) + lint from an empty cache; report-only
The MCP server
Spawn to initialize on the release binary — the number that decides whether an agent's first turn waits.
| Metric | Value | Budget |
|---|---|---|
mcp.coldStart.binary | 23 ms | 50 ms ✅ |
mcp.coldStart | 21.4 ms | report-only |
How each was taken:
mcp.coldStart.binary— the artefact a release ships (bun build --compile --splitting), spawn →initialize; D2's lane since S18c · median of 21 interleaved roundsmcp.coldStart— report-only since S18c:bun packages/mcp/src/server.ts, the dev loop, not the thing anyone installs — interleaved with the binary lane, so the delta between the two rows is real even when the box is loaded
Install
What bunx @snypd/cli init pulls, and how much of it is snypd rather than the runtime it ships in.
| Metric | Value | Budget |
|---|---|---|
install.download.mb | 36.31 MB | report-only |
install.binary.mb | 83.97 MB | report-only |
install.code.mb | 5.26 MB | 8 MB ✅ |
How each was taken:
install.download.mb— report-only, and deliberately: the tarballbunx @snypd/cli initpulls, and that the host pulls again on every deploy — ~94 % of it is Bun's runtime, which no commit here movesinstall.binary.mb— report-only: the artefact unpacked — the half of install time that is disk rather than networkinstall.code.mb— the release binary (83.97 MB) minus a one-line program through the samecompile()(78.71 MB): Bun cancels and what is left is snypd — the only half of the download this repo can move
Serving
Time to first byte for a static dist/, for the preview server, and for the Desk it serves at /_snypd.
| Metric | Value | Budget |
|---|---|---|
serve.ttfb | 0.11 ms | report-only |
preview.ttfb | 0.09 ms | 50 ms ✅ |
desk.ttfb | 2.12 ms | report-only |
How each was taken:
serve.ttfb— static dist/ over Bun.serve — the floor the preview server is measured againstpreview.ttfb— the preview server (snypd dev), unchanged tree, drafts included; review page serveddesk.ttfb— report-only (S19a):/_snypdon the same server and the same unchanged tree. The Desk inherits D2's 50 ms rather than owning it, and this is the first session that can say whether it is inside it
Tokens
What a page costs an agent to read, and what a site costs one to learn — counted with the tokenizer named above.
| Metric | Value | Budget |
|---|---|---|
tokens.page.md | 510 tokens | 2500 tokens ✅ |
tokens.page.html | 1610 tokens | report-only |
tokens.page.reduction | 68.3 % | report-only |
tokens.learn | 4605 tokens | 6000 tokens ✅ |
tokens.page.html.editorial | 1663 tokens | report-only |
tokens.page.reduction.editorial | 69.3 % | report-only |
tokens.learn.editorial | 4779 tokens | 6000 tokens ✅ |
tokens.tools | 2230 tokens | 3000 tokens ✅ |
tokens.tools.full | 4256 tokens | report-only |
How each was taken:
tokens.page.reduction— vs this theme's own HTML — how thin this theme already is, not what an agent saves; low is good (docs/07 decision 15)tokens.learn— 17 resourcestokens.page.html.editorial— editorial themetokens.page.reduction.editorial— vs this theme's own HTML (editorial) — how thin this theme already is, not what an agent saves; low is good (docs/07 decision 15)tokens.learn.editorial— 17 resources · editorial themetokens.tools— 11 always listed (content.* + find_tools); paid every turn, on top of tokens.learn, which docs/05 scopes to config + spec + themetokens.tools.full— the same 15 tools with the catalogue listed rather than found — what deferring it saves a turn (docs/07 decision 38); report-only
The agent surface
The eight things a page has to offer a machine reader, and whether every one of them is there.
| Metric | Value | Budget |
|---|---|---|
surface.completeness | 100 % | ≥ 100 % ✅ |
How each was taken:
surface.completeness— 8/8: ✓ llms.txt, ✓ .md twin, ✓ Accept: text/markdown, ✓ link rel=alternate, ✓ JSON API, ✓ feed.xml, ✓ sitemap.xml, ✓ JSON-LD; public MCP joins in S19
Visual primitives
Chart, diagram and flow at their worst shapes: render time and SVG weight, zero JS, zero CSS.
| Metric | Value | Budget |
|---|---|---|
viz.chart.renderMs | 0.1 ms | 3 ms ✅ |
viz.chart.svgKb | 7.6 KB | 12 KB ✅ |
viz.diagram.renderMs | 0.88 ms | 15 ms ✅ |
viz.diagram.svgKb | 13.2 KB | 25 KB ✅ |
viz.flow.renderMs | 0.66 ms | 15 ms ✅ |
viz.flow.svgKb | 14.5 KB | 25 KB ✅ |
How each was taken:
viz.chart.renderMs— worst type (bar) on the worst shape — bar 0.10 ms / 6.6 KB · line 0.06 ms / 4.3 KB · area 0.06 ms / 4.6 KB · donut 0.10 ms / 6.9 KB · lollipop 0.08 ms / 7.6 KBviz.chart.svgKb— worst type (lollipop); zero JS, zero CSSviz.diagram.renderMs— worst shape (feedback) at the 40-node cap, layout cache defeated — chain 0.52 ms / 10.5 KB · wide 0.64 ms / 12.0 KB · feedback 0.88 ms / 13.2 KBviz.diagram.svgKb— worst shape (feedback); zero JS, zero CSSviz.flow.renderMs— worst shape (retry loop) at the 40-node cap, layout cache defeated — ladder 40 steps 0.33 ms / 13.7 KB · retry loop 38 steps 0.66 ms / 13.2 KB · nested 40 steps 0.36 ms / 14.5 KBviz.flow.svgKb— worst shape (nested); zero JS, zero CSS
Suggestions
content.suggest_blocks over a hand-labelled corpus: how often it finds the chart inside the table, and how often it invents one.
| Metric | Value | Budget |
|---|---|---|
suggest.precision | 1 | ≥ 0.8 ✅ |
suggest.recall | 1 | report-only |
suggest.ms | 3.37 ms | report-only |
How each was taken:
suggest.precision— 17/17 suggestions matched a label over 20 posts, 7 of which are labelled with no upgradesuggest.recall— 17/17 labelled upgrades found; report-only — a miss costs the author nothing, a false positive rewrites their postsuggest.ms— per post: parse + shapes + score + verify (the verify pass lints each candidate against the document it would land in)
The page in a browser
Six routes at 1280 and 390 px in headless Chrome: JavaScript on the wire, the webfont, axe violations, bytes and vitals.
| Metric | Value | Budget |
|---|---|---|
page.js.kb | 0 KB | 0 KB ✅ |
page.font.kb | 30.43 KB | 31 KB ✅ |
page.a11y.violations | 0 violations | 0 violations ✅ |
page.bytes.kb | 67.44 KB | report-only |
page.lcp | 68 ms | report-only |
page.cls | 0 | 0.05 ✅ |
How each was taken:
page.js.kb— editorial: 6 routes × 1280/390 px — /posts/every-primitive-once/, /about/, /authors/sunny/, /, /category/engineering/, /tag/markdown/; worst /posts/every-primitive-once/ @ 1280 (0 B loaded + 0 B inline/handlers). JSON-LD excluded: it is datapage.font.kb— worst /posts/every-primitive-once/ @ 1280; budget 31 KB is the theme's own font.kb — what it declared, not what the file happens to weighpage.a11y.violations— axe-core, 0 across 12 route/viewport pairspage.bytes.kb— worst /posts/every-primitive-once/ @ 1280: 16.26 KB html + 17.56 KB css + 3.19 KB img + 30.43 KB font, 4 requests — uncompressed, which no host serves; report-onlypage.lcp— worst /posts/every-primitive-once/ @ 1280; localhost, unthrottled — the shape of the page, not a field number; report-onlypage.cls— worst /posts/every-primitive-once/ @ 1280; caused by the theme, not the network — the one vital localhost measures honestly
The Desk in a browser
The same suite over /_snypd, with a draft in flight, and the first run of a scaffolded site.
| Metric | Value | Budget |
|---|---|---|
desk.js.kb | 0 KB | 0 KB ✅ |
desk.font.kb | 30.46 KB | 31 KB ✅ |
desk.a11y.violations | 0 violations | 0 violations ✅ |
desk.bytes.kb | 56.27 KB | report-only |
desk.lcp | 76 ms | report-only |
desk.cls | 0 | 0.05 ✅ |
desk.first.js.kb | 0 KB | 0 KB ✅ |
desk.first.font.kb | 30.46 KB | 31 KB ✅ |
desk.first.a11y.violations | 0 violations | 0 violations ✅ |
desk.first.bytes.kb | 58.73 KB | report-only |
desk.first.lcp | 76 ms | report-only |
desk.first.cls | 0 | 0.05 ✅ |
How each was taken:
desk.js.kb— desk: 2 routes × 1280/390 px — /_snypd, /_snypd/review/post/a-draft-in-flight; worst /_snypd @ 1280 (0 B loaded + 0 B inline/handlers). JSON-LD excluded: it is datadesk.font.kb— worst /_snypd @ 1280; budget 31 KB is the theme's own font.kb — what it declared, not what the file happens to weighdesk.a11y.violations— axe-core, 0 across 4 route/viewport pairsdesk.bytes.kb— worst /_snypd @ 1280: 8.23 KB html + 17.59 KB css + 0 KB img + 30.46 KB font, 3 requests — uncompressed, which no host serves; report-onlydesk.lcp— worst /_snypd/review/post/a-draft-in-flight @ 1280; localhost, unthrottled — the shape of the page, not a field number; report-onlydesk.cls— worst /_snypd @ 1280; caused by the theme, not the network — the one vital localhost measures honestlydesk.first.js.kb— first run: 2 routes × 1280/390 px — /_snypd, /; worst /_snypd @ 1280 (0 B loaded + 0 B inline/handlers). JSON-LD excluded: it is datadesk.first.font.kb— worst /_snypd @ 1280; budget 31 KB is the theme's own font.kb — what it declared, not what the file happens to weighdesk.first.a11y.violations— axe-core, 0 across 4 route/viewport pairsdesk.first.bytes.kb— worst /_snypd @ 1280: 10.69 KB html + 17.59 KB css + 0 KB img + 30.46 KB font, 3 requests — uncompressed, which no host serves; report-onlydesk.first.lcp— worst / @ 1280; localhost, unthrottled — the shape of the page, not a field number; report-onlydesk.first.cls— worst /_snypd @ 1280; caused by the theme, not the network — the one vital localhost measures honestly