Docs

WriteBuf

Pick the buffer a route renders into.

Open Markdown

All web rendering writes into a WriteBuf. The buffer controls allocation behaviour, capacity handling, and how the response is finalized.

Use RenderBuffer unless you have a reason not to.

src/pages/about.rs
use proa_core::{WebContext, WebRenderSync};

let mut cx = WebContext::new();
page.render(&mut cx)?;
let html = cx.into_output().into_string();

Choosing a buffer

BufferUse when
RenderBufferRoutes, tests, and adapters. The default.
ZeroCopyBufA custom transport experiment preserves large static byte slices.
StreamingZeroCopyBufYou are building a manual checkpoint-based streaming response.
SinkWriter<N>A legacy adapter explicitly selected the deprecated page-pool writer. See Sink.

RenderBuffer grows by chaining reusable 4 KB pages, never relocates bytes it has already written, and recycles pages after clear().

The last two rows belong to the streaming path. Sink covers them.

Reusing a buffer

For a long-lived service, keep the writer request-local or worker-local and clear it between renders:

src/render_worker.rs
use proa_core::{RenderBuffer, WebContext, WebRenderSync, WriteBuf};

pub struct RenderWorker {
    out: RenderBuffer,
}

impl RenderWorker {
    pub fn render_page<P>(&mut self, page: P) -> Result<&RenderBuffer, proa_core::WriteError>
    where
        P: WebRenderSync,
    {
        self.out.clear();
        let mut cx = WebContext::with_buffer(std::mem::take(&mut self.out));
        let result = page.render(&mut cx);
        self.out = cx.into_output();
        result?;
        Ok(&self.out)
    }
}

The caller flattens or streams the returned slices before the next clear().

For most Axum handlers, creating a fresh RenderBuffer per request and calling into_string() is also fine. Reuse matters when profiling shows allocation churn in a high-throughput render loop.

Good to know: Fixed-capacity writers and contiguous byte-vector helpers exist for low-level tests, benchmarks, and narrow adapters. Normal routes should not depend on guessing an output size.

Context state

WebContext carries the output buffer plus request-scoped render state: escape mode and dialect, request metadata and route params, an optional data loader, streaming recorders and flushers, and RSJS island usage records.

Components treat cx as the place to render children and write output. Route and framework code choose the buffer and finalize the response.

When the render tree needs request-scoped data loading:

src/pages/blog.rs
use std::sync::Arc;
use proa_core::WebContext;

let mut cx = WebContext::with_loader(Arc::new(loader));

At the route boundary

ZeroCopyBuf stores large static template slices as static parts and dynamic bytes in an owned buffer. Reach for it only when a custom transport preserves scatter-gather response structure instead of flattening immediately:

src/pages/home.rs
use proa_core::{WebContext, WebRenderSync, ZeroCopyBuf};

let mut cx = WebContext::with_buffer(ZeroCopyBuf::with_capacity(128, 16 * 1024));
page.render(&mut cx)?;
let out = cx.into_output();

This is the advanced custom-writer path: the page tree must explicitly support WebRenderSync<L, ZeroCopyBuf>. If you flatten the result right away, RenderBuffer and the default WebRenderSync<L> form are simpler.

Using proa_framework_axum directly? Return RouteResponse::ssr(Page) from the handler to use the standard RenderBuffer path. See Route responses.

Before you ship: use RenderBuffer for app routes and tests, call render_static first for docs routes, keep response finalization at the route boundary, and reach for zero-copy or sink-backed buffers only when the transport path actually uses them.

Next steps

Search

Type at least 2 characters