thanos

thanos

PhoenixPrerender - static prerendering and incremental static regeneration for Phoenix

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

  1. Build time: mix phoenix.prerender renders your marked routes through the full Phoenix endpoint pipeline and writes static HTML files to disk
  2. Runtime: A plug intercepts requests and serves those files directly — ~200μs instead of ~22ms (110x faster)
  3. 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 macroprerender do ... end wraps routes cleanly, or use metadata: %{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-session attributes so the browser hydrates normally
  • Atomic writes — write to .tmp then rename, so you never serve a partial file
  • Concurrent generation — parallel rendering with Task.async_stream
  • Manifest + sitemap — automatic manifest.json (with checksums) and sitemap.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-Control header 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

Feedback, issues, and PRs welcome. Would love to hear if this is useful for your projects.

https://github.com/thanos/phoenix_prerender

Most Liked

greven

greven

Reporting back.
Added it to my site as I said I would above.
Works wonders! Pages loaded fast but now they are lightning fast. :smiley:

Good job man, really nice library. This is such a good idea I could it see it being part of Phoenix itself.
I will report any problems I find.

Now I to add the mix command to prerender pages to before I release to keep them updated. :slight_smile:

PS - My server is the smalleste instance of a Hetzner VPS, so 2 vCPU / 4GB RAM.

Where Next?

Popular in Announcing Top

Eiji
ExApi is a library that I’m developing now and hope release soon This library will allow to: list all apis list all api implementation...
New
josevalim
Yes, yet another parser combinator library! Most of the parser combinators in the ecosystem are either compile-time, often using AST tra...
159 19228 141
New
blatyo
The best overview for how things are tied together is this presentation. Modules and functions are pretty well documented at this point, ...
New
pkrawat1
Presenting Aviacommerce, open source e-commerce platform in Elixir Aviacommerce is an open source e-commerce platform in Elixir. We at...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
dbern
I’m excited to announce that TaxJar has developed and open-sourced DateTimeParser. We developed it because we found a need to parse user ...
New
mathieuprog
Hello :waving_hand: Allow me to introduce you to Tz, an alternative time zone database support to Tzdata. Why another library? First a...
New
ostinelli
Let’s write a database! Well not really, but I think it’s a little sad that there doesn’t seem to be a simple in-memory distributed KV da...
New
mtrudel
Bandit is an HTTP server for Plug and WebSock apps. Bandit is written entirely in Elixir and is built atop Thousand Island. It can serve...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New

Other popular topics Top

johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New

We're in Beta

About us Mission Statement