fun with static site generators
Introduction
Static site generators (SSG’s) are often used for blogs. SSG’s typically process Markdown files into static HTML files. The use of static HTML files (rather than dynamically generating the site using a database) offers benefits for performance and security.
I use Hugo for my blog and have used (or experimented with) the following static site generators (SSG’s).
- Hugo (Go)
- Eleventy (Javascript)
- BSSG (Bash)
- Zola (Rust)
- Nikola (Python)
- Pelican (Python)
- Jekyll (Ruby)
A throwaway post from Neil Brown on Mastodon prompted me to investigate the build performance of different static site generators with a significant volume of posts (1,000).
Neil reduced the build time for his Hugo blog simply by adding more hardware resources. A decent strategy involving minimal effort producing an immediate improvement.
‘I have upped the RAM in the VM running my Web server to a heady 1GB, and added a second CPU core’.
‘That has halved the Hugo build time for my blog [from 4 minutes to 2 minutes]’.
This comment surprised me as I also use Hugo which builds my entire blog, containing 1,000 pages, in less than two seconds on a 10 year old desktop computer.
Neil’s blog is self-hosted and uses Hugo. A ‘View Source’ reveals the version of Hugo in use. Neil is using v0.131.0 (released in August 2024).
<meta name="generator" content="Hugo 0.131.0">
The latest version of Hugo is 0.151.1 (released in October 2025). However, Neil is using the Hugo version included in the Debian (Trixie) repositories.
Reviewing the ‘Archives’ page, Neil’s blog contains 457 posts.
Neil moved his blog from HTMLy to Hugo in 2023 and was using Hugo v.0.111.3 (from the Debian repos) back then and used the Etch theme.
Looking at the Etch theme, it is clear Neil has stuck with this attractive, minimal Hugo theme since then.
Given the disparity between Neil’s build time and mine, I thought it would be fun to compare the performance of Hugo with different themes as well as other popular static site generators.
Test Environment
Hardware
- Lenovo M900 Tower Desktop (SFF)
- CPU: Intel i7-6700 CPU @ 3.40GHz (4 cores, 8 threads)
- Memory: 48GB
- Disk: 1TB
- O/S: Linux 6.17.2
Test Data
I used my personal blog as the data set for the performance tests.
- 1028 articles (Markdown)
- Content spanning twenty years (2005 to 2025)
- 116 code blocks
- 74 static images
- 29 categories
- 46 tags
Hugo
Hugo: https://gohugo.io/
Hugo is a popular static site generator written in Go with a reputation for speed and performance.
$ hugo version
hugo v0.151.1+extended+withdeploy linux/amd64 BuildDate=unknown
$ go version
go version go1.25.3 X:nodwarf5 linux/amd64
The tests ran the standard ‘hugo build’ command three times and took the average elapsed time.
| Theme | Time (secs) |
|---|---|
| Ananke | 0.615 |
| PaperMod (Base) | 1.067 |
| PaperMod (Custom) | 1.633 |
| Etch (Base) | 1.911 |
| Etch (Related) | 1.958 |
| BearBlog | 0.425 |
| Simple | 0.377 |
| Beautiful Hugo | 1.541 |
Most Hugo themes used the default, out of the box settings with no customisation.
The ‘PaperMod (Custom)’ test used my personal blog which includes additional ‘Archive’, ‘Categories’, ‘Posts’ pages and search functionality.
The timings for ‘Etch’ surprised me as it is a relatively simple theme that displays a list of all posts.
I added support for up to 15 ‘Related Posts’ using Neil’s code but saw no noticeable increase in build time (which is less than two seconds).
Hugo supports ‘Related Posts’ functionality and the list of articles is built during the build (regardless of whether it is used or not).
Ananke: https://github.com/theNewDynamic/gohugo-theme-ananke
PaperMod: https://github.com/adityatelange/hugo-PaperMod
Etch: https://github.com/LukasJoswiak/etch
BearBlog: https://github.com/janraasch/hugo-bearblog
Simple: https://github.com/maolonglong/hugo-simple/
Beautiful Hugo: https://github.com/halogenica/beautifulhugo
Hugo provides useful diagnostics about potential performance bottlenecks.
Here is the template metrics report for the Etch theme (with related posts). There are three candidate templates that could be cached (header, footer and posts).
$ hugo --templateMetrics --templateMetricsHints
Template Metrics:
cumulative average maximum cache percent cached total
duration duration duration potential cached count count template
---------- -------- -------- --------- ------- ------ ----- --------
2.691672237s 53.833444ms 83.570806ms 0 0 0 50 rss.xml
1.409607939s 1.345045ms 11.568865ms 0 0 0 1048 single.html
322.542988ms 307.77µs 2.927895ms 28 0 0 1048 _partials/related.html
126.863653ms 115.54µs 1.183336ms 44 0 0 1098 _partials/head.html
81.827628ms 40.913814ms 41.042982ms 100 0 0 2 _partials/posts.html
79.788949ms 25.193µs 363.218µs 0 0 0 3167 li.html
55.705094ms 1.160522ms 7.457174ms 0 0 0 48 _default/taxonomy.html
44.028022ms 44.028022ms 44.028022ms 0 0 0 1 index.html
43.649693ms 43.649693ms 43.649693ms 0 0 0 1 list.html
32.992786ms 32.992786ms 32.992786ms 0 0 0 1 sitemap.xml
24.19536ms 22.035µs 948.957µs 100 0 0 1098 _partials/header.html
7.410874ms 6.749µs 143.528µs 100 0 0 1098 _partials/footer.html
961.219µs 106.802µs 278.188µs 0 0 0 9 _shortcodes/figure.html
708.877µs 141.775µs 299.575µs 0 0 0 5 _markup/render-table.html.html
551.337µs 110.267µs 208.377µs 0 0 0 5 _markup/render-table.rss.xml
105.423µs 52.711µs 88.545µs 0 0 0 2 alias.html
15.147µs 15.147µs 15.147µs 0 0 0 1 /css/dark.css
1.76µs 1.76µs 1.76µs 0 0 0 1 404.html
Total in 2002 ms
Eleventy
Eleventy: https://www.11ty.dev/
Eleventy is a popular SSG written in Javascript.
Eleventy Base Blog (v9): https://github.com/11ty/eleventy-base-blog
The Eleventy Base Blog theme is minimal and not dissimilar in appearance from the Hugo PaperMod theme.
$ node --version
v20.19.5
$ npx @11ty/eleventy --version
3.1.2
Building the Eleventy blog. Eleventy doesn’t have separate ‘build’ and ‘serve’ commands.
The Eleventy build summary for my blog.
$ npx @11ty/eleventy --serve
[11ty/eleventy-img] 143 images optimized (143 deferred)
[11ty] Benchmark 1664ms 11% 1052× (Configuration) "@11ty/eleventy/html-transformer" Transform
[11ty] Copied 5 Wrote 1043 files in 14.53 seconds (13.9ms each, v3.1.2)
[11ty] Server at http://localhost:8080/
Eleventy supports incremental builds using the ‘–incremental’ parameter which only processes content modified since the last build.
Initially, I saw no difference using ‘–incremental’ but the Eleventy documentation suggested adding the ‘–ignore-initial’ option. This reduced the build time significantly from 14 seconds to sub-second.
$ npx @11ty/eleventy --serve --incremental --ignore-initial
[11ty] Copied 5 Wrote 0 files in 0.77 seconds (v3.1.2)
[11ty] Watching…
# Add a new post with tags
[11ty/eleventy-img] 3 images optimized (3 deferred)
[11ty] Wrote 32 files (skipped 1012) in 0.51 seconds (v3.1.2)
# Add more text to existing post
[11ty/eleventy-img] 3 images optimized (3 deferred)
[11ty] Wrote 32 files (skipped 1012) in 0.46 seconds (v3.1.2)
Summary of timings for Eleventy
| Theme | Time (secs) |
|---|---|
| Eleventy (Full) | 14.42 |
| Eleventy (Incremental) | 0.46 |
BSSG
BSSG - https://bssg.dragas.net/
Bash Static Site Generator (BSSG) is an SSG created by Stefano Marinelli. BSSG is written in the Bash shell
BSSG is a relatively new SSG. The first public release of BSSG was in March 2025 but there have been 14 subsequent releases.
BSSG includes a broad range of themes, support for incremental builds, parallel processing and a post article editor to manage content
BSSG doesn’t currently support ‘Categories’ so all existing ‘Categories’ were migrated to ‘Tags’.
It is possible this skewed the data set slightly and adversely affected performance as it resulted in four tags having a lot of associated posts. BSSG can list all tags with article counts using the ‘bssg.sh tags’ command.
| Tag | Count |
|---|---|
| blogging | 236 |
| football | 112 |
| software | 122 |
| UK | 260 |
$ bssg.sh
BSSG - Bash Static Site Generator (v0.32)
$ bash --version
GNU bash, version 5.3.3(1)-release (x86_64-pc-linux-gnu)
Initial BSSG build from scratch.
$ bssg.sh build
BBSG uses incremental builds and only rebuilds what has changed.
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 2,389 | Full (2,389 secs = 39 mins) |
| Default | 23 | Unchanged. |
| Default | 61 | Add new post (no tags). |
| Default | 100 | Add new post (existing tag). |
| Default | 62 | Modify existing post. |
| Default | 107 | Add existing tag to existing post. |
| Default | 83 | Add new tag to existing post. |
By default, BSSG generates related posts based on the ‘Tags’ in each post. The default number of related posts displayed is 3. If this feature is disabled, then the build time is reduced significantly.
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 169 | Full |
| Default | 12 | Unchanged. |
| Default | 63 | Add new post (no tags). |
| Default | 62 | Add new post (existing tag). |
| Default | 64 | Modify existing post. |
| Default | 62 | Add existing tag to existing post. |
| Default | 70 | Add new tag to existing post. |
BSSG also supports parallel processing using the GNU parallel shell tool. The GNU parallel package is very lightweight (< 1MB).
BSSG detects the presence of GNU parallel automatically and spawns N processes in parallel where N is the number of threads available.
Checked dependencies. Parallel available: true
GNU parallel found! Using parallel processing.
On my computer, this resulted in BSSG spawning 8 Bash processes which may have been too many as the load average climbed to between 10 and 15.
However, the elapsed time for the initial build of a blog with 1,000 posts reduces from 39 minutes to under 10 minutes.
Before you exclaim ‘10 minutes when Hugo and Eleventy are sub-second’, think about how often you completely rebuild every single post on your blog. Not very often.
The typical use case is writing a new blog post. There may be occasions when you change theme or spend two weeks consolidating all your tags and categories but, hopefully, those should be relatively rare.
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 559 | Full. Parallel. |
| Default | 32 | Unchanged. |
| Default | 60 | Add new post (no tags). |
| Default | 75 | Add new post (existing tag). |
| Default | 67 | Modify existing post. |
| Default | 73 | Add existing tag to existing post. |
| Default | 67 | Add new tag to existing post. |
Removing ‘Related Posts’ and running in parallel reduces the time for a full build to 1 minute and an incremental build to 45 seconds.
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 59 | Full. Parallel. |
| Default | 30 | Unchanged. |
| Default | 44 | Add new post (no tags). |
| Default | 44 | Add new post (existing tag). |
One big advantage of BSSG is the ability to quickly and easily change themes. You simply select a theme, modify the THEME entry in the configuration file and it just works. This is because BSSG themes use a single CSS style sheet. This may limit the functionality available but it just works.
Zola
Zola - https://www.getzola.org/
Zola is a SSG written in Rust. Like Hugo, Zola is a single executable. Like Hugo, Zola is fast. Like Hugo, changing themes in Zola is not simply a case of modifying the THEME entry in ‘config.toml’. Each theme seems to have additional, custom configuration options that need to be set.
$ zola --version
zola 0.21.0
$ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14) (Arch Linux rust 1:1.90.0-3)
Serene Theme - https://github.com/isunjn/serene
Building the blog
$ zola build
The Zola build time for 1,000 posts was so lightning fast, I had to check it actually worked !
$ zola build
Building site...
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1030 pages (0 orphan) and 1 sections
Done in 351ms.
Like Hugo, Zola also has a live development server that watches for changes to the site in real-time. This is also fast.
Building site...
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1031 pages (0 orphan) and 1 sections
Done in 309ms.
Listening for changes in zola-blog/{config.toml,content,sass,static,templates,themes}
Web server is available at http://127.0.0.1:1111 (bound to 127.0.0.1:1111)
Change detected @ 2025-10-14 13:17:09
-> Content changed zola-blog/content/posts/zola-new-post.md
Checking all internal links with anchors.
> Successfully checked 0 internal link(s) with anchors.
-> Creating 1031 pages (0 orphan) and 1 sections
Done in 283ms.
Finally I experimented with a couple more themes.
Linkita - https://www.getzola.org/themes/linkita/
BearBlog - https://www.getzola.org/themes/bearblog/
PaperMod - https://www.getzola.org/themes/papermod/
| Theme | Time (secs) | Notes |
|---|---|---|
| Serene | 0.35 | Full |
| Serene | 0.28 | Incremental |
| Linkita | 1.77 | Full |
| Linkita | 1.70 | Incremental |
| Bearblog | 0.26 | Full |
| Bearblog | 0.25 | Incremental |
| PaperMod | 20.70 | Full |
| PaperMod | 20.51 | Incremental |
Nikola
Nikola - https://getnikola.com/blog/
$ python -V
Python 3.13.7
$ nikola version
Nikola v8.3.3
Useful Nikola commands.
nikola build
nikola serve --browser
nikola auto
To force a full rebuild in Nikola, you need to remove the ‘output’ directory.
You also need to use the Linux time command to get the elapsed timings for the ’nikola build’ command.
Nikola includes the wonderful blog.txt theme (originally written for Wordpress by Scott Wallick) so kudos to Nikola’s author Roberto Alsina for that.
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 44.86 | Full |
| Default | 4.72 | Unchanged |
| Default | 5.86 | Add new post (no tags). |
| Default | 5.75 | Add new post (existing tag). |
| Default | 5.98 | Modify existing post. |
| Default | 5.87 | Add existing tag to existing post. |
| Default | 5.70 | Add new tag to existing post. |
| blogtxt | 47.34 | Full |
| blogtxt | 4.93 | Unchanged |
| blogtxt | 4.78 | Add new post (no tags). |
| blogtxt | 4.82 | Add new post (existing tag). |
Pelican
Pelican - https://getpelican.com/
Create a dedicated virtual environment for Pelican.
$ workon Pelican
(Pelican) $ python -V
Python 3.13.7
(Pelican) $ pelican --version
4.11.0
Pelican doesn’t have separate build and server commands. You simply run the development server which builds the site and watches for any changes.
(Pelican) $ pelican --autoreload --listen
Serving site at: http://127.0.0.1:8000 - Tap CTRL-C to stop
Done: Processed 1034 articles, 0 drafts, 0 hidden articles, 0 pages,
0 hidden pages and 0 draft pages in 3.65 seconds.
Add a new post (incremental build).
-> Modified: pelican-blog/content/my-pelican-post.md.
re-generating...
Done: Processed 1035 articles, 0 drafts, 0 hidden articles, 0 pages,
0 hidden pages and 0 draft pages in 2.96 seconds.
Summary
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 3.65 | Full |
| Default | 2.96 | Incremental |
Jekyll
Ruby based blog.
$ ruby -v
ruby 3.4.6 (2025-09-16 revision dbd83256b1) +PRISM [x86_64-linux]
$ bundle exec jekyll -v
jekyll 4.4.1
Jekyll base theme (minima) - https://github.com/jekyll/minima
Build the blog
$ bundle exec jekyll serve
<snip>
Run in verbose mode to see all warnings.
done in 2.788 seconds.
Auto-regeneration: enabled for '/home/andy/devel/my-jekyll-blog'
Server address: http://127.0.0.1:4000/
Live reload uses a different command and port
Run in verbose mode to see all warnings.
done in 4.7 seconds.
Auto-regeneration: enabled for 'devel/my-jekyll-blog'
LiveReload address: http://127.0.0.1:35729
Server address: http://127.0.0.1:4000/
Server running... press ctrl-c to stop.
Jekyll also produces a lot of warnings (deprecation) that clutter up the display. This is surprising (and irritating) for the latest version of Jekyll and the standard, bundled theme.
There is a ‘–quiet’ option for ‘jekyll build’ but this doesn’t appear to silence the warnings.
Attempting to access the live development server on port 35729 fails. However, the live reload is actually available on port 4000.
This port only serves livereload.js over HTTP.
Given Jekyll was first released back in 2008, Jekyll feels rather neglected and outdated to me. Tags didn’t work properly. All tags were processed and listed but the click through from an individual article as ‘404 - Not Found’ error.
Also Jekyll insists on blog posts following a naming convention (‘yyyy-mm-dd-title.md’).
| Theme | Time (secs) | Notes |
|---|---|---|
| Default | 5.091 | Full |
| Default | 1.029 | Incremental |
| Default | 8.561 | Live reload |
Useful SSG performance resources
Zach Leatherman (Eleventy lead developer) performed some performance benchmarks (in 2022) which are a useful benchmark comparing SSG’s for pure Markdown conversion throughput for large sites.
However, Zach’s tests don’t include meta-data (tags, categories, dates) so aren’t necessarily representative of a real-life blog or site.
https://www.zachleat.com/web/build-benchmark/
Generating representative test data is difficult but this Bash script scrapes a random Wikipedia page and generates Markdown (including tags and categories).
https://gist.github.com/jgreely/2338c72c825d2a93713e4f0fc0025985
Each SSG has its own format for front-matter. There are even two different formats for front matter; TOML and YAML.
Hugo has a very useful builtin conversion function to convert the Hugo front matter an all posts between the formats (including JSON).
$ hugo convert --help
Usage:
hugo convert [command]
Available Commands:
toJSON Convert front matter to JSON
toTOML Convert front matter to TOML
toYAML Convert front matter to YAML