Docs

Writing performant pages

Avoid intermediate allocations with direct-buffer text, class composition, numeric rendering, and reusable buffers.

Open Markdown

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:

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:

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.

BufferBest forAllocation
RenderBufferProduction servers, tests, app-owned buffered renders4 KB pages recycled across renders
ZeroCopyBufCustom scatter-gather transport experimentsStatic bytes are referenced, dynamic bytes are owned
StreamingZeroCopyBufStreaming responsesSegmented 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

PatternWhyFix
format!() in html!Allocates a String every renderInline segments for text, pre-compute for attrs
.to_string()Unnecessary allocation, primitives render directlyRemove the call: {count} not {count.to_string()}
.to_owned()Clones a &str into a String for no reasonUse the &str directly
String::from()Same as aboveUse a string literal or &str
.join()Collects into a StringUse a template for loop with a renderable final expression
.collect()Allocates a Vec then iteratesIterate the original iterator directly
vec![...]Heap-allocates a VecUse &[...] (a stack-allocated slice)
{"literal"}String literal in braces gets runtime-escapedUse "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:

When NOT to use it:

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

  1. Avoid temporary formatting strings. Use plain slots, adjacent text, or text! for text and non-URL attributes. Prepare URLs explicitly.
  2. Keep escaping as the default. Use SafeText only for a proven safe character set.
  3. Use ClassList for CSS class composition. It's const fn and zero-allocation.
  4. Prepare reusable display data once where practical. Retain typed numeric values when direct formatting is enough.
  5. Use RenderBuffer in production for buffer reuse across requests.
  6. Maximize static runs by keeping dynamic values at the leaves of your template.
  7. Never allocate in a loop. Each iteration should write directly to the output buffer.
  8. 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.

Search

Type at least 2 characters