Docs
Writing performant pages
Avoid intermediate allocations with direct-buffer text, class composition, numeric rendering, and reusable buffers.
Proa's html! macro compiles static markup to byte literals at build time. At request time, only dynamic values are rendered. This guide covers how to keep the dynamic path fast.
The core principle: write authored output directly into the destination buffer instead of building temporary strings. Use WebRenderSync and html_sync! by default; the same techniques apply to WebRender and html! when async work is required.
This describes the template-writing path. Request contexts, loaders, managed metadata preparation, and response ownership have their own costs. Measure the complete request when evaluating an application; eliminating an intermediate formatting string does not make every request allocation-free.
Avoid format!()
format!() allocates a String on every call. In a render path that runs hundreds of thousands of times per second, this is the single most common performance mistake.
In text content, use inline segments
Instead of format!(), place string literals and expressions side by side. Each segment writes directly to the output buffer with zero allocation:
// Bad: allocates a String every render
html! {
<p>{format!("${} / night", price)}</p>
}
html! {
<p>{format!("Hello, {}!", name)}</p>
}
// Good: inline segments — zero allocation
html! {
<p>"$"{price}" / night"</p>
}
html! {
<p>"Hello, "{name}"!"</p>
}
How it works: "$" compiles into the static byte run (no runtime formatting), {price} renders the integer directly via itoa_lut (no .to_string()), and " / night" compiles into the next static byte run. No intermediate String is ever created.
More examples:
// Bad
html! {
<span>{format!("{} items in cart", count)}</span>
}
html! {
<title>{format!("{} - MyApp", page_title)}</title>
}
// Good
html! {
<span>{count}" items in cart"</span>
}
html! {
<title>{page_title}" - MyApp"</title>
}
In attributes, use typed values
Use text! for formatted text attributes. It writes directly to the output buffer:
html_sync! {
<button title={text!("Add {} items", count)}>"Add"</button>
}
For URL attributes, use a prepared URL or cx.url(path) for explicit application-base-path composition. Placeholder-bearing text! is rejected in URLs. Keep resource paths in your data rather than constructing a temporary URL during rendering:
// Bad: allocates a String every render
html! {
<img src={format!("/images/{}.webp", product.slug)} />
}
// Good: pre-compute in your data
struct Product {
image_path: &'static str, // "/images/alphafly-3.webp"
}
html! {
<img src={product.image_path} />
}
If a dynamic URL needs an owned string, construct it in the handler or view model and move the String into the deferred response component. Borrow as_str() while rendering it; a borrow of a handler-local string cannot outlive that handler. Computing it on every request still has a per-request cost. See Linking for cx.url(...) and mounted paths.
SafeText
SafeText<'a> wraps a &str and skips HTML escape scanning. Use it when you know the string contains no HTML-unsafe characters (<, >, &, ", ').
use proa::SafeText;
use proa::safe_text;
// Compile-time verified literal
safe_text!("Hello world")
// Runtime: a label whose allowed characters were validated before rendering
SafeText::from_trusted(validated_label)
SafeText copies verified text directly into the output buffer. Regular &str rendering scans every byte with memchr3 looking for unsafe characters. For strings with a proven safe character set, SafeText eliminates that scan. A value is not trusted merely because it came from a database; keep ordinary escaped strings as the default.
When NOT to use it: Never wrap user input in SafeText. If the string could contain <script>, you've created an XSS vulnerability.
ClassList
ClassList<N> composes CSS classes without string concatenation or allocation. All construction methods except add_opt are const fn.
use proa::ClassList;
// Compile-time class composition
const fn button_classes(primary: bool) -> ClassList<3> {
ClassList::new()
.add("inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium")
.add_if(primary, "bg-blue-600 text-white")
.add_if(!primary, "bg-gray-100 text-gray-900")
}
// In your component
html! {
<button class={button_classes(self.primary)}>{self.label}</button>
}
Available methods:
add(class)- always add (const)add_if(condition, class)- conditional (const)add_non_empty(class)- skip empty strings (const)add_opt(class)-Option<&'static str>(runtime)
ClassList renders by writing each segment directly to the buffer with space separators. No intermediate string is ever built.
Numeric rendering
Primitive numeric types implement Proa's render traits directly. Integers use a lookup-table algorithm (itoa_lut) that formats digits in pairs. Floats use zmij, which produces the shortest round-trip representation with Ryu-style exponent spelling. Both write directly to the output buffer.
// Good: direct numeric rendering, zero allocation
html! {
<span>{product.price_cents / 100}</span>
<span>{product.rating}</span>
}
// Bad: format!() for number formatting
html! {
<span>{format!("${}", product.price_cents / 100)}</span>
}
For formatted prices, text! writes the currency marker and numeric formatting directly:
html_sync! {
<span>{text!("${}.{:02}", price_cents / 100, price_cents % 100)}</span>
}
This example uses nonnegative integer cents. For locale-specific currency rules, prepare a display value using the application's formatter.
Pre-formatting pattern
When your data has values that need formatting, do it once at initialization, not per render.
// Bad: formats on every render
struct Product {
price_cents: u32,
color_hex: &'static str,
}
// In render:
html! {
<span>{format!("${}", self.price_cents / 100)}</span>
<div style={format!("background-color:{}", self.color_hex)} />
}
// Good: pre-formatted in static data
struct Product {
price: &'static str, // "$205"
color_style: &'static str, // "background-color:#26292B"
image_path: &'static str, // "/images/air-jordan-14.webp"
color_count: &'static str, // "3 Colors"
}
// In render: zero allocation
html! {
<span>{SafeText::from_trusted(self.price)}</span>
<div style={SafeText::from_trusted(self.color_style)} />
}
The footwear showcase uses preformatted display data. Timing depends on the fixture, build, machine, and buffer ownership; use a pinned benchmark and verify output before comparing results.
Static consolidation
The html! macro automatically merges consecutive static content into single byte literals at compile time. Understand this to write templates that maximize static runs.
// This entire template is one static byte literal at compile time:
html! {
<div class="flex items-center gap-4">
<h1>"Product title"</h1>
<span class="text-sm text-gray-500">"In stock"</span>
</div>
}
// Generated: static BYTES: &[u8] = b"<div class=\"flex items-center gap-4\">..."
// Render cost: one memcpy
// Dynamic values break static runs:
html! {
<div class="flex items-center gap-4">
<h1>{self.title}</h1> // breaks here
<span class="text-sm text-gray-500">
{self.stock_label} // breaks here
</span>
</div>
}
// Generated: 3 static byte literals + 2 dynamic renders
// Still fast, but more work than fully static
Tips for maximizing static runs:
- Put dynamic values at the leaves, not in wrapper elements
- Use pre-computed strings (SafeText) for values that rarely change
- Static attribute values are consolidated; dynamic ones break the run
Choosing a buffer
Proa supports multiple buffer types. Most applications should use one of the standard targets so the compiler does not repeatedly monomorphize the same component tree for many buffer shapes.
| Buffer | Best for | Allocation |
|---|---|---|
RenderBuffer | Production servers, tests, app-owned buffered renders | 4 KB pages recycled across renders |
ZeroCopyBuf | Custom scatter-gather transport experiments | Static bytes are referenced, dynamic bytes are owned |
StreamingZeroCopyBuf | Streaming responses | Segmented output with flush checkpoints |
Specialized writer variants, contiguous byte-vector buffers, and custom page sizes are benchmark tools or narrow adapter internals. Avoid copying them into normal app code; production routes should not depend on guessing an output size.
For production servers, use RenderBuffer:
// Create once, reuse across requests
let mut buf = RenderBuffer::new();
// Per request:
buf.render_into(component); // Clears first, then recycles pages as it renders
RenderBuffer grows by chaining 4 KB pages. When a page fills, it rotates to a new one. On clear(), pages are cached for reuse. Once its reusable capacity covers a workload, the buffer can avoid further page allocations. Components, context state, metadata, and transport finalization can still allocate independently. This low-level helper uses manual rendering and panics on render errors; use framework response helpers for managed documents.
Attribute rendering
Proa fuses attribute names and delimiters into single static string slices at compile time.
// The macro generates:
cx.out.extend_static_str(" class=\"")?; // single pointer, not 3 separate writes
value.render_attr_value(cx)?;
cx.out.extend_static_str("\"")?;
For attribute values you control, use SafeAttr to skip escape scanning:
use proa::SafeAttr;
html! {
<div data-page={SafeAttr("product-detail")} />
}
Loops and iteration
Use the standard Rust for pattern inside html!. Each iteration writes directly to the output buffer.
html! {
<ul class="space-y-2">
{for product in &self.products {
html! {
<li class="flex items-center gap-4">
<img src={product.image_path} />
<span>{SafeText::from_trusted(product.title)}</span>
<span>{SafeText::from_trusted(product.price)}</span>
</li>
}
}}
</ul>
}
Do not collect into a Vec<String> and then render. Each iteration should write directly.
The macro renders the loop body's final expression automatically. Leave off the
semicolon and .render(cx)?; explicit rendering is still needed in ordinary
Rust code outside a template slot.
Compile-time performance hints
Proa's html! macro detects common allocation patterns at compile time and emits compiler warnings. These don't block compilation, they surface mistakes before they reach production.
What gets flagged
| Pattern | Why | Fix |
|---|---|---|
format!() in html! | Allocates a String every render | Inline segments for text, pre-compute for attrs |
.to_string() | Unnecessary allocation, primitives render directly | Remove the call: {count} not {count.to_string()} |
.to_owned() | Clones a &str into a String for no reason | Use the &str directly |
String::from() | Same as above | Use a string literal or &str |
.join() | Collects into a String | Use a template for loop with a renderable final expression |
.collect() | Allocates a Vec then iterates | Iterate the original iterator directly |
vec![...] | Heap-allocates a Vec | Use &[...] (a stack-allocated slice) |
{"literal"} | String literal in braces gets runtime-escaped | Use "literal" as a text node (compile-time escaped) |
Example warnings
// This produces a compiler warning:
html! {
<p>{format!("${} / night", price)}</p>
}
The hint surfaces as a rustc warning anchored to the offending expression:
warning: unused `_perf_format__inside_html_allocates_a_String_on_every_render_0` that must be used
--> src/pages/room.rs:24:17
|
= note: perf: format!() inside html! allocates a String on every render.
for text content, use inline segments: "$"{price}" / night" — for
attributes, pre-compute the value before rendering
(docs/guides/performance). Wrap in perf_ok() to suppress if
intentional
Silencing a hint with perf_ok()
When you intentionally allocate in the render path, wrap the expression in perf_ok() to suppress the warning:
use proa::perf_ok;
// Flagged:
html! {
<span>{format!("${}", price)}</span>
}
// Silenced — you've acknowledged the allocation:
html! {
<span>{perf_ok(format!("${}", price))}</span>
}
perf_ok() is an #[inline(always)] identity function, it returns its argument unchanged and compiles away to nothing. It exists only as a signal to the scanner (and to code reviewers) that the allocation is intentional.
When to use it:
- Prototyping where speed of development matters more than render performance
- Rare code paths (error pages, admin panels) where allocation cost is negligible
- Complex formatting that can't be pre-computed (e.g., locale-dependent number formatting)
When NOT to use it:
- Hot paths (product listings, search results, home pages), pre-compute instead
- Inside
forloops, N allocations per render, compose typed children directly instead
Disabling all hints
To disable performance hints project-wide (e.g., during early prototyping):
[dependencies]
proa = { version = "0.4", features = ["no-perf-hints"] }
Remove the feature flag when you're ready to optimize.
Summary of rules
- Avoid temporary formatting strings. Use plain slots, adjacent text, or
text!for text and non-URL attributes. Prepare URLs explicitly. - Keep escaping as the default. Use
SafeTextonly for a proven safe character set. - Use
ClassListfor CSS class composition. It'sconst fnand zero-allocation. - Prepare reusable display data once where practical. Retain typed numeric values when direct formatting is enough.
- Use
RenderBufferin production for buffer reuse across requests. - Maximize static runs by keeping dynamic values at the leaves of your template.
- Never allocate in a loop. Each iteration should write directly to the output buffer.
- Use
perf_ok()to silence hints on intentional allocations, not to ignore them.
Reading the benchmark results
The current benchmark evidence
separates renderer-only time, complete HTTP responses, streaming, and cold start.
The November 11, 2026 Proa refresh measured commit bbbc968 on Apple M4 Max;
peer render, HTTP, and cold-start rows retain their October 21 captures.
The renderer-only Proa lane allocates a fresh owned RenderBuffer for each
response and keeps that buffer. It does not include flattening it into a
String. HTTP captures add the server's request and response path. Streaming
response-end timings need another distinction: Proa sends resolved HTML, while
SvelteKit sends promised data that the browser still has to render. Consult the
browser-completion results
when comparing that end-to-end work.
Choose the boundary your application uses and measure it with your own data, output ownership, host, and request load. The published timings describe the recorded captures rather than every Proa application.