diff --git a/bench/README.md b/bench/README.md
@@ -0,0 +1,25 @@
+# 벤치마크 재현
+
+<https://sqzass.sqzer.com/benchmark/> 의 수치를 만든 도구다. 수치는 머신마다
+다르니 **비율을 보되, 반드시 직접 돌려서** 확인할 것.
+
+```bash
+cargo build --release # sqzass (기본 = onig 엔진)
+python3 bench/gen.py # 도구 5종 × 시나리오 4종 코퍼스 생성
+python3 bench/bench.py sqzass # 도구별 실행 (hugo/zola/jekyll/astro)
+```
+
+- 대조군 설치는 각자: hugo, zola, jekyll(gem), astro(`bench/astro-base`에
+ `package.json`을 만들고 `npm install astro`).
+- `gen.py`는 자기 디렉터리 아래에 코퍼스를 만든다. 본문 마크다운은 도구 간
+ 바이트 동일, front matter 형식만 다르다.
+- `bench.py`는 매 회 출력물과 도구별 캐시(.jekyll-cache, astro cacheDir·dist,
+ hugo resources·lock)를 지운 콜드 빌드다. `taskset -c 0-3`, 3회, 중앙값,
+ GNU time으로 peak RSS.
+- 결과는 `results/<tool>-<scenario>.json`. 공개 수치의 원본은
+ `results-2026-07-29.json`.
+
+시나리오 정의는 `gen.py`가 곧 명세다 — 특히 heavy가 **repeat**(페이지당 동일
+코드 블록 5개)와 **unique**(전부 다른 블록)로 나뉘는 이유는 코드 반복도가
+순위를 가르는 변수라서다. 하나의 heavy 수치만 공개하는 벤치마크는 그 변수를
+숨긴 것이다.
diff --git a/bench/bench.py b/bench/bench.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""벤치마크 하네스. 사용: bench.py <tool> [scenario...]
+콜드 규율: 매 회 출력 디렉터리와 도구별 캐시를 지운다. taskset -c 0-3, 3회, 중앙값.
+결과는 results/<tool>-<scenario>.json 으로 append-safe하게 저장."""
+import json, os, re, shutil, statistics, subprocess, sys, time
+
+S = os.path.dirname(os.path.abspath(__file__))
+RUNS = 3
+GEM_BIN = subprocess.run(["ruby", "-e", "puts Gem.user_dir"], capture_output=True, text=True).stdout.strip() + "/bin"
+
+TOOLS = {
+ "sqzass": {
+ "cmd": lambda d: [os.environ.get("SQZASS", "target/release/sqzass"),
+ "build", "-i", d, "-o", f"{d}/out"],
+ "clean": lambda d: [f"{d}/out"],
+ "cwd": None,
+ },
+ "hugo": {
+ "cmd": lambda d: ["hugo", "--quiet", "--source", d, "--destination", f"{d}/out"],
+ "clean": lambda d: [f"{d}/out", f"{d}/resources", f"{d}/.hugo_build.lock"],
+ "cwd": None,
+ },
+ "zola": {
+ "cmd": lambda d: ["zola", "--root", d, "build", "--output-dir", f"{d}/out", "--force"],
+ "clean": lambda d: [f"{d}/out"],
+ "cwd": None,
+ },
+ "jekyll": {
+ "cmd": lambda d: [f"{GEM_BIN}/jekyll", "build", "-s", d, "-d", f"{d}/out", "--disable-disk-cache"],
+ "clean": lambda d: [f"{d}/out", f"{d}/.jekyll-cache"],
+ "cwd": None,
+ },
+ "astro": {
+ "cmd": lambda d: ["./node_modules/.bin/astro", "build"],
+ "clean": lambda d: [f"{d}/dist", f"{d}/.astro-cache"],
+ "cwd": lambda d: d,
+ },
+}
+
+def clean(paths):
+ for p in paths:
+ if os.path.isdir(p):
+ shutil.rmtree(p, ignore_errors=True)
+ elif os.path.exists(p):
+ os.remove(p)
+
+def run_one(tool, scenario):
+ t = TOOLS[tool]
+ d = f"{S}/{tool}/{scenario}"
+ times, rss = [], []
+ for i in range(RUNS):
+ clean(t["clean"](d))
+ cmd = ["taskset", "-c", "0-3", "/usr/bin/time", "-v"] + t["cmd"](d)
+ cwd = t["cwd"](d) if t["cwd"] else None
+ t0 = time.monotonic()
+ r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
+ wall = time.monotonic() - t0
+ if r.returncode != 0:
+ print(f"FAIL {tool}/{scenario}: {r.stderr[-500:]}", file=sys.stderr)
+ sys.exit(1)
+ m = re.search(r"Maximum resident set size \(kbytes\): (\d+)", r.stderr)
+ times.append(wall * 1000)
+ rss.append(int(m.group(1)) if m else 0)
+ time.sleep(2)
+ result = {
+ "tool": tool, "scenario": scenario,
+ "median_ms": round(statistics.median(times)),
+ "runs_ms": [round(x) for x in times],
+ "peak_rss_mb": round(max(rss) / 1024),
+ }
+ os.makedirs(f"{S}/results", exist_ok=True)
+ with open(f"{S}/results/{tool}-{scenario}.json", "w") as f:
+ json.dump(result, f)
+ print(f"{tool:7} {scenario:8} 중앙값 {result['median_ms']:>7} ms "
+ f"(runs: {', '.join(str(x) for x in result['runs_ms'])}) peakRSS {result['peak_rss_mb']} MB")
+
+tool = sys.argv[1]
+scenarios = sys.argv[2:] or ["minimal", "blog", "heavyr", "heavyu"]
+for sc in scenarios:
+ run_one(tool, sc)
diff --git a/bench/gen.py b/bench/gen.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""ssg 벤치마크 코퍼스 생성기. 본문 마크다운은 도구 간 바이트 동일, front matter 형식만 다르다."""
+import os, shutil, subprocess, sys
+
+S = os.path.dirname(os.path.abspath(__file__))
+N = 1000
+PARA = "The quick brown fox jumps over the lazy dog while the build system watches. " * 4
+
+def code_block(page, blk, unique):
+ tag = f"page{page}_blk{blk}_" if unique else ""
+ lines = "\n".join(
+ f"fn {tag}handler_{j}(req: Request) -> Response {{ Response::ok(req.body_{tag}{j}()) }}"
+ for j in range(20)
+ )
+ return f"```rust\n{lines}\n```"
+
+def body(scenario, i):
+ if scenario == "minimal":
+ return f"# Post {i}\n\n{PARA}\n"
+ if scenario == "blog":
+ nxt = f"/posts/post-{(i + 1) % N}/"
+ return (f"# Post {i}\n\n" + "\n\n".join([PARA] * 6)
+ + f"\n\n- one\n- two\n- three\n\n> quote block\n\n[next]({nxt})\n")
+ unique = scenario == "heavyu"
+ parts = [f"# Post {i}"]
+ for b in range(5):
+ parts.append(PARA)
+ parts.append(code_block(i, b, unique))
+ return "\n\n".join(parts) + "\n"
+
+def w(path, content):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w") as f:
+ f.write(content)
+
+SCENARIOS = ["minimal", "blog", "heavyr", "heavyu"]
+SQZASS = os.environ.get("SQZASS", "target/release/sqzass")
+
+for sc in SCENARIOS:
+ # --- sqzass: init 스캐폴드 + TOML +++ front matter
+ d = f"{S}/sqzass/{sc}"
+ if not os.path.exists(d):
+ subprocess.run([SQZASS, "init", d], check=True, capture_output=True)
+ w(f"{d}/content/posts/_index.md", '+++\ntitle = "posts"\n+++\n')
+ for i in range(N):
+ w(f"{d}/content/posts/post-{i}.md", f'+++\ntitle = "Post {i}"\n+++\n\n{body(sc, i)}')
+
+ # --- hugo: TOML +++ front matter (동일 형식)
+ d = f"{S}/hugo/{sc}"
+ w(f"{d}/hugo.toml", 'baseURL = "https://example.com/"\ntitle = "bench"\n')
+ w(f"{d}/layouts/_default/single.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{{ .Title }}</title></head>"
+ "<body><main>{{ .Content }}</main></body></html>\n")
+ w(f"{d}/layouts/_default/list.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{{ .Title }}</title></head>"
+ "<body><main>{{ range .Pages }}<a href=\"{{ .RelPermalink }}\">{{ .Title }}</a> {{ end }}</main></body></html>\n")
+ for i in range(N):
+ w(f"{d}/content/posts/post-{i}.md", f'+++\ntitle = "Post {i}"\n+++\n\n{body(sc, i)}')
+
+ # --- zola: TOML +++ front matter, highlight_code 명시적 활성화(기본 꺼짐)
+ d = f"{S}/zola/{sc}"
+ w(f"{d}/config.toml",
+ 'base_url = "https://example.com"\ntitle = "bench"\ncompile_sass = false\n'
+ 'build_search_index = false\n\n[markdown.highlighting]\ntheme = "github-dark"\n')
+ w(f"{d}/templates/index.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>bench</title></head><body><main>index</main></body></html>\n")
+ w(f"{d}/templates/page.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{{ page.title }}</title></head>"
+ "<body><main>{{ page.content | safe }}</main></body></html>\n")
+ w(f"{d}/templates/section.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{{ section.title }}</title></head>"
+ "<body><main>{% for p in section.pages %}<a href=\"{{ p.permalink }}\">{{ p.title }}</a> {% endfor %}</main></body></html>\n")
+ w(f"{d}/content/posts/_index.md", '+++\ntitle = "posts"\n+++\n')
+ for i in range(N):
+ w(f"{d}/content/posts/post-{i}.md", f'+++\ntitle = "Post {i}"\n+++\n\n{body(sc, i)}')
+
+ # --- jekyll: YAML --- front matter
+ d = f"{S}/jekyll/{sc}"
+ w(f"{d}/_config.yml",
+ 'title: bench\ndefaults:\n - scope: { path: "" }\n values: { layout: default }\n')
+ w(f"{d}/_layouts/default.html",
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{{ page.title }}</title></head>"
+ "<body><main>{{ content }}</main></body></html>\n")
+ for i in range(N):
+ w(f"{d}/posts/post-{i}.md", f'---\ntitle: "Post {i}"\n---\n\n{body(sc, i)}')
+
+ # --- astro: YAML front matter + layout 지정, node_modules는 공용 심링크
+ d = f"{S}/astro/{sc}"
+ os.makedirs(d, exist_ok=True)
+ shutil.copy(f"{S}/astro-base/package.json", f"{d}/package.json")
+ w(f"{d}/astro.config.mjs", "export default { cacheDir: './.astro-cache' };\n")
+ if not os.path.islink(f"{d}/node_modules"):
+ os.symlink(f"{S}/astro-base/node_modules", f"{d}/node_modules")
+ w(f"{d}/src/layouts/Base.astro",
+ "---\nconst { frontmatter } = Astro.props;\n---\n"
+ "<!doctype html><html><head><meta charset=\"utf-8\"><title>{frontmatter.title}</title></head>"
+ "<body><main><slot /></main></body></html>\n")
+ for i in range(N):
+ w(f"{d}/src/pages/posts/post-{i}.md",
+ f'---\nlayout: ../../layouts/Base.astro\ntitle: "Post {i}"\n---\n\n{body(sc, i)}')
+
+print("corpora ready:", ", ".join(SCENARIOS))
diff --git a/bench/results-2026-07-29.json b/bench/results-2026-07-29.json
@@ -0,0 +1,233 @@
+{
+ "date": "2026-07-29",
+ "machine": "4 cores (taskset -c 0-3), AC, performance governor",
+ "versions": {
+ "sqzass": "0.1.0 (bf1a9dc)",
+ "hugo": "0.164.0+extended",
+ "zola": "0.22.1",
+ "jekyll": "4.4.1 (ruby 3.4.10)",
+ "astro": "5.18.2 (node 26.5.0)"
+ },
+ "results": [
+ {
+ "tool": "astro",
+ "scenario": "blog",
+ "median_ms": 6699,
+ "runs_ms": [
+ 6699,
+ 7079,
+ 6666
+ ],
+ "peak_rss_mb": 604
+ },
+ {
+ "tool": "astro",
+ "scenario": "heavyr",
+ "median_ms": 22090,
+ "runs_ms": [
+ 23517,
+ 22090,
+ 21101
+ ],
+ "peak_rss_mb": 2578
+ },
+ {
+ "tool": "astro",
+ "scenario": "heavyu",
+ "median_ms": 23958,
+ "runs_ms": [
+ 23211,
+ 25187,
+ 23958
+ ],
+ "peak_rss_mb": 2607
+ },
+ {
+ "tool": "astro",
+ "scenario": "minimal",
+ "median_ms": 4672,
+ "runs_ms": [
+ 3899,
+ 4672,
+ 4894
+ ],
+ "peak_rss_mb": 555
+ },
+ {
+ "tool": "hugo",
+ "scenario": "blog",
+ "median_ms": 112,
+ "runs_ms": [
+ 114,
+ 112,
+ 111
+ ],
+ "peak_rss_mb": 140
+ },
+ {
+ "tool": "hugo",
+ "scenario": "heavyr",
+ "median_ms": 3559,
+ "runs_ms": [
+ 3559,
+ 3553,
+ 3604
+ ],
+ "peak_rss_mb": 411
+ },
+ {
+ "tool": "hugo",
+ "scenario": "heavyu",
+ "median_ms": 5912,
+ "runs_ms": [
+ 5792,
+ 6061,
+ 5912
+ ],
+ "peak_rss_mb": 421
+ },
+ {
+ "tool": "hugo",
+ "scenario": "minimal",
+ "median_ms": 93,
+ "runs_ms": [
+ 94,
+ 93,
+ 92
+ ],
+ "peak_rss_mb": 123
+ },
+ {
+ "tool": "jekyll",
+ "scenario": "blog",
+ "median_ms": 752,
+ "runs_ms": [
+ 769,
+ 752,
+ 746
+ ],
+ "peak_rss_mb": 62
+ },
+ {
+ "tool": "jekyll",
+ "scenario": "heavyr",
+ "median_ms": 10850,
+ "runs_ms": [
+ 9754,
+ 10850,
+ 11406
+ ],
+ "peak_rss_mb": 174
+ },
+ {
+ "tool": "jekyll",
+ "scenario": "heavyu",
+ "median_ms": 11456,
+ "runs_ms": [
+ 11456,
+ 11668,
+ 10797
+ ],
+ "peak_rss_mb": 182
+ },
+ {
+ "tool": "jekyll",
+ "scenario": "minimal",
+ "median_ms": 442,
+ "runs_ms": [
+ 446,
+ 442,
+ 441
+ ],
+ "peak_rss_mb": 56
+ },
+ {
+ "tool": "sqzass",
+ "scenario": "blog",
+ "median_ms": 25,
+ "runs_ms": [
+ 25,
+ 25,
+ 24
+ ],
+ "peak_rss_mb": 24
+ },
+ {
+ "tool": "sqzass",
+ "scenario": "heavyr",
+ "median_ms": 187,
+ "runs_ms": [
+ 191,
+ 186,
+ 187
+ ],
+ "peak_rss_mb": 229
+ },
+ {
+ "tool": "sqzass",
+ "scenario": "heavyu",
+ "median_ms": 1298,
+ "runs_ms": [
+ 1298,
+ 1308,
+ 1291
+ ],
+ "peak_rss_mb": 439
+ },
+ {
+ "tool": "sqzass",
+ "scenario": "minimal",
+ "median_ms": 18,
+ "runs_ms": [
+ 17,
+ 18,
+ 18
+ ],
+ "peak_rss_mb": 15
+ },
+ {
+ "tool": "zola",
+ "scenario": "blog",
+ "median_ms": 172,
+ "runs_ms": [
+ 175,
+ 169,
+ 172
+ ],
+ "peak_rss_mb": 142
+ },
+ {
+ "tool": "zola",
+ "scenario": "heavyr",
+ "median_ms": 5084,
+ "runs_ms": [
+ 5084,
+ 5055,
+ 5398
+ ],
+ "peak_rss_mb": 269
+ },
+ {
+ "tool": "zola",
+ "scenario": "heavyu",
+ "median_ms": 6789,
+ "runs_ms": [
+ 6969,
+ 6752,
+ 6789
+ ],
+ "peak_rss_mb": 279
+ },
+ {
+ "tool": "zola",
+ "scenario": "minimal",
+ "median_ms": 141,
+ "runs_ms": [
+ 141,
+ 142,
+ 140
+ ],
+ "peak_rss_mb": 136
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/content/benchmark/_index.ko.md b/docs/content/benchmark/_index.ko.md
@@ -0,0 +1,104 @@
++++
+title = "벤치마크"
+description = "생성기 다섯, 머신 하나, 코퍼스 하나 — 그리고 정의는 전부 공개"
+weight = 60
+toc = true
++++
+
+같은 1,000페이지 사이트를 같은 머신에서 다섯 생성기로 콜드 빌드했습니다.
+짧을수록 좋습니다. sqzass 수치에는 기본으로 내보내는 모든 것 — 검색 색인,
+sitemap, robots.txt, llms.txt — 이 포함돼 있고, 다른 도구는 이걸 만들지
+않습니다.
+
+수치는 특정 머신 한 대에서 잰 콜드 3회의 중앙값입니다. 밀리초보다 비율이
+더 멀리 갑니다. 생성기와 하네스가
+[`bench/`](https://github.com/sqzer-x/sqzass/tree/main/bench)에 있으니 직접
+돌려 보세요.
+
+## minimal — 제목 하나와 문단 하나
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="2%" height="100%" rx="3"></rect></svg><span class="b-val">93 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="3%" height="100%" rx="3"></rect></svg><span class="b-val">141 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="9.5%" height="100%" rx="3"></rect></svg><span class="b-val">442 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">4,672 ms</span></span>
+</div>
+
+## blog — 문단 여섯, 목록, 인용, 링크
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="1.7%" height="100%" rx="3"></rect></svg><span class="b-val">112 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="2.6%" height="100%" rx="3"></rect></svg><span class="b-val">172 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="11.2%" height="100%" rx="3"></rect></svg><span class="b-val">752 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">6,699 ms</span></span>
+</div>
+
+## heavy·반복 — 페이지마다 동일한 20줄 Rust 블록 5개
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.85%" height="100%" rx="3"></rect></svg><span class="b-val">187 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="16.1%" height="100%" rx="3"></rect></svg><span class="b-val">3,559 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="23%" height="100%" rx="3"></rect></svg><span class="b-val">5,084 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="49.1%" height="100%" rx="3"></rect></svg><span class="b-val">10,850 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">22,090 ms</span></span>
+</div>
+
+## heavy·고유 — 페이지마다 전부 다른 20줄 Rust 블록 5개
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="5.4%" height="100%" rx="3"></rect></svg><span class="b-val">1,298 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="24.7%" height="100%" rx="3"></rect></svg><span class="b-val">5,912 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="28.3%" height="100%" rx="3"></rect></svg><span class="b-val">6,789 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="47.8%" height="100%" rx="3"></rect></svg><span class="b-val">11,456 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">23,958 ms</span></span>
+</div>
+
+## 전체 수치
+
+벽시계 시간(콜드 3회 중앙값)과 peak RSS입니다.
+
+| | minimal | blog | heavy·반복 | heavy·고유 |
+|---|---|---|---|---|
+| **sqzass 0.1.0** | **18 ms** · 15 MB | **25 ms** · 24 MB | **187 ms** · 229 MB | **1,298 ms** · 439 MB |
+| Hugo 0.164.0 | 93 ms · 123 MB | 112 ms · 140 MB | 3,559 ms · 411 MB | 5,912 ms · 421 MB |
+| Zola 0.22.1 | 141 ms · 136 MB | 172 ms · 142 MB | 5,084 ms · 269 MB | 6,789 ms · 279 MB |
+| Jekyll 4.4.1 | 442 ms · 56 MB | 752 ms · 62 MB | 10,850 ms · 174 MB | 11,456 ms · 182 MB |
+| Astro 5.18.2 | 4,672 ms · 555 MB | 6,699 ms · 604 MB | 22,090 ms · 2,578 MB | 23,958 ms · 2,607 MB |
+
+## heavy가 둘인 이유
+
+코드 반복도가 순위를 가르는데, 대부분의 벤치마크는 자기 코퍼스의 반복도를
+말하지 않기 때문입니다. sqzass는 서로 다른 블록만 빌드에서 한 번씩
+하이라이트하므로, 같은 블록을 반복하는 생성 코퍼스와 전부 다른 블록인
+코퍼스는 서로 다른 측정입니다 — 187 ms와 1,298 ms의 간격이 바로 그
+변수입니다. 반복도를 밝히지 않은 단일 "heavy" 수치는 결과를 만든 변수를
+숨긴 것입니다.
+
+## 방법
+
+- **콜드.** 매 회 출력 디렉터리와 도구별 캐시 — `.jekyll-cache`, Astro의
+ `cacheDir`와 `dist`, Hugo의 `resources`와 빌드 락 — 를 지웠습니다. 벽시계
+ 시간과 peak RSS는 GNU time으로 쟀습니다.
+- **코퍼스.** 1,000페이지 + 섹션 인덱스. 마크다운 *본문*은 도구 간 바이트
+ 동일이고 front matter 문법만 다릅니다. 문단은 76자 문장 하나를 네 번
+ 반복한 고정 텍스트입니다.
+- **하이라이팅은 다섯 도구 모두 빌드 타임에**, 각자 싣는 그대로 켰습니다:
+ syntect+Oniguruma(sqzass), Chroma(Hugo), Giallo(Zola), Rouge(Jekyll),
+ Shiki(Astro). 마크업 세분도는 다릅니다 — 같은 heavy 페이지의 span 수:
+ sqzass 3,205 · Jekyll 1,800 · Zola·Astro 1,700 · Hugo 600. sqzass는 다섯 중
+ 가장 세밀한 마크업을 내면서 위의 수치를 냅니다.
+- **템플릿.** 다섯 도구 모두 테마 없는 동일한 최소 레이아웃 — 렌더된 본문을
+ 감싸는 HTML 셸 — 을 씁니다.
+- **Astro의 시간에는 Node 기동(~1초)이 포함**됩니다. Astro는 마크다운→HTML
+ 이상의 일을 하는 컴포넌트 프레임워크이기도 합니다. 그게 정직한 단서이고,
+ 이 페이지의 대상은 순수 마크다운 워크로드입니다.
+
+## 이 페이지가 주장하지 않는 것
+
+당신의 머신은 다른 밀리초를 냅니다. 증분 빌드와 개발 서버는 재지 않았습니다.
+기능 범위도 서로 다릅니다 — 여기서 잰 건 하나입니다: 마크다운 디렉터리를
+HTML 디렉터리로, 콜드로, 각 도구의 기본값으로 바꾸는 일. 위의 정의와
+[`bench/`](https://github.com/sqzer-x/sqzass/tree/main/bench)의 스크립트가
+방법론의 전부입니다. 수치가 이상해 보이면 직접 돌려 보세요.
diff --git a/docs/content/benchmark/_index.md b/docs/content/benchmark/_index.md
@@ -0,0 +1,104 @@
++++
+title = "Benchmark"
+description = "Five generators, one machine, one corpus — and the definitions published"
+weight = 60
+toc = true
++++
+
+The same 1,000-page site, built cold by five generators on the same machine.
+Lower is better. The sqzass numbers include everything it emits by default —
+search index, sitemap, robots.txt, llms.txt — which none of the others build.
+
+Numbers are medians of three cold runs on one specific machine. Ratios travel
+better than milliseconds; the generator and harness are in
+[`bench/`](https://github.com/sqzer-x/sqzass/tree/main/bench), so run them on
+yours.
+
+## minimal — a heading and a paragraph
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">18 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="2%" height="100%" rx="3"></rect></svg><span class="b-val">93 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="3%" height="100%" rx="3"></rect></svg><span class="b-val">141 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="9.5%" height="100%" rx="3"></rect></svg><span class="b-val">442 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">4,672 ms</span></span>
+</div>
+
+## blog — six paragraphs, a list, a quote, a link
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.6%" height="100%" rx="3"></rect></svg><span class="b-val">25 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="1.7%" height="100%" rx="3"></rect></svg><span class="b-val">112 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="2.6%" height="100%" rx="3"></rect></svg><span class="b-val">172 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="11.2%" height="100%" rx="3"></rect></svg><span class="b-val">752 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">6,699 ms</span></span>
+</div>
+
+## heavy, repeated code — five identical 20-line Rust blocks per page
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="0.85%" height="100%" rx="3"></rect></svg><span class="b-val">187 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="16.1%" height="100%" rx="3"></rect></svg><span class="b-val">3,559 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="23%" height="100%" rx="3"></rect></svg><span class="b-val">5,084 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="49.1%" height="100%" rx="3"></rect></svg><span class="b-val">10,850 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">22,090 ms</span></span>
+</div>
+
+## heavy, unique code — five distinct 20-line Rust blocks per page
+
+<div class="bench">
+<span class="b-name">sqzass</span><span class="b-row"><svg><rect class="b-us" width="5.4%" height="100%" rx="3"></rect></svg><span class="b-val">1,298 ms</span></span>
+<span class="b-name">Hugo</span><span class="b-row"><svg><rect width="24.7%" height="100%" rx="3"></rect></svg><span class="b-val">5,912 ms</span></span>
+<span class="b-name">Zola</span><span class="b-row"><svg><rect width="28.3%" height="100%" rx="3"></rect></svg><span class="b-val">6,789 ms</span></span>
+<span class="b-name">Jekyll</span><span class="b-row"><svg><rect width="47.8%" height="100%" rx="3"></rect></svg><span class="b-val">11,456 ms</span></span>
+<span class="b-name">Astro</span><span class="b-row"><svg><rect width="100%" height="100%" rx="3"></rect></svg><span class="b-val">23,958 ms</span></span>
+</div>
+
+## Every number
+
+Wall clock, median of three cold runs, and peak RSS.
+
+| | minimal | blog | heavy · repeat | heavy · unique |
+|---|---|---|---|---|
+| **sqzass 0.1.0** | **18 ms** · 15 MB | **25 ms** · 24 MB | **187 ms** · 229 MB | **1,298 ms** · 439 MB |
+| Hugo 0.164.0 | 93 ms · 123 MB | 112 ms · 140 MB | 3,559 ms · 411 MB | 5,912 ms · 421 MB |
+| Zola 0.22.1 | 141 ms · 136 MB | 172 ms · 142 MB | 5,084 ms · 269 MB | 6,789 ms · 279 MB |
+| Jekyll 4.4.1 | 442 ms · 56 MB | 752 ms · 62 MB | 10,850 ms · 174 MB | 11,456 ms · 182 MB |
+| Astro 5.18.2 | 4,672 ms · 555 MB | 6,699 ms · 604 MB | 22,090 ms · 2,578 MB | 23,958 ms · 2,607 MB |
+
+## Why heavy comes twice
+
+Because code repetition decides rankings, and most benchmarks do not say what
+theirs is. sqzass highlights each distinct block once per build, so a corpus
+whose generator repeats the same block is a different measurement from one
+where every block is unique — the gap between 187 ms and 1,298 ms *is* that
+variable. A single "heavy" number that does not state its repetition has
+hidden the thing that produced it.
+
+## Method
+
+- **Cold.** Before every run the output directory and each tool's caches are
+ removed — `.jekyll-cache`, Astro's `cacheDir` and `dist`, Hugo's `resources`
+ and build lock. Wall clock and peak RSS come from GNU time.
+- **Corpus.** 1,000 pages plus a section index. The markdown *bodies* are
+ byte-identical across tools; only the front matter syntax differs.
+ A paragraph is one fixed 76-character sentence repeated four times.
+- **Highlighting is on everywhere**, at build time, as each tool ships it:
+ syntect + Oniguruma (sqzass), Chroma (Hugo), Giallo (Zola), Rouge (Jekyll),
+ Shiki (Astro). Markup granularity differs — spans on the same heavy page:
+ sqzass 3,205 · Jekyll 1,800 · Zola and Astro 1,700 · Hugo 600. sqzass emits
+ the most detailed markup of the five *and* the numbers above.
+- **Templates.** Each tool gets the same minimal no-theme layout: an HTML
+ shell around the rendered content.
+- **Astro's time includes Node startup** (~1 s). It is also doing more than
+ markdown-to-HTML — it is a component framework. That is the honest caveat;
+ the corpus is plain markdown, which is the workload this page is about.
+
+## What this page does not claim
+
+Your machine will produce different milliseconds. Incremental builds and dev
+servers are not measured. Feature scopes differ — this measures one thing:
+turning a directory of markdown into a directory of HTML, cold, with the
+defaults each tool ships. The definitions above and the scripts in
+[`bench/`](https://github.com/sqzer-x/sqzass/tree/main/bench) are the whole
+methodology; if a number looks wrong, run it.
diff --git a/docs/static/css/main.css b/docs/static/css/main.css
@@ -598,6 +598,36 @@ h4:hover > .anchor, h5:hover > .anchor, h6:hover > .anchor,
border-radius: var(--radius) var(--radius) 0 0;
}
+/* ---------- benchmark bars ---------- */
+
+/* 도구 식별은 행 라벨이, 수치는 막대 옆 텍스트가 운반한다. 색은 우리(악센트)와
+ 비교군(중립) 둘뿐이라 색이 단독으로 정보를 나르지 않는다. 막대 폭이 style
+ 속성이 아니라 SVG rect의 표현 속성인 이유: 인라인 style은 이 사이트가
+ 지켜 온 strict style-src CSP 가능성을 깨기 때문이다. */
+.bench {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.45rem 0.8rem;
+ align-items: center;
+ margin: 0.8rem 0 1.8rem;
+}
+.bench .b-name {
+ font-family: var(--mono);
+ font-size: 0.8rem;
+ color: var(--ink);
+ text-align: right;
+}
+.bench .b-row { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
+.bench svg { flex: 1; min-width: 0; height: 0.75rem; display: block; }
+.bench rect { fill: var(--ink-3); opacity: 0.55; }
+.bench rect.b-us { fill: var(--accent); opacity: 1; }
+.bench .b-val {
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ color: var(--ink-2);
+ white-space: nowrap;
+}
+
/* ---------- tables ---------- */
/* `display: block`이었다. 넓은 표를 가로로 굴리려던 것인데, 그러면 표가 내부
|