Table of Contents¶
- Introduction
- Test Environment
- Hugo
- Eleventy
- BSSG
- Zola
- Nikola
- Pelican
- Jekyll
- Useful SSG performance resources
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.
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
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 |
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