These measurements use existing repository fixtures with the FastAPI editor. The benchmark includes the exact post that BenchmarkBuild_Incremental edits: benchmarks/site/posts/blog/2024/01/post-001.md. The other cases come from rendering-fixtures/; they are visual regression fixtures, rather than existing performance benchmarks.

Fixture coverage

File Coverage
post-001.md Deterministic site benchmark post, including code
home.md Small page with minimal assets
markdown-chart-fallback.md Chart.js output and accessible data fallbacks
md-card-headings.md Cards and expressive headings
roads.md Roads, layout styles, and initialization script
story-layout.md Longer structured page
test-headings.md Heading syntax and typography

Inputs retain their complete file contents, including frontmatter. These calls measure the standalone renderer; they do not run the site build pipeline or its frontmatter loading stage.

Measured timings

All values are milliseconds. The Go column shows the median of three benchmark runs. The other columns show medians across 15 calls, except the final column.

Fixture Input bytes Go renderer Python render call FastAPI render timer, edit HTTP edit HTTP edit p95
post-001.md 1,518 3.05 137.51 135.02 136.25 148.45
home.md 426 0.40 128.50 128.24 129.15 136.19
markdown-chart-fallback.md 1,550 33.04 476.93 260.01 264.50 470.84
md-card-headings.md 1,104 0.89 151.58 152.03 153.45 167.69
roads.md 1,598 1.08 139.34 174.50 177.22 224.00
story-layout.md 3,900 1.46 156.73 153.57 154.97 174.24
test-headings.md 1,245 0.82 168.01 151.57 152.70 170.24

For the existing site benchmark post, Go rendering takes about 3 ms, while a Python render call takes about 138 ms. The Python API starts a Go process for each call and decodes its JSON output. The gap includes process initialization, serialization, pipe transfer, and Python decoding; these measurements do not isolate each part.

Chart.js has higher rendering and transfer costs because its response includes vendored JavaScript and generated initialization code. The current fragment response still sends those feature assets with each chart edit. Keeping shared CSS loaded does not remove all repeated feature-asset bytes.

Initial document versus later edits

Response sizes below are uncompressed JSON body bytes, rounded to KiB (1,024 bytes). They exclude HTTP headers and separately requested appearance assets. Initial requests include a full document; edit requests set include_document to false.

Fixture Initial HTTP median, ms Edit HTTP median, ms Initial response, KiB Edit response, KiB Fewer response bytes
post-001.md 135.39 136.25 46.44 20.01 56.9%
home.md 130.10 129.15 8.13 0.86 89.4%
markdown-chart-fallback.md 432.89 264.50 460.25 226.92 50.7%
md-card-headings.md 136.74 153.45 45.55 19.57 57.0%
roads.md 167.05 177.22 93.22 43.40 53.4%
story-layout.md 152.85 154.97 25.48 9.54 62.6%
test-headings.md 154.95 152.70 13.25 3.42 74.2%

Omitting the document shell reduces response bytes in every fixture. It does not establish a latency improvement: each mode runs separately, and host load varies between modes. Some fragment requests therefore take longer despite their smaller responses.

Timing boundaries and limits

Each Python and HTTP mode has two excluded warm-up calls and 15 measured calls per file. Every render still starts a fresh Go process; these are warm filesystem measurements, rather than a persistent renderer process. The nearest-rank p95 equals the maximum with only 15 samples. Use more samples for stable tail-latency decisions.

The measurements ran sequentially on a shared workstation with normal background activity. They are observations from this host, rather than service limits or a controlled before-and-after experiment. They exclude the editor's 350 ms debounce, browser layout, chart drawing, Mermaid execution, and user-perceived paint time. No site build cache, encryption key, or site configuration participates in these standalone calls.

Environment and raw results

The JSON report contains every measured sample, fixture SHA-256 hashes, and the installed binary and wrapper hashes. The Go benchmark output contains iteration counts, allocation counts, and all three runs.

Reproduce

Use a virtual environment with the current Markata wheel, FastAPI, and Uvicorn. Run these commands from the repository root, sequentially:

python python/benchmark_editor.py
go test ./pkg/plugins -run '^$' -bench '^BenchmarkRenderStandaloneFixtures$' -benchtime=200ms -count=3

The Python script starts a temporary localhost server and stops it when the run ends. It writes benchmarks/renderer-integration-YYYY-MM-DD.json using the UTC date. It does not replace the live editor server.

Before comparing changes, keep the fixture bytes, dependency versions, machine, and renderer options the same. Compare medians and raw samples as well as response sizes. Useful next measurements include process initialization, JSON encoding, asset transfer, browser paint, and memory retained after repeated diagram edits.

Go server workbench

The Go workbench uses the same seven fixture files and the same HTTP timing boundaries. All fixture hashes match the Python report above. The server renders directly in process and keeps one previous result in memory.

Each mode uses two excluded warm-up calls and 15 measured calls. Before every cache-miss call, a separate unmeasured request renders a small invalidator document. This keeps the measured fixture bytes unchanged while preventing an accidental cache hit. Every measured response checks its cached field. Cached edits use the same source repeatedly without invalidation. The invalidator request is excluded from the HTTP time and response size.

HTTP results

Values are milliseconds. Each median and p95 comes from 15 calls. The Python column comes from the earlier report; the Go columns come from a separate run on the same workstation.

Fixture Python HTTP edit median Go initial miss median Go edit miss median Go edit miss p95 Go cached edit median
post-001.md 136.245 2.247 2.143 3.599 0.365
home.md 129.152 0.645 0.466 0.867 0.324
markdown-chart-fallback.md 264.495 36.032 28.571 53.724 0.295
md-card-headings.md 153.448 0.799 1.504 1.790 0.366
roads.md 177.217 0.957 0.973 1.547 0.325
story-layout.md 154.968 1.368 1.326 1.827 0.300
test-headings.md 152.701 0.838 0.805 1.721 0.293

For the site benchmark post, an uncached Go edit takes 2.143 ms over loopback HTTP. A repeated cached edit takes 0.365 ms. The earlier Python HTTP measurement for that exact file is 136.25 ms.

The Go server avoids a renderer subprocess per request. It also links feature assets instead of embedding their contents in each response. Both differences contribute to the observed gap. These runs do not isolate language or process-startup costs. The native Go renderer benchmark uses inline assets, so it can take longer than the Go HTTP path with linked assets.

Response sizes

Values are uncompressed JSON body KiB. Separately requested stylesheets, libraries, and fonts are excluded. Initial responses contain a complete document and fragment fields. Edit responses omit the document.

Fixture Python edit response Go initial response Go edit response Go initial-to-edit reduction
post-001.md 20.01 24.62 8.82 64.2%
home.md 0.86 8.99 1.02 88.6%
markdown-chart-fallback.md 226.92 24.15 8.54 64.6%
md-card-headings.md 19.57 12.99 2.99 77.0%
roads.md 43.40 19.58 6.30 67.8%
story-layout.md 9.54 30.59 11.82 61.4%
test-headings.md 3.42 16.24 4.65 71.4%

The Go editor retains loaded libraries and shared styles in the preview frame. The linked-asset policy makes its chart response smaller, but the browser must fetch each dependency when it is first needed. Response-size reductions do not include those initial asset transfers and do not establish a browser paint-time improvement.

Go measurements and reproduction

The Go run uses runtime source revision 059bb011f3314c43ca9e0c6e51d7ed735cf68790. The merged parent adds benchmark tooling and documentation without changing that runtime. The host is the same AMD Ryzen 5 3600 workstation, with 12 logical CPUs and Go 1.26.3. The Go HTTP client uses Python 3.14.8; the earlier Python-reference client uses 3.13.13. The comparison includes that client-version difference. The JSON report records its timestamp, OS, binary hash, and harness hash.

The Go server timer includes rendering, asset registration, and optional document construction. It excludes HTTP JSON encoding and network transfer. The Python server timer above excludes document construction. Use HTTP measurements to compare the complete request paths.

These are sequential measurements on a shared workstation, rather than a controlled before-and-after experiment. They exclude server startup, appearance generation, cache invalidation calls, separately fetched assets, editor debounce, browser layout, and drawing. The Go editor debounce is 250 ms. The p95 equals the maximum with 15 samples; larger runs are needed for stable tail estimates. No site configuration, site build cache, encryption keys, or site lifecycle stages participate.

The Go HTTP report contains every sample and fixture/binary/harness hashes. The native Go output contains three fresh runs of the parent's renderer benchmark on the same fixtures.

Run these commands sequentially from the repository root:

go build -o /tmp/markata-workbench-benchmark ./cmd/markata-workbench
python python/benchmark_go_editor.py --binary /tmp/markata-workbench-benchmark
go test ./pkg/plugins -run '^$' -bench '^BenchmarkRenderStandaloneFixtures$' -benchtime=200ms -count=3

The standard-library Python client starts a temporary Go server on an OS-assigned loopback port and stops it after the run. It does not restart the live editor or change its cache. Use --samples, --warmups, and --output to control the run. The default output uses the UTC date.

Browser WASM worker

Start the WASM demo before the benchmark:

python3 scripts/wasm-workbench.py --serve

In another terminal, run:

uv run --with playwright python scripts/benchmark-wasm.py --output /tmp/wasm-benchmark.json

Use --url for a different demo address. Install Playwright Chromium if the browser is unavailable. The benchmark uses the same seven fixtures as the HTTP clients. Each fixture uses a fresh worker, two excluded warmups, and fifteen measured renders. The report includes raw samples, fixture hashes, browser version, median, and p95.

These timings measure worker rendering. They exclude worker startup, message transfer, layout, and paint. Do not compare them directly with HTTP request timings.

Local demos use these addresses when all three run together:

Integration verification on 2026-10-29

Fresh reports use the *-suite-2026-10-29.json names for Python and Go. The WASM report is benchmarks/wasm-workbench-2026-10-29.json. For post-001.md, the Python HTTP edit median was 149.282 ms. The Go HTTP edit median was 2.389 ms; the cached median was 0.301 ms. The WASM worker median was 10.9 ms. These measurements use different timing boundaries and asset policies, as described above.

Shared editor features and runtime differences

All three demos provide the same editor features:

An oversized import leaves the current draft intact. Download the draft before clearing browser storage. If storage is unavailable or full, editing and downloads remain available, but drafts cannot survive closing the page. Each origin stores its own draft and appearance settings. Different ports do not share these settings. For a font comparison, select the same font pack and text size in each demo. The default uses system fonts; operating systems can display different system typefaces.

Runtime behavior Python / FastAPI Go server Browser WASM
Rendering location Server subprocess per request Directly in Go server Persistent browser worker
Markdown transfer HTTP to local server HTTP to local server No render request
Rich feature assets Inline response assets Linked assets reused by browser Bundled assets cached locally
Offline reload Requires running server Requires running server Available after Ready offline
Authored raw HTML Enabled inside sandbox Enabled inside sandbox Disabled
Repeated source New renderer process Last result cache Persistent renderer runtime

Runtime differences do not require different fonts, import controls, or draft feedback. Raw HTML and cache policies are demo choices, rather than requirements of Python, Go, or WASM. All previews use a sandbox. Authored external images and links can still access the network. These demos render standalone Markdown; they do not load a site's configuration or run its complete build lifecycle.