Performance Tuning
View SourceOptimize Mau template compilation and rendering for maximum performance.
Overview
This guide covers performance optimization techniques for Mau templates, from compilation to rendering and filter usage.
Template Compilation
Compile Once, Render Many
The most important optimization: compile templates once and reuse the AST.
# ❌ Bad: Compiles on every render (expensive)
defmodule MyApp.BadExample do
def render_user(user_data) do
template = "User: {{ name }}, Email: {{ email }}"
{:ok, output} = Mau.render(template, user_data)
output
end
end
# ✅ Good: Compile once at startup
defmodule MyApp.GoodExample do
@user_template """
User: {{ name }}, Email: {{ email }}
"""
@compiled_template elem(Mau.compile(@user_template), 1)
def render_user(user_data) do
{:ok, output} = Mau.render(@compiled_template, user_data)
output
end
endPre-compile in Application Init
For applications with many templates, pre-compile during startup:
defmodule MyApp.Templates do
@moduledoc """
Pre-compiled templates for the application.
"""
# Compile all templates at startup
def init_templates do
%{
user_card: compile_template("user_card.html"),
email_welcome: compile_template("email_welcome.html"),
report_summary: compile_template("report_summary.txt")
}
end
defp compile_template(filename) do
content = File.read!(Path.join(["templates", filename]))
{:ok, ast} = Mau.compile(content)
ast
end
end
# Usage in application startup
defmodule MyApp.Application do
use Application
def start(_type, _args) do
# Pre-compile all templates
templates = MyApp.Templates.init_templates()
Application.put_env(:my_app, :compiled_templates, templates)
# ... rest of startup
end
endCache Compiled Templates
Store compiled templates in ETS for fast access:
defmodule MyApp.TemplateCache do
@cache_table :template_cache
def init do
:ets.new(@cache_table, [:named_table, :public, :set])
end
def get_or_compile(name, template_string) do
case :ets.lookup(@cache_table, name) do
[{^name, ast}] ->
{:ok, ast}
[] ->
case Mau.compile(template_string) do
{:ok, ast} ->
:ets.insert(@cache_table, {name, ast})
{:ok, ast}
error ->
error
end
end
end
def clear do
:ets.delete_all_objects(@cache_table)
end
endRendering Optimization
Use Type Preservation Wisely
Type preservation adds overhead - use only when needed:
# ❌ Unnecessary type preservation
{:ok, output} = Mau.render("Count: {{ items | length }}", context, preserve_types: true)
# Result: "Count: 3" (string anyway)
# ✅ Smart type preservation
{:ok, result} = Mau.render("{{ total }}", context, preserve_types: true)
# Result: 1500 (number, no string conversion)Set Appropriate Loop Limits
Prevent runaway loops with realistic limits:
# Dangerous: User could create infinite-like loops
{:ok, output} = Mau.render(user_template, context)
# Safe: Limit iterations
{:ok, output} = Mau.render(
user_template,
context,
max_loop_iterations: 5000 # Reasonable limit for most cases
)Batch Rendering
For multiple templates with same context, batch them:
# ❌ Inefficient: Processes context separately
results =
Enum.map(templates, fn template ->
{:ok, output} = Mau.render(template, context)
output
end)
# ✅ Efficient: Prepare context once
prepared_context = prepare_context(raw_context)
results =
Enum.map(templates, fn template ->
{:ok, output} = Mau.render(template, prepared_context)
output
end)
defp prepare_context(raw_context) do
%{
"name" => String.downcase(raw_context.name),
"items" => Enum.sort(raw_context.items),
"totals" => calculate_totals(raw_context)
}
endFilter Performance
Use Built-in Filters
Built-in filters are optimized in Elixir:
# ❌ Manual looping (slower)
def custom_filter(items, _args) do
result = []
for item <- items do
result = [item | result]
end
{:ok, Enum.reverse(result)}
end
# ✅ Use Enum (optimized)
def custom_filter(items, _args) do
{:ok, Enum.reverse(items)}
endChain Filters Efficiently
Order filters for best performance:
# ❌ Processes large list multiple times
{{ items | sort | reverse | first }}
# ✅ Filter before sort (smaller dataset)
{{ items | filter("status", "active") | sort | reverse | first }}Avoid N+1 Filter Problems
# ❌ Creates 1 lookup per item (N+1)
{% for item in items %}
{{ item | lookup_price(prices) }}
{% endfor %}
# ✅ Preprocess lookups before template
{:ok, enriched_items} = Mau.render_map(%{
"#map" => ["{{$items}}", %{
"id" => "{{$loop.item.id}}",
"price" => "{{$self.prices[$loop.item.id]}}"
}]
}, %{
"$items" => items,
"$self" => %{"prices" => prices_map}
})Context Optimization
Keep Context Minimal
Only include data that templates need:
# ❌ Large context with unused data
context = %{
"user" => all_user_data, # 50+ fields
"items" => all_items, # 10,000+ items
"settings" => all_settings # 100+ fields
}
# ✅ Minimal context with only needed data
context = %{
"user" => %{
"name" => user.name,
"email" => user.email
},
"items" => Enum.filter(all_items, &(&1.visible)),
"settings" => %{
"theme" => settings.theme
}
}Preprocess Complex Data
Transform data before passing to templates:
# ❌ Let template do all the work
context = %{
"orders" => raw_orders
}
# Template processes all orders
# ✅ Preprocess in application code
context = %{
"orders" => Enum.map(raw_orders, fn order ->
%{
"id" => order.id,
"total" => order.total,
"formatted_total" => format_currency(order.total),
"status" => status_label(order.status)
}
end)
}
# Template just displays preprocessed dataUse Lazy Evaluation
For large datasets, compute only when needed:
# ❌ Evaluates all summaries upfront
context = %{
"monthly_summaries" => Enum.map(1..12, &calculate_month_summary/1)
}
# ✅ Compute summaries in template only if used
context = %{
"months" => 1..12,
"calculate_summary" => &calculate_month_summary/1
}Map Directives Optimization
Use #filter Before #map
Filter collections before transforming:
# ❌ Maps everything then filters
input = %{
"results" => %{
"#map" => [
"{{$items}}",
%{"id" => "{{$loop.item.id}}"}
]
},
"active_only" => %{
"#filter" => ["{{results}}", "{{$loop.item.status == 'active'}}"]
}
}
# ✅ Filters first, then maps
input = %{
"active_results" => %{
"#pipe" => [
"{{$items}}",
[
%{"#filter" => "{{$loop.item.status == 'active'}}"},
%{"#map" => %{"id" => "{{$loop.item.id}}"}}
]
]
}
}Avoid Nested #map with Complex Logic
# ❌ Complex nested logic
%{
"#map" => [
"{{$data}}",
%{
"items" => %{
"#map" => [
"{{$loop.item.children}}",
%{
"status" => %{
"#if" => ["{{$loop.item.status}}", ...]
}
}
]
}
}
]
}
# ✅ Preprocess in application
preprocessed = Enum.map(data, fn item ->
%{
"items" => Enum.map(item.children, fn child ->
%{"status" => compute_status(child)}
end)
}
end)
{:ok, result} = Mau.render_map(%{
"items" => "{{$items}}"
}, %{"$items" => preprocessed})Benchmarking
Measure Performance
Use :timer.tc for benchmarking:
defmodule MyApp.Benchmarks do
def benchmark_template do
template = "Hello {{ name }}, you have {{ count }} items"
context = %{"name" => "Alice", "count" => 42}
# Warm up
Mau.render(template, context)
# Measure
{time_us, {:ok, _output}} = :timer.tc(Mau, :render, [template, context])
time_ms = time_us / 1000
IO.puts("Rendered in #{time_ms} ms")
end
def benchmark_filter do
{time_us, result} = :timer.tc(fn ->
Mau.FilterRegistry.apply("upper_case", "hello world", [])
end)
IO.puts("Filter took #{time_us / 1000} ms")
end
endUse Benchee for Comprehensive Testing
defmodule MyApp.BenchmarksWithBenchee do
def run do
Benchee.run(%{
"simple_render" => fn ->
{:ok, _} = Mau.render("{{ name }}", %{"name" => "Alice"})
end,
"complex_render" => fn ->
{:ok, _} = Mau.render(complex_template(), complex_context())
end,
"precompiled_render" => fn ->
{:ok, _} = Mau.render(precompiled_ast(), complex_context())
end
},
time: 10,
memory_time: 2
)
end
endCommon Performance Issues
Issue: Slow Template Rendering
Symptoms: Templates take seconds to render
Causes:
- Large datasets
- N+1 lookups in filters
- Unoptimized filters
Solutions:
# 1. Profile with :fprof
:fprof.start()
Mau.render(template, context)
:fprof.stop()
# 2. Use simpler templates for large datasets
# 3. Preprocess data in application
# 4. Add loop limits
Mau.render(template, context, max_loop_iterations: 5000)Issue: Memory Usage Growing
Symptoms: Application memory keeps increasing
Causes:
- Compiled templates not cached properly
- Unbounded context growth
- Large template strings
Solutions:
# 1. Use template cache
MyApp.TemplateCache.get_or_compile("my_template", template_source)
# 2. Clear old compiled templates periodically
:ets.delete_all_objects(:template_cache)
# 3. Use streaming for large contexts
Enum.each(large_dataset, fn item ->
context = %{"item" => item}
{:ok, output} = Mau.render(template, context)
IO.write(output)
end)Issue: Slow Filter Chains
Symptoms: Chained filters slow down template rendering
Causes:
- Multiple passes over data
- Inefficient filter order
Solutions:
# ❌ Slow: Multiple passes
{{ items | sort | reverse | map("name") | join(", ") }}
# ✅ Fast: Preprocess
preprocessed = items
|> Enum.sort()
|> Enum.reverse()
|> Enum.map(&(&1["name"]))
|> Enum.join(", ")
{{ preprocessed }}Caching Strategies
Fragment Caching
Cache rendered fragments:
defmodule MyApp.FragmentCache do
@cache_table :fragment_cache
def init do
:ets.new(@cache_table, [:named_table, :public, :set])
end
def render_cached(key, template, context, ttl_seconds \\ 3600) do
case :ets.lookup(@cache_table, key) do
[{^key, output, expiry}] ->
if System.os_time(:second) < expiry do
output
else
:ets.delete(@cache_table, key)
render_and_cache(key, template, context, ttl_seconds)
end
[] ->
render_and_cache(key, template, context, ttl_seconds)
end
end
defp render_and_cache(key, template, context, ttl) do
{:ok, output} = Mau.render(template, context)
expiry = System.os_time(:second) + ttl
:ets.insert(@cache_table, {key, output, expiry})
output
end
endBest Practices Summary
- Compile once, render many times
- Cache compiled templates
- Preprocess complex data
- Use type preservation selectively
- Set reasonable loop limits
- Filter before transformation
- Keep context minimal
- Profile and benchmark
- Batch operations
- Monitor memory usage
See Also
- Custom Filters - Creating efficient custom filters
- API Reference - Mau API options
- Map Directives - Directive optimization