Hugo vs Astro vs Eleventy vs Jekyll vs Zola: an engineering evaluation
A technical evaluation of five static site generators — rendering lifecycles, CI build caching on Vercel/Netlify/Cloudflare, and architecture fits for large docs, component showcases, and long-lived blogs.
On this page
This is a build-time-behaviour comparison of five static site generators for an infrastructure migration: Hugo, Astro, Eleventy, Jekyll, and Zola. It covers how each compiles, how to cache each in CI, and which fits three concrete target profiles.
# Operational mechanics & rendering cycles
Every generator here resolves to the same output contract — flat HTML/CSS/JS on disk — but the pipeline that produces it differs in language runtime, concurrency, and what (if anything) is shipped to the browser.
Hugo is a single Go binary. It reads the content/ tree into a page-bundle graph, merges front matter with /data files (JSON/TOML/YAML) into the .Site.Data namespace, and renders through Go’s html/template. Assets pass through Hugo Pipes — Dart Sass, PostCSS, minification, fingerprinting, and bundling — before flat HTML is written to /public. Rendering is concurrent across goroutines, which is why it stays in low single-digit seconds for thousands of pages. No client runtime is emitted.
Zola is a single Rust binary with a similar shape and an even smaller surface. It parses content sections and pages (CommonMark via pulldown-cmark), reads TOML front matter, builds taxonomies, and renders through Tera. Sass compilation, syntax highlighting (syntect), image processing, and a search index are all built in — there is no plugin system and no JavaScript build step. Output lands in /public in one pass.
Jekyll is Ruby and Liquid. It reads _config.yml, _data sources (YAML/JSON/CSV/TSV), and collections (_posts plus any custom collection), renders Liquid to HTML, converts Sass via jekyll-sass-converter, and writes to _site. Rendering is single-threaded; --incremental regeneration exists but is coarse, so build time grows roughly linearly and becomes the bottleneck at scale.
Astro runs on Node and Vite, and the compiler boundary is the whole story. The Astro compiler transforms .astro files into JavaScript render functions. In static output mode those functions execute at build time to produce HTML strings — server-side, discarded after render. Only components carrying a client:* directive (load, idle, visible, media, only) are compiled into hydration bundles. Vite owns the module graph: code-splitting, asset hashing, and emitting each island as a separate ESM chunk paired with a small hydration runtime. Content Collections validate front matter against Zod schemas at build. The net effect is zero-JS HTML for everything that isn’t explicitly an interactive island.
Eleventy is Node with no client runtime, and its distinguishing mechanism is the data cascade. Data is merged from multiple sources in a fixed priority order (highest wins): computed data (eleventyComputed), front matter in the template, template data files (post.11tydata.js), directory data files ascending toward the template, front matter in layouts, then global _data. Because every template language it supports — Nunjucks, Liquid, Markdown, 11ty.js, Handlebars, and others — resolves against the same cascade and layout chain at build time, you can mix languages across a single site and still reason about variable resolution deterministically.
# Caching & CI pipeline integration
The deployment footprint splits by runtime. Vercel and Netlify auto-detect the Node generators (Astro, Eleventy) from package.json; Hugo, Jekyll, and Zola run through an explicit build command (Vercel’s “Other” preset, or a netlify.toml [build] command). Cloudflare Pages takes a build command plus an output directory and runs it in a Node-oriented build image, so non-Node binaries must be fetched in the build step. In all three, the output directory is the only thing published — /public (Hugo, Zola), _site (Jekyll/Eleventy default), or /dist (Astro).
Effective caching is keyed to what each toolchain regenerates:
| Generator | Restore each run | Cache key |
|---|---|---|
| Astro / Eleventy | node_modules or ~/.npm · ~/.pnpm-store; Astro also node_modules/.vite | lockfile hash |
| Hugo | resources/_gen (processed assets) · $HOME/.cache/hugo_cache (HUGO_CACHEDIR) | content + config hash |
| Jekyll | vendor/bundle (Bundler) · .jekyll-cache (Liquid render cache) | Gemfile.lock hash |
| Zola | the pinned binary itself; nothing else meaningful | binary version |
For the Go and Rust generators the biggest CI win is not dependency caching — there is little to cache — but pinning an exact binary version so the build is reproducible, and restoring the generated-asset layer (resources/_gen, .jekyll-cache) keyed on a content hash. That skips re-processing unchanged images and stylesheets, which is where large builds actually spend their time.
Zola’s practical advantage in CI is the absence of a dependency graph: you download one prebuilt binary and run it, so there is no node_modules restore step to get wrong and no lockfile drift. The Node generators trade that simplicity for the npm ecosystem; the cost is a dependency cache you must key correctly or pay for on every run.
# Architectural fit for target profiles
# 10k+ page internal directory, instant localization, zero-client search indexing
Build time dominates at this page count, which favours the concurrent compilers. Hugo renders 10k pages in seconds and has first-class multilingual support (per-language content directories, translation tables under i18n/, .Lang-aware linking); Zola is also fast with built-in i18n but its bundled elasticlunr index becomes a multi-megabyte client payload at this scale.
For search, decouple it from the generator and use Pagefind: it builds a fragmented, WASM-backed index after the build and loads only the fragments a query touches, so a 10k-page corpus stays responsive without shipping a monolithic index or running a search server.
A single lunr/elasticlunr index serializes the entire corpus into one JSON blob the browser must download and parse before the first query. At 10k+ pages that is multi-megabyte and blocks interaction. Pagefind’s fragmented index is the scalable alternative and is generator-agnostic.
Recommendation: Hugo (or Zola) with content/<lang>/…, plus Pagefind invoked in CI after the build. Jekyll is disqualified here on build time alone.
# Design component system showcase: static docs + live stateful components
This is the islands use case, and Astro is the direct fit. Documentation prose renders to static HTML (Markdown/MDX/Markdoc), while live component demos are authored as React/Vue/Svelte/Solid islands hydrated per directive — client:visible for below-the-fold demos, client:load for immediately interactive ones. Content Collections give the docs a typed schema, and the actual component library can be co-located in the same repo and imported directly into the showcase.
Eleventy can approximate this with WebC or shortcodes, but embedding stateful framework components is awkward compared to Astro’s first-class hydration model. Recommendation: Astro, with a typed docs collection and per-directive hydration.
# Lightweight blog, maintainable for 10+ years with minimal dependency churn
Here the dependency surface is the risk. Zola ships as one static Rust binary with no runtime dependency tree and no plugin ecosystem to rot; content is CommonMark plus TOML, and an upgrade is swapping a single binary. Hugo offers the same single-binary property with a larger template surface. The Node generators (Astro, Eleventy) carry a transitive npm graph that needs ongoing security maintenance, and Jekyll requires keeping a Ruby toolchain alive. Recommendation: Zola (or Hugo) — built-in Sass, highlighting, and Pagefind for search, with no Node build step to maintain for a decade.
# Summary
| Generator | Runtime | Client JS | Build at scale | Best-fit profile |
|---|---|---|---|---|
| Hugo | Go binary | none | excellent | large localized directories |
| Zola | Rust binary | none | excellent | long-lived low-maintenance sites |
| Astro | Node + Vite | islands only | good | interactive component showcases |
| Eleventy | Node | none | good | flexible multi-language templating |
| Jekyll | Ruby | none | poor | small GitHub Pages sites |
The migration decision reduces to three axes: build concurrency (Hugo/Zola win at page count), interactivity model (Astro wins when stateful components are first-class content), and dependency surface (single-binary Hugo/Zola win on decade-scale maintenance).
Click to show appreciation
Discussion