ggtypst compiles Typst source to SVG through a Rust
backend and then imports the SVG into ggplot2 as a grid
grob. This article presents benchmark results obtained by running
tools/benchmark.R, so you can estimate how long rendering
will take in your own workflows.
In summary, pure Typst rendering is fast, but full plot rendering is
much more expensive than the Typst compilation step alone on this
machine. Rendering many Typst objects in a single plot therefore becomes
noticeably slower than rendering one-off annotations. For simple
rich-text styling without math, ggtext is substantially
faster in the geom path.
ℹ️ In actual use, avoid rendering more than about 50 Typst objects in a single plot.
Test Environment
| Item | Value |
|---|---|
| CPU | Intel Core i9-12900H |
| RAM | 32 GB |
| OS | Arch Linux |
| R | 4.5.3 |
| ggtypst | 0.1.0 |
| Rust | 1.89.0+ (release build with LTO) |
All timings were collected with base R proc.time() in
manual loops after a 3-iteration warmup that forces Rust font loading
and runtime initialization. A full GC was run before each iteration.
Layer 1: Typst Rendering
This layer measures only build_typst_source() +
typst_svg() — the Typst source generation and Rust
compilation step — without any ggplot2 overhead or
SVG-to-grob import.
Results
| Expression | Median | Min | Max |
|---|---|---|---|
plain_short — "Hello"
|
2.0 ms | 1.0 ms | 2.0 ms |
plain_styled —
"*bold* _italic_ ..."
|
2.0 ms | 1.0 ms | 3.0 ms |
math_simple —
$sum_(i=1)^n ...$
|
2.0 ms | 2.0 ms | 3.0 ms |
math_moderate —
$integral_0^1 ...$
|
2.0 ms | 1.0 ms | 3.0 ms |
math_complex — Fourier integral with
matrices |
5.0 ms | 4.0 ms | 6.0 ms |
mitex_simple —
\frac{1}{2} + \sqrt{3}
|
5.0 ms | 4.0 ms | 15.0 ms |
mitex_complex — Fourier integral
(LaTeX) |
8.0 ms | 7.0 ms | 10.0 ms |
Analysis
The Rust compilation engine is fast. Plain text and simple math all render in about 2 ms. Even the most complex Typst math expression — a display-style Fourier integral with nested matrices, cases, underbrace, and overbrace — only takes about 5 ms.
The MiTeX conversion (LaTeX → Typst) adds a visible
overhead. The simple MiTeX case takes about 6 ms (3 times that of a
plain Typst compile), and the complex one takes about 8 ms. The extra
cost comes from the LaTeX-to-Typst transpilation step followed by
Typst’s eval() call inside a module scope.
Key takeaway: Typst compilation itself is not the bottleneck. Even the heaviest expression compiles in well under 10 ms.
Layer 2: Full Plot Performance
This layer measures build-and-draw time from already assembled
ggplot objects: Typst compilation, SVG import, and grid
drawing to a null PDF device.
2A: API Type Comparison
All tests use the same math content:
sum_(i=1)^n x_i.
| Expression | Median | Min | Max |
|---|---|---|---|
base plot |
56 ms | 51 ms | 82 ms |
annotate (1 item) |
64 ms | 59 ms | 72 ms |
annotate (5 items) |
124 ms | 111 ms | 142 ms |
geom (1 row) |
173 ms | 167 ms | 245 ms |
geom (10 rows) |
1.19 s | 1.13 s | 1.28 s |
geom (50 rows) |
5.64 s | 5.38 s | 5.96 s |
element (title) |
171 ms | 158 ms | 180 ms |
A base ggplot2 scatter plot with no ggtypst
content takes about 56 ms to render. A single
annotate_math_typst() call increases that to about
64 ms, so the additional cost for one small annotation
is modest.
With 5 annotations on one plot (a mix of
annotate_typst(), annotate_math_typst(), and
annotate_math_mitex()), the total comes to 124
ms. That is still cheaper than a large data-driven label layer,
because the plot-level overhead is shared across only a handful of
objects.
geom_math_typst() with a single row takes about
173 ms, much slower than the annotate path. Relative to
the 56 ms base plot, a single geom row adds a little over 100 ms of
extra work. As row count grows, the timings increase roughly
linearly.
element_math_typst() applied to plot.title
takes about 171 ms. Relative to the base plot, a
Typst-rendered title has roughly the same overhead as one geom row.
2B: Content Complexity
All tests use annotate_typst() at a fixed position.
| Expression | Content | Median |
|---|---|---|
text_only |
"Hello from ggtypst" |
66 ms |
text_styled |
"*bold* _italic_ #text(fill: red)[colored]" |
66 ms |
math_inline |
"Result: $x^2 + y^2 = r^2$" |
66 ms |
math_display |
"$integral_0^1 frac(...)$" |
68 ms |
math_massive |
Fourier integral with matrices | 168 ms |
For simple to moderate content, the full plot time stays in the 66–68 ms range, only a little above the 56 ms base-plot baseline. That supports the same broad conclusion as Layer 1: for small labels, pure Typst compilation is not the dominant part of end-to-end plotting time.
The massive formula (168 ms) is about 2.5 times that of the simple cases. This benchmark does not isolate which downstream step is responsible, but it shows that very large expressions incur substantially more end-to-end cost than ordinary text or small formulas. In most use cases, this is acceptable.
2C: Geom Row Scaling
All rows use the same label:
"*Point* #linebreak() text".
| Rows (n) | Median | Per-Row Cost |
|---|---|---|
| 1 | 185 ms | 129 ms above baseline |
| 5 | 684 ms | ~126 ms per row above baseline |
| 10 | 1.39 s | ~133 ms per row above baseline |
| 25 | 3.35 s | ~132 ms per row above baseline |
| 50 | 7.05 s | ~140 ms per row above baseline |
The scaling is close to linear. After subtracting
the 56 ms base-plot baseline, each additional row
contributes roughly 125–140 ms on this machine. That is
much larger than the 2–8 ms pure Typst rendering times from Layer 1, so
the dominant cost in large geom_typst() layers lies
somewhere in the rest of the plotting pipeline rather than in Typst
compilation alone.
2D: ggtypst vs ggtext for styled text
This section compares ggtypst with ggtext in a
narrower scenario: simple styled text with bold and italic formatting,
but no math. The goal is not to compare feature completeness. Instead,
it measures the end-to-end rendering cost of similar rich-text
labels.
To keep the comparison fair, the ggtext layers disable
the label background, outline, and padding. That way, the test focuses
on text rendering rather than text-box decoration.
2D-1: Annotate comparison
Equivalent label content:
-
ggtypst:*Bold* _italic_ plain text -
ggtext:<b>Bold</b> <i>italic</i> plain text
| Expression | Median | Min | Max |
|---|---|---|---|
base plot |
54 ms | 51 ms | 62 ms |
ggtypst annotate (1) |
68 ms | 62 ms | 74 ms |
ggtext annotate (1) |
79 ms | 76 ms | 144 ms |
ggtypst annotate (5) |
172 ms | 130 ms | 5.45 s |
ggtext annotate (5) |
199 ms | 172 ms | 309 ms |
For a single styled annotation, the two packages are in the same
general range. ggtypst is slightly faster here, at about
68 ms versus 79 ms for
ggtext. With five annotations, both remain interactive, but
the ggtypst results become more variable because each label
still goes through Typst rendering and SVG import.
2D-2: Geom comparison
All rows use the same equivalent styled-text label, repeated with no
ggtext text-box styling.
| Expression | Median | Min | Max |
|---|---|---|---|
ggtypst geom (1) |
318 ms | 308 ms | 412 ms |
ggtext geom (1) |
65 ms | 63 ms | 70 ms |
ggtypst geom (10) |
2.72 s | 2.67 s | 2.89 s |
ggtext geom (10) |
111 ms | 107 ms | 119 ms |
ggtypst geom (50) |
13.59 s | 12.30 s | 15.88 s |
ggtext geom (50) |
334 ms | 320 ms | 437 ms |
The geom comparison shows the clearest difference. For repeated
styled-text labels, ggtext is dramatically faster at every
tested row count. That makes sense: ggtext renders
Markdown/HTML text directly as plot text, while ggtypst
renders each label through Typst and then imports the resulting SVG back
into the plot.
The conclusion is therefore simple: if you only need lightweight
rich-text styling such as bold and italic labels, ggtext is
the faster tool. If you need Typst syntax, higher-quality mathematical
notation, or a unified workflow for text and math, ggtypst
provides capabilities that ggtext does not target.
Practical Guidelines
annotate_*()for one-off labels: At ~62 ms per call in the math benchmark, annotations are effectively instant in interactive use.geom_*()scales roughly linearly: After subtracting the base plot, each row costs about 125–140 ms in the math benchmark. For 10 rows expect about 1.1-1.3 s; for 50 rows expect about 5.5-6.5 s. Usegeom_typst()for small data layers and avoid over-labeling a single plot.ggtextis faster for simple styled text: If you only need basic bold/italic rich text in annotations or label layers,ggtexthas much lower end-to-end rendering cost, especially forgeomuse cases.element_*()is fine for a few theme labels: A single Typst-rendered title took about 167 ms here, so titles, subtitles, and axis titles are reasonable uses. This benchmark does not evaluate dense tick-label or facet-heavy cases.Content complexity and LaTeX → Typst conversion usually have little impact: For simple content the end-to-end cost changes very little, and the pure Typst/MiTeX step stays in the 2–8 ms range. Only very large expressions show a major increase in full plotting time.
Choose the tool by the rendering task: Use
ggtextwhen you want lightweight Markdown/HTML styling only. Useggtypstwhen you need Typst markup, native Typst math, MiTeX-backed LaTeX math, or a single rendering system for both text and formulas.
