2026-09-23·by Sijie Wang#standmeet#rendering#tikz

tikzjax

TikZ (precise math figures) — shipped

Parent: rendering-engines

Fills the gap mermaid can't: precise mathematical figures — geometry constructions, commutative diagrams, coordinate/labeled boxes, function curves. A ```tikz fence renders to SVG. Live in the reader.

Engine symmetry is why TikZ won. node-tikzjax is the same engine as the Obsidian TikZJax plugin, so the owner draws TikZ in Obsidian and StandMeet renders the identical figure — the same argument as katex / mermaid. (Weighed and rejected: quiver, Mafs, function-plot, JSXGraph, Penrose — the need is static precise figures.)

It renders on the SERVER, not in the browser

The original sketch was browser-side WASM with the payload lazy-loaded or gated behind a setting. That is not what shipped, and the reversal is the design. The TeX engine runs server-side (/render-tikz, a Node-runtime route, usecase in lib/render/tikz); the client's TikZBlock POSTs the source and receives SVG. The heavy WASM never reaches a visitor.

The real cost turned out to be elsewhere: serverExternalPackages + outputFileTracingIncludes, because the TeX runtime assets (core.dump.gz, tex.wasm.gz, tex_files.tar.gz) are read from the filesystem, not imported, so Next's tracer does not carry them into the standalone build — and without them the route ENOENTs and hangs.

The engine is not reentrant — this is load-bearing

node-tikzjax drives a single in-process WASM TeX engine. Two renders that overlap trample each other and both throw TeX engine render failed. A reader page fires one POST per diagram at once, so a note with four figures lost a random subset of them — the reader got a slab of raw LaTeX where a picture should be, and which ones failed changed between loads.

Measured on prod: the same source returned 200 alone and 422 twice over when sent as two concurrent requests, answering in 0.6s — nowhere near the 25s timeout, so it was never a slowness problem.

Renders are therefore queued: one enters the engine at a time. The timeout starts when a render actually begins, not when it joins the queue, so a diagram waiting its turn is not judged slow. Per-process queue; multiple replicas each serialize their own, which is enough because the singleton is per-process anyway.

Fonts must be self-hosted

The SVG's text is <text> + font-family: cmr10 / cmsy10 / …, and the characters are TeX font slots, not Unicode: $\to$ sits at 0x21 in cmsy10 — that is !. Without the fonts the browser falls back, the arrow becomes an exclamation mark, and metrics shift enough to split words (stochasticsto chastic).

The package ships embedFontCss: false with a default fontCssUrl pointing at jsDelivr. Both are wrong for a self-hosted product: an offline instance cannot fetch them, and every figure would make one third-party request on the owner's readers' behalf. The fonts are served by the instance (scripts/copy-tikz-fonts.mjs moves them into public/ at build).

Failure is not the reader's problem

A failed render used to print the LaTeX source into the page. mermaid had already settled this question — the owner sees the diagnostic, the visitor sees nothing, and the failure is logged either way — and TikZ simply never got that treatment. Loading now shows a placeholder rather than source, which matters more once renders are queued: on a multi-figure page the last one waits several seconds, and those seconds must not be a wall of LaTeX.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →