Hi Elixir Drinkers!
I just published PhoenixPrerender, a library that brings static site generation and incremental static regeneration (ISR) to Phoenix — think Next.js ISR or SvelteKit prerendering, but built entirely on the BEAM.
I hope it’s of use. I been using it in production on several sites I thought I package it let it out into the big bad world.
The problem
Phoenix is great at rendering HTML, but for pages that rarely change (marketing pages, docs, changelogs), you’re paying the full rendering cost on every request — router matching, controller dispatch, template compilation, layout assembly. That adds up at scale.
What PhoenixPrerender does
- Build time:
mix phoenix.prerenderrenders your marked routes through the full Phoenix endpoint pipeline and writes static HTML files to disk - Runtime: A plug intercepts requests and serves those files directly — ~200μs instead of ~22ms (110x faster)
- ISR: When pages go stale, the next request serves the existing content immediately while triggering a background regeneration. Users never wait.
Setup takes 5 minutes
# 1. Add the dep
{:phoenix_prerender, "~> 0.1.0"}
# 2. Mark routes
import PhoenixPrerender
prerender do
get "/about", PageController, :about
get "/pricing", PageController, :pricing
live "/docs", DocsLive
end
# 3. Add the plug (before the router)
plug PhoenixPrerender.Plug
# 4. Configure
config :phoenix_prerender, enabled: true
# 5. Generate
# $ mix phoenix.prerender
That’s it. Everything else is opt-in.
Key features
- Router macro —
prerender do ... endwraps routes cleanly, or usemetadata: %{prerender: true}explicitly - Three-tier serving — ETS memory cache → disk → Phoenix fallback
- ISR — stale-while-revalidate with configurable TTL, ETS-based locks prevent thundering herd
- Distributed — cluster-wide locking via
:global.trans/2, cache invalidation via Phoenix PubSub - LiveView compatible — prerender LiveView routes; the static HTML includes
data-phx-sessionattributes so the browser hydrates normally - Atomic writes — write to
.tmpthen rename, so you never serve a partial file - Concurrent generation — parallel rendering with
Task.async_stream - Manifest + sitemap — automatic
manifest.json(with checksums) andsitemap.xml - Telemetry — events for generation, rendering, serving, and regeneration
- Strict paths — only serve paths listed in the manifest (enabled by default)
Benchmarks
Same /about page (10.6 KB), Apple M1 Max:
| Serving Mode | Throughput | Latency | Memory |
|---|---|---|---|
| Prerendered (ETS cache) | 5,000 req/s | 200 μs | 5.4 KB |
| Prerendered (disk) | 4,040 req/s | 248 μs | 8.3 KB |
| Dynamic (full Phoenix) | 45 req/s | 22,134 μs | 46.0 KB |
110x faster, 8.5x less memory per request.
What this is NOT
- Not a static site generator that replaces Phoenix — your app still runs normally
- Not a CDN — but pairs well with one (the
Cache-Controlheader is configurable) - Not needed for every page — contact forms, dashboards, personalized content should stay dynamic
Demo app
The repo includes a full demo app that showcases all four rendering modes side by side: prerendered controller pages, LiveView + prerender, ISR, and dynamic. You can run it locally with:
cd demo && mix deps.get && mix phx.server
Links
- Hex: phoenix_prerender | Hex
- GitHub: GitHub - thanos/phoenix_prerender: This library provides build-time prerendering of Phoenix routes and optional runtime ISR-style incremental regeneration. · GitHub
- Docs: PhoenixPrerender v0.1.0 — Documentation
Feedback, issues, and PRs welcome. Would love to hear if this is useful for your projects.






















