Rendering & Performance
Where output goes, and what it costs to put it there.
The @render Macro
@render turns a template into output. Where that output goes depends on the destination it is given.
Basic Rendering
With no destination @render returns a String. A block renders each element in turn:
using HypertextTemplates
using HypertextTemplates.Elements
# Render to String (default)
html = @render @div "Hello, World!"<div>Hello, World!</div>
# Render multiple elements
html2 = @render begin
@h1 "Title"
@p "Paragraph"
end<h1>Title</h1>
<p>Paragraph</p>
Rendering to IO
Pass an IO as the first argument and the render writes through to it, with no string built along the way. That is what a server response or a file wants.
using HypertextTemplates
using HypertextTemplates.Elements
# Render to IOBuffer
buffer = IOBuffer()
@render buffer @div begin
@h1 "Small Document"
for i in 1:3
@p "Paragraph $i"
end
end
result = String(take!(buffer))<div>
<h1>Small Document</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
<p>Paragraph 3</p>
</div>
Output Type Control
Specify the desired output type:
using HypertextTemplates
using HypertextTemplates.Elements
# Render to String (explicit)
str = @render String @div "Content"
println(typeof(str), ": ", str)
# Render to Vector{UInt8}
bytes = @render Vector{UInt8} @div "Binary content"
println(typeof(bytes), ": ", String(bytes))String: <div>Content</div>
Vector{UInt8}: <div>Binary content</div>What a Render Allocates
A render allocates a few hundred bytes for the context it carries, then nothing per element. There is no DOM, nothing is concatenated, and escaping a value builds no intermediate string. A three item list and a three thousand item one cost the same. What grows with the page is the destination's own storage, the buffer behind a String or a Vector{UInt8}; write to a socket or a file and even that goes away.
Direct IO Streaming
Given an IO, the render writes to it as it goes:
using HypertextTemplates
using HypertextTemplates.Elements
title = "My Article"
paragraphs = ["First paragraph.", "Second paragraph.", "Third paragraph."]
io = IOBuffer()
@render io @article begin
@h1 $title
for paragraph in paragraphs
@p $paragraph
end
end
result = String(take!(io))
# The writes it makes are: "<article>", "<h1>", the escaped title,
# "</h1>", and so on to the closing tag.<article>
<h1>My Article</h1>
<p>First paragraph.</p>
<p>Second paragraph.</p>
<p>Third paragraph.</p>
</article>
Static Structure Becomes Constants
A tag name and the run of literal attributes that follows it are known while the macro expands, so they are merged into single string constants there. Rendering writes those constants out; the work left at run time is the interpolated values and the control flow around them.
using HypertextTemplates
using HypertextTemplates.Elements
@component function static_heavy()
@div {class = "container"} begin
@header {class = "header"} begin
@nav begin
@a {href = "/"} "Home"
@a {href = "/about"} "About"
end
end
@article {class = "content"} begin
@__slot__ # Only dynamic part
end
end
end
@deftag macro static_heavy end
html = @render @static_heavy begin
@p "This is the dynamic content that goes in the slot."
end<div class="container">
<header class="header">
<nav><a href="/">Home</a><a href="/about">About</a></nav>
</header>
<article class="content">
<p>This is the dynamic content that goes in the slot.</p>
</article>
</div>
StreamingRender
For large documents or slow-loading content, use StreamingRender to send content as it becomes available.
Basic Streaming
using HypertextTemplates
using HypertextTemplates.Elements
# Create a streaming render iterator
stream = StreamingRender() do io
@render io @div begin
@h1 "Streaming Example"
# Render multiple sections
for i in 1:3
@section begin
@h2 "Section $i"
@p "This is paragraph $i"
end
end
end
end
# Consume the stream
chunks = String[]
for chunk in stream
push!(chunks, String(chunk))
end
println("Streamed $(length(chunks)) chunks:")
for (i, chunk) in enumerate(chunks)
println("Chunk $i: ", repr(chunk))
endStreamed 1 chunks:
Chunk 1: "<div><h1>Streaming Example</h1><section><h2>Section 1</h2><p>This is paragraph 1</p></section><section><h2>Section 2</h2><p>This is paragraph 2</p></section><section><h2>Section 3</h2><p>This is paragraph 3</p></section></div>"HTTP Streaming
Each chunk is a Vector{UInt8} ready to write to a response body. HTTP.jl is not a dependency of this manual, so the example below is not executed here:
using HTTP
using HypertextTemplates, HypertextTemplates.Elements
function handle_request(req)
return HTTP.Response(200, ["Content-Type" => "text/html"]) do io
for chunk in StreamingRender() do render_io
@render render_io @html begin
@head @title "Streaming Page"
@body begin
@h1 "Live Data"
for i in 1:1000
@p "Item $i"
end
end
end
end
write(io, chunk)
end
end
endMicro-batching
A render makes thousands of small writes. Handing each one to the consumer separately would cost more than the render does, so they are batched. A write of immediate_threshold bytes or more goes over as its own chunk. Smaller ones accumulate until the batch fills, until 64 of them have arrived, or until a timer fires a millisecond after the last flush. Chunk boundaries therefore follow the batching and have nothing to do with where elements begin and end.
using HypertextTemplates
using HypertextTemplates.Elements
chunks = String[]
for chunk in StreamingRender() do io
@render io @ul begin
for i in 1:20
@li "Item $i"
end
end
end
push!(chunks, String(chunk))
end
println("Total chunks: ", length(chunks))
println("First chunk: ", repr(first(chunks)))Total chunks: 2
First chunk: "<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li><li>Item 6</li><li>Item 7</li><li>Item 8</li><li>Item 9</li><li>Item 10</li><li>Item 11</li><li>Item 12</li><li>Item 13</li><li>Item 14</li><li>Item 15</li><li>Item 16"StreamingRender Configuration
Three keywords control the batching. chunk_size is how many bytes a batch may reach before it is flushed, clamped to 256. immediate_threshold is the write size that bypasses batching. buffer_size is how many chunks the channel holds before the render task blocks, which is what applies backpressure when a consumer reads more slowly than the render produces.
using HypertextTemplates
using HypertextTemplates.Elements
@component function sample_document()
@div begin
@h1 "Document Title"
for i in 1:10
@section begin
@h2 "Section $i"
@p "Content for section $i"
end
end
end
end
@deftag macro sample_document end
chunks = String[]
for chunk in StreamingRender(;
chunk_size = 128,
immediate_threshold = 128,
buffer_size = 8,
) do render_io
@render render_io @sample_document
end
push!(chunks, String(chunk))
end
println("Configured streaming produced ", length(chunks), " chunks")Configured streaming produced 6 chunksStopping a Stream Early
Rendering runs in its own task, so a consumer that stops reading part way through leaves that task blocked with nowhere to put the next chunk. close releases it:
using HypertextTemplates
using HypertextTemplates.Elements
stream = StreamingRender() do io
@render io @ul begin
for i in 1:10_000
@li "Item $i"
end
end
end
first_chunk = nothing
for chunk in stream
global first_chunk = String(chunk)
break
end
close(stream)
println("Read ", length(first_chunk), " bytes, then closed the stream")Read 246 bytes, then closed the streamReading a stream to the end closes it for you, so close is only needed when a client disconnects or the consumer has seen enough.
Advanced Patterns
Buffered Rendering
A fragment can be rendered on its own and spliced in later, which is what caching part of a page needs. The buffer holds markup this code produced, so it goes back in as a SafeString:
using HypertextTemplates
using HypertextTemplates.Elements
@component function left_content()
@nav begin
@h3 "Navigation"
@ul begin
@li @a {href = "/"} "Home"
@li @a {href = "/about"} "About"
@li @a {href = "/contact"} "Contact"
end
end
end
@component function right_content()
@article begin
@h2 "Main Content"
@p "This is the main content area."
@p "It contains the primary information."
end
end
@component function two_column_layout(; left_content, right_content)
left_buffer = IOBuffer()
right_buffer = IOBuffer()
@render left_buffer @div {class = "column-left"} @<left_content
@render right_buffer @div {class = "column-right"} @<right_content
@div {class = "two-column"} begin
@text SafeString(String(take!(left_buffer)))
@text SafeString(String(take!(right_buffer)))
end
end
@deftag macro two_column_layout end
html = @render @two_column_layout {left_content, right_content}<div class="two-column">
<div class="column-left">
<nav>
<h3>Navigation</h3>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<div class="column-right">
<article>
<h2>Main Content</h2>
<p>This is the main content area.</p>
<p>It contains the primary information.</p>
</article>
</div>
</div>
Loading Data During a Render
A component runs while the page renders. It can fetch what it needs at that point, and the caller does not have to prepare everything up front:
using HypertextTemplates
using HypertextTemplates.Elements
@component function render_data(; data)
@div begin
@h3 $(data.title)
@ul begin
for item in data.items
@li $item
end
end
end
end
@component function lazy_section(; data_loader)
@div {class = "lazy-load"} begin
data = data_loader()
if isnothing(data)
@p "No data available"
else
@<render_data {data}
end
end
end
@deftag macro lazy_section end
# Stands in for a database call
function expensive_database_query()
return (title = "Query Results", items = ["Result 1", "Result 2", "Result 3"])
end
function empty_query()
return nothing
end
# Example with data
html1 = @render @lazy_section {
data_loader = () -> expensive_database_query()
}<div class="lazy-load">
<div>
<h3>Query Results</h3>
<ul>
<li>Result 1</li>
<li>Result 2</li>
<li>Result 3</li>
</ul>
</div>
</div>
# Example without data
html2 = @render @lazy_section {
data_loader = () -> empty_query()
}<div class="lazy-load">
<p>No data available</p>
</div>
Progressive Enhancement
Render basic content first, enhance later:
using HypertextTemplates
using HypertextTemplates.Elements
@component function progressive_gallery(; images)
@div {class = "gallery"} begin
for img in images
@img {
src = img.thumbnail,
"data-full-src" := img.full_size,
loading = "lazy",
alt = img.alt
}
end
# Enhancement script
@script """
// Progressively load full images
document.querySelectorAll('[data-full-src]').forEach(img => {
// Load full size when visible
});
"""
end
end
@deftag macro progressive_gallery end
# Example usage
images = [
(thumbnail = "/thumb1.jpg", full_size = "/full1.jpg", alt = "Image 1"),
(thumbnail = "/thumb2.jpg", full_size = "/full2.jpg", alt = "Image 2"),
(thumbnail = "/thumb3.jpg", full_size = "/full3.jpg", alt = "Image 3")
]
html = @render @progressive_gallery {images}<div
class="gallery"><img src="/thumb1.jpg" data-full-src="/full1.jpg" loading="lazy" alt="Image 1"><img src="/thumb2.jpg" data-full-src="/full2.jpg" loading="lazy" alt="Image 2"><img src="/thumb3.jpg" data-full-src="/full3.jpg" loading="lazy" alt="Image 3"><script>// Progressively load full images
document.querySelectorAll('[data-full-src]').forEach(img => {
// Load full size when visible
});
</script></div>
Best Practices
1. Choose the Right Output
- Use
IOfor server responses - Use
Stringfor testing or caching - Use
StreamingRenderfor large/slow content
2. Minimize Dynamic Content
using HypertextTemplates
using HypertextTemplates.Elements
# Good: Static structure, dynamic content
@component function good_list(; items)
@ul {class = "static-class"} begin
for item in items
@li $item # Only content is dynamic
end
end
end
@deftag macro good_list end
# Less optimal: Dynamic structure
function compute_class(items)
length(items) > 5 ? "long-list" : "short-list"
end
function compute_item_class(item)
startswith(item, "A") ? "a-item" : "other-item"
end
@component function suboptimal_list(; items)
@ul {class = compute_class(items)} begin # Computed every render
for item in items
@li {class = compute_item_class(item)} $item
end
end
end
@deftag macro suboptimal_list end
# Example usage
items = ["Apple", "Banana", "Cherry"]
@render @good_list {items}<ul class="static-class">
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
@render @suboptimal_list {items}<ul class="short-list">
<li class="a-item">Apple</li>
<li class="other-item">Banana</li>
<li class="other-item">Cherry</li>
</ul>